How to build a Service Tracker Application

Introduction

The Service Tracker is an application that allows for equipment dealers to manage their customers as well as their service technicians that they need to maintain that equipment.

For example, lets say you are building a Serverless web application for a company that sells HVAC equipment through a number of Dealers. Each Dealer has their own Customers that they need to manage within the application. In addition, each Customer should be assigned Equipment which the Dealer has sold to them. To complicate it further, each Dealer would like to have their own Contractors who are used to service that Equipment. They would like to be able to schedule Appointments and issue Time Clocks for each Appointment. Normally, an application like this would present enough complexities that it would be hard to cover in a single walkthrough, but with Form.io, these kind of applications are made trivial.

Here is a video where I show off the Form.io structure of this application we will be building from scratch in this tutorial.

Source Code

All of the source code for this application is available online. We encourage you to walk through the full application, but if you would like to have this application up and running quickly, then feel free to run the application from the source code. You can also use the source code as a reference when walking through this tutorial. Click on the button below to download the source code for this application.

Setup

To get started, you will need to make sure that you have the following tools installed on your machine.

  • Form.io Account - Go to https://portal.form.io and create a new account.
  • Node.js (https://nodejs.org/en/) - We recommend using the latest LTS version.
  • Bower.js - Using your computer console, type the following
  npm install -g bower
  • Gulp - Used to build the front end application.
  npm install -g gulp
  • Yeoman - We will use this to generate the starting point for our application.
  npm install -g yo
  • Yeoman Angular-Gulp Generator - A really good generator starting point for Form.io Applications.
  npm install -g generator-gulp-angular

Create Form.io Project

After you have your computer setup with all the dependencies, the first thing you will want to do is create your project within Form.io. You may notice that there is already a Service Tracker template within Form.io, so we will just use that as our starting point and then walk through the resources & forms within this tutorial to help understand how the project is structured.

Once you create your project, you will presented with the project page overview. If you are new to Form.io, then I highly recommend reading through the User Guide to understand all of the components about our Project interface. For this tutorial, we will simply be focusing on the Resource & Forms that create the structure.

Application Structure

Before we dive into the project, it is important to note how this application is structured. This can be done by identifying all of the resources within the application, which are simply the data objects that are used to establish the application structure. We can figure out these Resources by listing our requirements and then highlighting all of the object based constructs within those requirements. The requirements are as follows with all the Resources in bold.

  • Every Dealer should have their own login to the system.
  • Each Dealer should be able to create and manage their own Customers.
  • Each Dealer should be able to create and manage their own Contractors.
  • Customers should be assigned to the Equipment they purchase and can have multiple equipment.
  • Dealers can create Appointments for those Customers and assign Contractors to them.
  • Contractors should see the Appointments assigned to them and create Time Clock and Service records against them.

From this, we can create the following Structure.

  • Dealer
    • Contractor
      • Appointment
        • Service
        • Time Clock
    • Customer
      • Appointment
        • Service
        • Time Clock
      • Equipment

From this structure, we can now establish all of the resource relationships necessary to create the application structure. Let’s walk through the Form.io resources to better understand how they are structured.

Resources

Below you will find a list of all resource within Form.io along with labels to help explain each field.

Dealer

Customer

Contractor

Appointment

Time Clock

Equipment

Service

Understand the Actions & Roles

Once you create the resources, it is typical to shift our focus over to the Actions performed and Roles assigned for each of those resources. It is recommended to read up on our Actions as well as the Roles and Permissions section before getting into this part so that you fully understand what they are and how they are utilized. To keep this walkthrough tutorial primarily focused on the application development process, I would highly recommend watching the video at the beginning of this tutorial since it goes into great detail how the Roles and Permissions as well as Actions are configured for this application.

Create some Default users

Now that your application structure is established, along with all the actions & roles, we now need to create a default user account so that we can use the application. This is a very common gotcha where people try to use their Form.io user account to login to the application. The reason this does not work is because the authentication is directed toward the Resources within that application and not the Form.io database. This means that you can have your own independent user database within your application and not have to worry about conflicting user accounts. To create a default Admin account, we simply need to go to the Forms section, click on Admin form and then fill out some information.

Creating the Application

Now that we have our resources structured the way we need them to be, the next step is to build the application that will host these resources in application form. The Service Tracker application has actually already been created and can be found @ https://github.com/formio/formio-app-servicetracker. You can easily see this application in action by going to the Preview section, and even clone this locally and get it running by walking through the Launch > Local Development section.

In this section, however, we will be focused on building this application from scratch so that all of the development pieces are fully documented as well as provide you a good base for creating your own applications on the Form.io platform. To get started, we will be using the Angular-Gulp Yeoman Generator to create our base application. To do this, we will create a new folder and then create the scaffolding using Yeoman as follows. Execute the following in your Command Prompt.

  mkdir servicetracker
  cd servicetracker
  yo gulp-angular

Walk through the Yeoman Generator providing the following configurations.

  • Which version of Angular do you want?
    • 1.4.x (stable)
  • What Angular modules would you like to have? (press spacebar to deselect)
    • angular-sanitize.js
    • angular-aria.js
  • Do you need jQuery or perhaps Zepto?
    • jQuery 2.x (new version, lighter, IE9+)
  • Would you like to use a REST resource library?
    • None, $http is enough!
  • Would you like to use a router?
    • UI Router, flexible routing with nested views
  • Which UI framework do you want?
    • Bootstrap, the most popular HTML, CSS, and JS framework
  • How do you want to implement your Bootstrap components?
    • Angular UI Bootstrap, Bootstrap components written in pure AngularJS by the AngularUI Team
  • Which CSS preprocessor do you want?
    • Sass (Node)
  • Which JS preprocessor do you want?
    • None, I like to code in standard JavaScript.
  • Which HTML template engine would you want?
    • None, I like to code in standard HTML.

This will take several minutes to install. Once it does, you can now run the application by typing the following.

  gulp serve

Which will show the following.

We can now install all the dependencies we will need to create our Service Tracker applcation.

Install Dependencies

Upgrade Angular.js First we will upgrade Angular.js to the latest 1.5 version. Make sure that when it asks you questions on which version you want that you select the one that resolves to the latest version.

  bower install --save angular
  bower install --save angular-sanitize
  bower install --save angular-aria
  bower install --save angular-bootstrap

Install Bootswatch, Font Awesome, Google Maps, and Form.io Dependencies

  bower install --save bootswatch
  bower install --save font-awesome
  bower install --save ngmap
  bower install --save ng-formio
  bower install --save ng-formio-helper

Once we have the dependencies installed, we now need to open the following file within the application, and add the following dependencies to the angular.module declaration.

/src/app/index.module.js

    angular
      .module('servicetracker', [
        'ngSanitize',
        'ngAria',
        'ui.router',
        'ui.bootstrap',
        'toastr',
        'ngMap',
        'formio',
        'ngFormioHelper'
      ]);

Restructure the Scaffolding

While the Angular-Gulp generator is a fantastic start, there are a number of things we should do to allow for a better structure, maintainability, and configurability.

Delete the overrides in bower.json

Your bower.json file should look like the following when you are done.

/bower.json

Don’t exclude bootstrap.js in Gulp task

/gulp/conf.js

  /**
   *  Wiredep is the lib which inject bower dependencies in your project
   *  Mainly used to inject script tags in the index.html but also used
   *  to inject css preprocessor deps and js files in karma
   */
  exports.wiredep = {
    exclude: [/\/bootstrap-sass\/.*\.js/, /\/bootstrap\.css/],
    directory: 'bower_components'
  };

Allow Bootswatch by changing your /app/index.scss to the following

/src/app/index.scss

  @import "bower_components/bootswatch/yeti/_variables.scss";
  @import "bower_components/bootstrap-sass/assets/stylesheets/_bootstrap.scss";
  @import "bower_components/bootswatch/yeti/_bootswatch.scss";
  @import "bower_components/font-awesome/scss/font-awesome.scss";

  .browsehappy {
    margin: 0.2em 0;
    background: #ccc;
    color: #000;
    padding: 0.2em 0;
  }

Doing this allows you to add your own variables to your application like the following.

  @import "bower_components/bootswatch/yeti/_variables.scss";
  $brand-primary:         #2780E3;
  $brand-success:         #3FB618;
  $brand-info:            #9954BB;
  $brand-warning:         #FF7518;
  $brand-danger:          #FF0039;
  @import "bower_components/bootstrap-sass/assets/stylesheets/_bootstrap.scss";
  @import "bower_components/bootswatch/yeti/_bootswatch.scss";
  @import "bower_components/font-awesome/scss/font-awesome.scss";

Allow Font-awesome and UI Grid by modifying the Gulp fonts task

/gulp/build.js

  // Only applies for fonts from bower dependencies
  // Custom fonts are handled by the "other" task
  gulp.task('fonts', function () {
    return gulp.src([
        'bower_components/bootstrap-sass/assets/fonts/**/*',
        'bower_components/font-awesome/fonts/*'
      ])
      .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
      .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/')));
  });

  // You will need to add this task...
  gulp.task('ui-grid-fonts', function () {
    return gulp.src([
        'bower_components/ng-formio-grid/dist/*',
      ])
      .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
      .pipe(gulp.dest(path.join(conf.paths.dist, '/styles/')));
  });

… … …

  // Need to add the ui-grid-fonts to the gulp tasks...
  gulp.task('build', ['html', 'fonts', 'ui-grid-fonts', 'other', 'views', 'config']);

Allow the following routes in /gulp/server.js

/gulp/server.js

  if(baseDir === conf.paths.src || (util.isArray(baseDir) && baseDir.indexOf(conf.paths.src) !== -1)) {
    routes = {
      '/bower_components': 'bower_components',
      '/fonts': 'bower_components/font-awesome/fonts',
      '/fonts/bootstrap': 'bower_components/bootstrap-sass/assets/fonts/bootstrap'
    };
  }

Add the navbar to index.html

/src/index.html

  <nav class="navbar navbar-static-top navbar-inverse">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-navbar-collapse" aria-expanded="false">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" ui-sref="home()">
          <i class="fa fa-home"></i> Service Tracker
        </a>
      </div>
      <div class="collapse navbar-collapse" id="bs-navbar-collapse">
        <ul class="nav navbar-nav navbar-right">
          <li ng-if="authenticated"><a class='navbar-link' href="#" ng-click="logout()"><i class="fa fa-power-off"></i> Logout</a></li>
        </ul>
      </div>
    </div>
  </nav>
  <div ui-view class="container"></div>

Create the views folder and the gulp task to add it to the distribution

/gulp/build.js

  gulp.task('views', function() {
    return gulp.src([
      path.join(conf.paths.src, '/views/**/*')
    ]).pipe(gulp.dest(path.join(conf.paths.dist, '/views/')));
  });

  gulp.task('build', ['html', 'fonts', 'ui-grid-fonts', 'other', 'views']);

This should complete the restructuring of the application, we can now start adding the Form.io elements to create the application.

Configuration

The next step is to create the configuration for the Service Tracker application. This contains the definition to all your forms, resources, roles, and anything else you need to configure within the application. This file looks like the following.

/src/config.js

  var APP_URL = 'https://myproject.form.io';
  var API_URL = 'https://api.form.io';

  // Parse query string
  var query = {};
  location.search.substr(1).split("&").forEach(function(item) {
    query[item.split("=")[0]] = item.split("=")[1] && decodeURIComponent(item.split("=")[1]);
  });

  var appUrl = query.appUrl || APP_URL;
  var apiUrl = query.apiUrl || API_URL;
  angular.module('servicetracker').constant('AppConfig', {
    appUrl: appUrl,
    apiUrl: apiUrl,
    company: query.company || 'Service Tracker',
    icon: query.icon || 'assets/images/logo.png',
    forms: {
      userForm: appUrl + '/user',
      userLoginForm: appUrl + '/user/login',
      appointmentForm: appUrl + '/appointment'
    },
    roles: [
      'Contractor',
      'Dealer'
    ],
    resources: {
      dealer: {
        form: appUrl + '/dealer',
        resource: 'DealerResource'
      },
      customer: {
        form: appUrl + '/customer',
        resource: 'CustomerResource'
      },
      contractor: {
        form: appUrl + '/contractor',
        resource: 'ContractorResource'
      },
      appointment: {
        form: appUrl + '/appointment',
        resource: 'AppointmentResource'
      },
      timeclock: {
        form: appUrl + '/timeclock',
        resource: 'TimeClockResource'
      },
      equipment: {
        form: appUrl + '/equipment',
        resource: 'EquipmentResource'
      },
      service: {
        form: appUrl + '/service',
        resource: 'ServiceResource'
      }
    }
  });

You will need to make sure that you replace the “myproject” in the APP_URL with the url of your project within Form.io

You will also need to add this file manually to the index.html so that it will include it during the page load.

/src/index.html

    <!-- build:js(src) scripts/vendor.js -->
    <!-- bower:js -->
    <!-- run `gulp inject` to automatically populate bower script dependencies -->
    <!-- endbower -->
    <!-- endbuild -->

    <!-- build:js({.tmp/serve,.tmp/partials,src}) scripts/app.js -->
    <!-- inject:js -->
    <!-- js files will be automatically insert here -->
    <!-- endinject -->

    <!-- inject:partials -->
    <!-- angular templates will be automatically converted in js and inserted here -->
    <!-- endinject -->
    <!-- endbuild -->

    <!-- Add the configuration -->
    <script src="config.js"></script>

As well as include a new build routine which will include it along with the distribution build.

/gulp/build.js

  gulp.task('config', function() {
    return gulp.src([
      path.join(conf.paths.src, '/config.js')
    ]).pipe(gulp.dest(path.join(conf.paths.dist, '/')));
  });

  gulp.task('build', ['html', 'fonts', 'ui-grid-fonts', 'config', 'other', 'views']);

Form.io Initialization

Now that we have our configuration in place, our next task is to initialize Form.io within the index.config.js as well as the index.run.js files.

/src/app/index.config.js

  /** @ngInject */
  function config(
    toastrConfig,
    FormioProvider,
    AppConfig
  ) {
    // Set the base url for formio.
    FormioProvider.setBaseUrl(AppConfig.apiUrl);
    FormioProvider.setAppUrl(AppConfig.appUrl);

    // Set options third-party lib
    toastrConfig.allowHtml = true;
    toastrConfig.timeOut = 3000;
    toastrConfig.positionClass = 'toast-top-right';
    toastrConfig.preventDuplicates = true;
    toastrConfig.progressBar = true;
  }

And then finally we need to register all the forms to the $rootScope of the application by registering them from the config.js file.

/src/app/index.run.js

  /** @ngInject */
  function runBlock(
    $log,
    $rootScope,
    AppConfig
  ) {
    // Allow the app to have access to configurations.
    $rootScope.config = AppConfig;

    // Add the forms to the root scope.
    angular.forEach(AppConfig.forms, function(url, form) {
      $rootScope[form] = url;
    });

    $log.debug('runBlock end');
  }

User Authentication

Now that we have our application intializing, the next step is to establish the User Authentication which will allow our Dealers, Contractors, and Admins to log into the application. To start, you will first need to create the following auth pages.

/src/views/user/auth.html

  <div class="col-md-8 col-md-offset-2">
    <div class="panel panel-primary">
      <div class="panel-heading" style="border-bottom:0; padding-bottom: 0;">
        <ul class="nav nav-tabs">
          <li role="presentation" ng-class="{active: isActive('auth.login')}"><a ui-sref="auth.login()">Login</a></li>
        </ul>
      </div>
      <div class="panel-body">
        <div class="row">
          <div class="col-lg-12">
            <div ui-view></div>
          </div>
        </div>
      </div>
    </div>
  </div>

/src/views/user/login.html

  <formio src="userLoginForm"></formio>

The userLoginForm is actually a variable on $rootScope that provides the path to the form. You can also represent this in the following format.

  <formio src="'https://myproject.form.io/user/login'"></formio>

However, since we have many forms, it is easier to read and manage when all of those paths are defined within AppConfig and then handed to the $rootScope within the index.run.js controller.

The code above is one magical piece of Form.io, which allows you to use a single line of code to embed a full working JSON powered form into your application. All of the API is hooked up for you and will execute all of the actions on the backend which includes, for the case of the login form, the Login action.

Once we have our Login form in place, we need to register them with another special provider within the ngFormioHelper called FormioAuthProvider. It is very much recommended to look at the source code of this provider, which can be found @ https://github.com/formio/ngFormioHelper/blob/master/src/ng-formio-helper.js#L631. To take advantage of this library, you add the following code to your index.config.js.

/src/app/index.config.js

  /** @ngInject */
  function config(
    toastrConfig,
    FormioProvider,
    FormioAuthProvider,
    AppConfig
  ) {
    // Set the base url for formio.
    FormioProvider.setBaseUrl(AppConfig.apiUrl);
    FormioProvider.setAppUrl(AppConfig.appUrl);

    // Initialize our FormioAuth provider states.
    FormioAuthProvider.setStates('auth.login', 'home');
    FormioAuthProvider.setForceAuth(true);
    FormioAuthProvider.register('login', 'user');
  • The FormioAuthProvider.setStates tells your application which UI Router states you would like to have as both “authenticated” and “anonymous” states. We are configuring it to say, “When they are not logged in, go to the ‘auth.login’ ui-router state, and when they login, I want them to go to the ‘home’ state.”

  • The FormioAuthProvider.setForceAuth tells your application that you require users to be authenticated. If they are not, then no matter where they navigate, they will be redirected to the “anonymous” state which was configured using the FormioAuthProvider.setStates call.

  • The FormioAuthProvider.register method registers the ui-router path of ‘login’ within the parent of ‘user’. This makes it so that you go to ‘/user/login’ it will show the login form and call the ui-router state of ‘user.login’.

The last thing we need to do is initialize the FormioAuth system, which fortunately, is very simple.

/src/app/index.run.js

  /** @ngInject */
  function runBlock(
    $log,
    $rootScope,
    AppConfig,
    FormioAuth
  ) {
    // Initialize the Form.io authentication system.
    FormioAuth.init();

    // Allow the app to have access to configurations.
    $rootScope.config = AppConfig;

    // Add the forms to the root scope.
    angular.forEach(AppConfig.forms, function(url, form) {
      $rootScope[form] = url;
    });
  • The FormioAuth.init method is used to register all of the items on the $rootScope that you need within the application. Things like $rootScope.user and $rootScope.authenticated are examples of things that this creates for you, in addition to auto navigating to certain states depending on when the user is logged in or not.

To finish this up, we now just need to create a home page for them to land on when they log in. We can just have a simple landing page for now.

/src/views/home.html

  <div class="jumbotron bg-info">
    <h2>Welcome to the Service Tracker Application</h2>
    <p>The following applications highlights how you can create an applicatoin with complex nested resource relationships.</p>
  </div>

And then we need to just change the UI Router declaration in the index.route.js file.

/src/app/index.route.js

  /** @ngInject */
  function routerConfig($stateProvider, $urlRouterProvider) {
    $stateProvider
      .state('home', {
        url: '/?',
        templateUrl: 'views/home.html'
      });

    $urlRouterProvider.otherwise('/');
  }

We can now run the following within our Command Prompt to run the application and see User Authentication working!

  gulp serve

We are now ready to start registering our Resources!

Application Resources

One of the most powerful concepts of Form.io, is that it allows you to use a Form Builder to create your Resources, but that at the same time creates the API platform needed for all of the CRUD operations for those resources. When dealing with resources within your application, it is important to note all of the resource actions are mapped directly into your application using a special tool called the FormioResourceProvider. This is provided from the ngFormioHelper library and the source code can be seen @ https://github.com/formio/ngFormioHelper/blob/master/src/ng-formio-helper.js#L34

Within the AppConfig found @ /src/config.js we have already defined a number of resources with the Forms and then the resource Class that is associated with that form. Each of these classes should be defined in separate files within the /src/app/resources folder. To do this, we can take the Resource class pattern shown within the Basic Application at the following url https://github.com/formio/formio-app-basic. I recommend reading all of the comments within this file, since it illustrates how you can create highly customized behaviors within your resources. We can take what we know here, and copy the User.js to establish all of our Resource classes as follows.

Dealer Resource

/src/app/resources/Dealer.js

  angular.module('servicetracker')
  .provider('DealerResource', function() {
    return {
      $get: function() { return null; },
      templates: {
        abstract: 'views/dealer/dealer.html',
        view: 'views/dealer/view.html'
      }
    };
  });

/src/views/dealer/dealer.html

  <ul class="nav nav-tabs" ng-if="isDealer">
    <li role="presentation" ng-class="{active: isActive('dealer.view')}"><a ui-sref="dealer.view()">View</a></li>
    <li role="presentation" ng-class="{active: isActive('dealer.customer')}"><a ui-sref="dealer.customerIndex()">Customers</a></li>
    <li role="presentation" ng-class="{active: isActive('dealer.contractor')}"><a ui-sref="dealer.contractorIndex()">Contractors</a></li>
    <li role="presentation" ng-if="isAdmin" ng-class="{active: isActive('dealer.edit')}"><a ui-sref="dealer.edit()">Edit</a></li>
    <li role="presentation" ng-if="isAdmin" ng-class="{active: isActive('dealer.delete')}"><a ui-sref="dealer.delete()">Delete</a></li>
  </ul>
  <div ui-view></div>

/src/views/dealer/view.html

  <div class="panel panel-default">
      <div class="panel-heading">
          <h3 class="panel-title">Dealer Information</h3>
      </div>
      <div class="panel-body">
          <formio-submission submission="dealer.resource" form="dealer.form"></formio-submission>
      </div>
  </div>

Customer Resource

/src/app/resources/Customer.js

  angular.module('servicetracker')
  .provider('CustomerResource', function() {
    return {
      $get: function() { return null; },
      parent: 'dealer',
      base: 'dealer.',
      templates: {
        abstract: 'views/customer/customer.html',
        view: 'views/customer/view.html'
      },
      controllers: {
        view: ['$scope', function($scope) {
          $scope.position = {lat: '40.74', lng: '-74.18'};
          $scope.customer.loadSubmissionPromise.then(function(customer) {
            if (
              customer.data.address &&
              customer.data.address.geometry &&
              customer.data.address.geometry.location
            ) {
              $scope.position.lat = customer.data.address.geometry.location.lat;
              $scope.position.lng = customer.data.address.geometry.location.lng;
            }
          });
        }]
      }
    };
  });

Notice here we provided the parent and base, but also a view controller that will allow us to populate the GPS position within a map on the Customer view page.

/src/views/customer/customer.html

  <ul class="nav nav-tabs">
      <li role="presentation" ng-class="{active: isActive('dealer.customer.view')}"><a ui-sref="dealer.customer.view()">View</a></li>
      <li role="presentation" ng-class="{active: isActive('dealer.customer.equipment')}"><a ui-sref="dealer.customer.equipmentIndex()">Equipment</a></li>
      <li role="presentation" ng-class="{active: isActive('dealer.customer.appointment')}"><a ui-sref="dealer.customer.appointmentIndex()">Appointments</a></li>
      <li role="presentation" ng-class="{active: isActive('dealer.customer.edit')}"><a ui-sref="dealer.customer.edit()">Edit</a></li>
      <li role="presentation" ng-class="{active: isActive('dealer.customer.delete')}"><a ui-sref="dealer.customer.delete()">Delete</a></li>
  </ul>
  <div ui-view></div>

/src/views/customer/view.html

  <h2>{{ currentResource.resource.data.name }}</h2>
  <p><strong>Address: </strong> {{ currentResource.resource.data.address.formatted_address }}</p>
  <p><strong>Status: </strong> {{ currentResource.resource.data.inactive ? 'Inactive' : 'Active' }}</p>
  <map ng-if="position" zoom="8" center="[{{ position.lat }}, {{ position.lng }}]"> <marker position="[{{ position.lat }}, {{ position.lng }}]" title="{{ currentResource.resource.data.address.formatted_address }}" visible></marker></map>
  <br>
  <div class="panel panel-default">
    <div class="panel-heading">
      <h3 class="panel-title">Primary Contact Information</h3>
    </div>
    <div class="panel-body">
      <ul class="list-group">
        <li class="list-group-item"><strong>Name:</strong> {{ currentResource.resource.data.firstName }} {{ currentResource.resource.data.lastName }}</li>
        <li class="list-group-item"><strong>Phone:</strong> {{ currentResource.resource.data.phoneNumber }}</li>
        <li class="list-group-item"><strong>Email:</strong> {{ currentResource.resource.data.email }}</li>
      </ul>
    </div>
  </div>

We also need to make sure that we add the Google Maps API to the index.html page.

/src/index.html

  <head>
    <meta charset="utf-8">
    <title>servicetracker</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->

    <!-- build:css({.tmp/serve,src}) styles/vendor.css -->
    <!-- bower:css -->
    <!-- run `gulp inject` to automatically populate bower styles dependencies -->
    <!-- endbower -->
    <!-- endbuild -->

    <!-- build:css({.tmp/serve,src}) styles/app.css -->
    <!-- inject:css -->
    <!-- css files will be automatically insert here -->
    <!-- endinject -->
    <!-- endbuild -->
    <script src="https://maps.google.com/maps/api/js?key=YOUR_API_KEY"></script>
  </head>

Contractor Resource

/src/app/resources/Contractor.js

  angular.module('servicetracker')
  .provider('ContractorResource', function() {
    return {
      $get: function() { return null; },
      parent: 'dealer',
      base: 'dealer.'
    };
  });

In some cases, we just need to tell the application how this resource is nested within the application. For this case, this is a very easy resource to add.

Equipment Resource

/src/app/resources/Equipment.js

  angular.module('servicetracker')
  .provider('EquipmentResource', function() {
    return {
      $get: function() { return null; },
      parent: 'customer',
      base: 'dealer.customer.',
      templates: {
        view: 'views/equipment/view.html'
      }
    };
  });

/src/views/equipment/view.html

  <div class="panel panel-success">
    <div class="panel-heading">
      <h3 class="panel-title">Equipment</h3>
    </div>
    <div class="panel-body">
      <p><strong>Serial #: </strong> {{ currentResource.resource.data.serialNumber }}</p>
      <p><strong>Model #: </strong> {{ currentResource.resource.data.modelNumber }}</p>
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Customer Information</h3>
        </div>
        <div class="panel-body">
          <ul class="list-group">
            <li class="list-group-item"><strong>Name:</strong> {{ currentResource.resource.data.customer.data.firstName }} {{ currentResource.resource.data.customer.data.lastName }}</li>
            <li class="list-group-item"><strong>Phone:</strong> {{ currentResource.resource.data.customer.data.phoneNumber }}</li>
            <li class="list-group-item"><strong>Email:</strong> {{ currentResource.resource.data.customer.data.email }}</li>
          </ul>
        </div>
      </div>
    </div>
  </div>

Appointment Resource

/src/app/resource/Appointment.js

  angular.module('servicetracker')
  .provider('AppointmentResource', function() {
    return {
      $get: function() { return null; },
      parent: 'customer',
      base: 'dealer.customer.',
      templates: {
        abstract: 'views/appointment/appointment.html',
        view: 'views/appointment/view.html'
      }
    };
  });

/src/views/appointment/appointment.html

<ul class="nav nav-tabs">
    <li role="presentation" ng-class="{active: isActive('dealer.customer.appointment.view')}"><a ui-sref="dealer.customer.view()">View</a></li>
    <li role="presentation" ng-class="{active: isActive('dealer.customer.appointment.service')}"><a ui-sref="dealer.customer.appointment.serviceIndex()">Service</a></li>
    <li role="presentation" ng-class="{active: isActive('dealer.customer.appointment.timeclock')}"><a ui-sref="dealer.customer.appointment.timeclockIndex()">Time Clock</a></li>
    <li role="presentation" ng-class="{active: isActive('dealer.customer.appointment.edit')}"><a ui-sref="dealer.customer.appointment.edit()">Edit</a></li>
    <li role="presentation" ng-class="{active: isActive('dealer.customer.appointment.delete')}"><a ui-sref="dealer.customer.appointment.delete()">Delete</a></li>
</ul>
<div ui-view></div>

/src/views/appointment/view.html

  <div class="panel panel-success">
    <div class="panel-heading">
      <h3 class="panel-title">Appointment</h3>
    </div>
    <div class="panel-body">
      <p><strong>Appointment Time: </strong> {{ currentResource.resource.data.appointmentTime | date : 'medium' }}</p>
      <p><strong>Assigned Contractor: </strong> {{ currentResource.resource.data.contractor.data.name }}</p>
      <h2>{{ currentResource.resource.data.customer.name }}</h2>
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Customer Information</h3>
        </div>
        <div class="panel-body">
          <ul class="list-group">
            <li class="list-group-item"><strong>Name:</strong> {{ currentResource.resource.data.customer.data.firstName }} {{ currentResource.resource.data.customer.data.lastName }}</li>
            <li class="list-group-item"><strong>Phone:</strong> {{ currentResource.resource.data.customer.data.phoneNumber }}</li>
            <li class="list-group-item"><strong>Email:</strong> {{ currentResource.resource.data.customer.data.email }}</li>
            <li class="list-group-item"><strong>Address:</strong> {{ currentResource.resource.data.customer.data.address.formatted_address }}</li>
          </ul>
        </div>
      </div>
    </div>
  </div>

Service Resource

/src/app/resource/Service.js

  angular.module('servicetracker')
  .provider('ServiceResource', function() {
    return {
      $get: function() { return null; },
      parent: 'appointment',
      base: 'dealer.customer.appointment.',
      templates: {
        view: 'views/service/view.html'
      }
    };
  });

/src/views/service/view.html

  <div class="panel panel-success">
    <div class="panel-heading">
      <h3 class="panel-title">Service</h3>
    </div>
    <div class="panel-body">
      <p><strong>Service Performed: </strong> {{ currentResource.resource.data.servicePerformed }}</p>
      <p><strong>Service Notes: </strong> {{ currentResource.resource.data.serviceNotes }}</p>
      <p><strong><a ui-sref="dealer.customer.appointment.view()">View Appointment Details</a></strong></p>
    </div>
  </div>

TimeClock Resource

/src/app/resources/TimeClock.js

  angular.module('servicetracker')
  .provider('TimeClockResource', function() {
    return {
      $get: function() { return null; },
      parent: 'appointment',
      base: 'dealer.customer.appointment.',
      templates: {
        view: 'views/timeclock/view.html'
      },
      controllers: {
        create: ['$scope', 'Geolocation', function($scope, Geolocation) {
          Geolocation.getCurrentPosition()
            .then(function(data) {
              if (!data || !data.coords || !data.coords.longitude || !data.coords.latitude) return;
              $scope.submission.data.location = [data.coords.latitude, data.coords.longitude];
            })
            .catch(function(err) {
              $scope.submission.data.location = [0, 0];
            });
        }]
      }
    };
  });

There is a really cool feature exposed with the Time Clock resource, where it captures the GPS coordinates of the contractor at the time that they submit the time clock. This utilizes a Geolocation factory that we provided within the index.module.js file as follows.

/src/app/index.module.js

  angular
    .module('servicetracker', [
      'ngSanitize',
      'ngAria',
      'ui.router',
      'ui.bootstrap',
      'toastr',
      'formio',
      'ngFormioHelper'
    ])
    .factory('Geolocation', ['$q', '$window', function($q, $window) {
      return {
        getCurrentPosition: function() {
          var deferred = $q.defer();

          if (!$window.navigator.geolocation) {
            return deferred.reject('Geolocation not supported.');
          }

          $window.navigator.geolocation.getCurrentPosition(
            function(position) {
              deferred.resolve(position);
            },
            function(err) {
              deferred.reject(err);
            }
          );

          return deferred.promise;
        }
      };
    }]);

And the view that shows this map is as follows.

/src/views/timeclock/view.html

  <div class="panel panel-success">
    <div class="panel-heading">
      <h3 class="panel-title">Time Entry</h3>
    </div>
    <div class="panel-body">
      <p><strong>Time: </strong> {{ currentResource.resource.data.time | date : 'medium' }}</p>
      <p><strong>Activity: </strong> {{ currentResource.resource.data.activity }}</p>
      <p><strong>Contractor: </strong> {{ currentResource.resource.data.appointment.data.contractor.data.name }}</p>
      <p><strong>Appointment: </strong> {{ currentResource.resource.data.appointment.data.appointmentTime | date : 'medium' }}</p>
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Customer Information</h3>
        </div>
        <div class="panel-body">
          <ul class="list-group">
            <li class="list-group-item"><strong>Name:</strong> {{ currentResource.resource.data.appointment.data.customer.data.firstName }} {{ currentResource.resource.data.appointment.data.customer.data.lastName }}</li>
            <li class="list-group-item"><strong>Phone:</strong> {{ currentResource.resource.data.appointment.data.customer.data.phoneNumber }}</li>
            <li class="list-group-item"><strong>Email:</strong> {{ currentResource.resource.data.appointment.data.customer.data.email }}</li>
          </ul>
        </div>
      </div>
      <div ng-if="currentResource.resource.data.location">
        <p><strong>Location: </strong> {{currentResource.resource.data.location[0] }}, {{currentResource.resource.data.gpsLongitude[1] }}</p>
        <map zoom="8" center="{{ currentResource.resource.data.location }}"> <marker position="{{ currentResource.resource.data.location }}" visible></marker></map>
      </div>

    </div>
  </div>

Add the navigation to home page.

We now need to register all of the navigation into the home page so that they can get to the main resources.

/src/views/home.html

  <ul class="nav nav-tabs" ng-if="authenticated">
    <li role="presentation" ng-class="{active: isActive('home')}"><a ui-sref="home()"><i class="fa fa-home"></i></a></li>
    <li ng-if="!isAdmin && isContractor" role="persentation" ng-class="{active: isActive('appointments')}"><a ui-sref="appointments()">Appointments</a></li>
    <li ng-if="!isAdmin && isContractor" role="presentation" ng-class="{active: isActive('dealer.customerIndex')}"><a ui-sref="dealer.customerIndex({dealerId: user.data.dealer._id})">Customers</a></li>
    <li ng-if="!isAdmin && isDealer" role="presentation" ng-class="{active: isActive('dealer.customerIndex')}"><a ui-sref="dealer.customerIndex({dealerId: user._id})">Customers</a></li>
    <li ng-if="!isAdmin && isDealer" role="presentation" ng-class="{active: isActive('dealer.contractorIndex')}"><a ui-sref="dealer.contractorIndex({dealerId: user._id})">Contractors</a></li>
    <li ng-if="isAdmin" role="presentation" ng-class="{active: isActive('dealerIndex')}"><a ui-sref="dealerIndex()">Dealers</a></li>
  </ul>
  <div class="jumbotron bg-info">
    <h2>Welcome to the Service Tracker Application</h2>
    <p>The following applications highlights how you can create an applicatoin with complex nested resource relationships.</p>
  </div>

Resource Registration

Now that we have defined our resources, we now need to register them with the FormioResourceProvider so that it can register all of the ui router paths that support the structure of the application. All of the classes that we just got through building are what we pass into the FormioResourceProvider and we can then iterate over all the resources provided from the AppConfig to initialize these resources. We can do this with the following code.

/src/app/index.route.js

  /** @ngInject */
  function routerConfig(
    $stateProvider,
    $urlRouterProvider,
    FormioResourceProvider,
    AppConfig,
    $injector
  ) {
    $stateProvider
      .state('home', {
        url: '/?',
        templateUrl: 'views/home.html'
      });

    // Register all of the resources.
    angular.forEach(AppConfig.resources, function(resource, name) {
      FormioResourceProvider.register(name, resource.form, $injector.get(resource.resource + 'Provider'));
    });

    $urlRouterProvider.otherwise('/');
  }

Once you do that, you should then be able to restart your application by closing it out, and the re-running the following command.

  gulp serve

And you are done! You should now be able to see the full working application running Serverless within your browser!

We hope that this walkthrough demonstration really illustrates the power of Form.io. In just a short walkthrough, we were able to create a very complex Serverless Enterprise application that incorporates a number of nested resources, mapping, as well as total separtion between the front end application and the backend API Server!

If you have any questions, please do not hesitate to ask @ support@form.io.

Enjoy Form.io!

  • The Form.io Team