March 30, 2016
  • All
  • Ionic

Cross-Platform Apps with Ionic and Stamplay

Isaiah Grey

Ionic makes it easier to build a multi-platform mobile app using modern web technologies. Stamplay allows developers to use APIs as building blocks to to rapidly construct scalable, maintainable backend applications.

By using Stamplay and Ionic together, you can focus on building a unique app, instead of spending time scaling architecture or implementing the same feature two or three times across different platforms.

To help you get started with Ionic and Stamplay, we’ve put together a starter kit to jumpstart your mobile app development. In this article, we’ll show you how to use the code in this starter kit to build a cross-platform personal to-do app with user registration.

cover

What is Stamplay?

Stamplay is a Backend as a service that allows you to integrate with third-party APIs using a web-based GUI that allows you to create server side logic and workflows without needing to code anything. Stamplay integrates with many services including Twillio, Slack, Firebase, and Stripe.

Stamplay Integrations

Getting Started with Ionic and Stamplay: Set up the Starter Project

First, clone Ionic’s GitHub repository or download from GitHub:

git clone https://github.com/Stamplay/stamplay-ionic-starter.git

Switch to the new directory:

cd stamplay-ionic-starter

Install Ionic SDK through NPM:

npm install -g ionic

Install Stamplay CLI through NPM

npm install -g stamplay-cli

Install Gulp through NPM

npm install -g gulp

Install dependencies through NPM:

npm install && gulp install

Add/Update Ionic library files

ionic lib update

Setup Stamplay App

To continue, you will need to create a Stamplay account. After you have done this, you’ll want to login to the Stamplay Editor and create an app.

Create Stamplay App

Back on your machine, you’ll want to initialize Stamplay and enter your Stamplay AppId and APIKey when prompted, which can be found on the Stamplay Editor:

stamplay init

stamplay appid and apikey

stamplay init

Inside www/index.html, we will update Stamplay.init("YOUR APP ID") to include our APP ID.

<script>
  Stamplay.init("test-ionic-app");
</script>

Inside the Stamplay Editor, add a new object schema called “task” with the following properties:

  • title – string
  • body – string
  • complete – boolean

New Object

Task Object

Finally, on your machine, run ionic serve to launch your app in the browser. An important detail here is to make sure to use port 8080, or the app will not work.

ionic serve --lab -p 8080

Now, the application is set up, and you are ready to begin development.

Basic Configuration

Since your development environment is already set up, with your dependencies installed, you can go ahead and open up your www/js/app.js file, where your application is configured and bootstrapped.

<br />angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])

.run(function($ionicPlatform, $rootScope, AccountService) {

  AccountService.currentUser()
    .then(function(user) {
      $rootScope.user = user;
    })

  $ionicPlatform.ready(function() {
    if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
      cordova.plugins.Keyboard.disableScroll(true);
    }
    if (window.StatusBar) {
      StatusBar.styleDefault();
    }
  });

})

.constant('$ionicLoadingConfig', {
  template: "<ion-spinner></ion-spinner>",
  hideOnStateChange : false
})

.config(function($stateProvider, $urlRouterProvider) {

  $stateProvider

    .state('home', {
      url: '/',
      templateUrl: 'templates/home.html'
    })
    .state('login', {
      url: '/login',
      templateUrl: 'templates/login.html',
      controller: "AccountController",
      controllerAs : "account"
    })
    .state('signup', {
      url: '/signup',
      templateUrl: 'templates/signup.html',
      controller: "AccountController",
      controllerAs : "account"
    })
    .state('tasks', {
      cache : false,
      url: '/tasks',
      templateUrl: 'templates/tasks.html',
      controller: "HomeController",
      controllerAs : "task"
    })
    .state('new', {
      url: '/new',
      templateUrl: 'templates/new.html',
      controller: "TaskController",
      controllerAs : "new"
    })
    .state('edit', {
      url: '/task/:id',
      templateUrl: 'templates/edit.html',
      controller: "TaskController",
      controllerAs : "edit"
    })

  $urlRouterProvider.otherwise('/');

});

Up at the top of your app.js, inject the $ionicPlatform, $rootScope, and AccountService.

The $ionicPlatform is used to detect which platform the application is running on and to configure accordingly.

We brought in $rootScope to set a global value after the AccountService fetches the currently logged in user, if one exists.

After .run, we set up a constant, to avoid repeating ourselves throughout the application, when using the $ionicLoading service. We’ve taken its configuration provider, $ionicLoadingConfig and set defaults, so now every time this service is called in our application, it will have these settings.

Last, you have your .config block, where your application state is set up.

Note: The state tasks, options object, has a property cache that is set to false. This is because we do not want this state to load possibly stale data when we go to and from. This will cause the controller and view to re-render each time the state is entered.

User Accounts

Integrating local user accounts with Ionic and Stamplay is simple. By default, an API for managing user accounts and roles is set up within Stamplay for your application.

In the bootstrapping phase of your application, inside the .run block, we’ve already seen a method on the AccountService. The currentUser method fetches the user who is currently logged in.

So in our .run, we are checking and setting a user globally if one is logged in. Here is the AccountService setup:

<br />angular.module('starter.services', [])
.factory('AccountService', ["$q", function($q) {
  return {
    currentUser : function() {
      var def = $q.defer();
      Stamplay.User.currentUser()
        .then(function(response) {
          if(response.user === undefined) {
            def.resolve(false);
          } else {
            def.resolve(response.user);
          }
        }, function(error) {
          def.reject();
        }
      )
      return def.promise;
    }
  }
}])

If a user is logged in, the global value of the $rootScope user is set to the user data from Stamplay. Otherwise, the value is set to false.

It’s important that this is set before the view loads, so the correct blocks of HTML will render to the type of user using the application: either a logged in user or a guest user.

The global $rootScope.user value will first be used to display options based on if a user is logged in or not. This can be seen on the home.html template:

<ion-view>
  <ion-content class="home">
    <div>
      <p class="home-title center-align">Create better apps faster</p>
      <p class="home-subtitle center-align">Build complete, secure and scalable backends in your browser piecing together APIs and features as building blocks.</p>
    </div>
    <div class="card">
      <div class="item item-body center-align">
        <p class="home-desc">
          This is a starter kit for <a href="https://stamplay.com" target="_blank">Stamplay</a> using the Ionic framework. It features simple to-do like capabilities, with user accounts.
        </p>
      </div>
    </div>
    <div ng-hide="user === undefined">
      <div ng-hide="user">
        <div>
          <button ui-sref="login" class="button button-full button-balanced bold">Login</button>
          <button ui-sref="signup" class="button button-full button-positive bold">Signup</button>
        </div>
        <div class="center-align">
          <b class="or">or</b>
        </div>
        <div>
          <button ui-sref="tasks" class="button button-full button-dark bold">Continue as Guest</button>
        </div>
      </div>
      <div ng-show="user" ng-controller="AccountController as account">
        <button ui-sref="tasks" class="button button-full button-dark bold">My Tasks</button>
        <button ng-click="account.logout()" class="button button-full button-stable bold">Logout</button>
      </div>
    </div>
  </ion-content>
</ion-view>

Throughout the views, you can use common Angular directives, like ng-hide, ng-show, and ng-click. The ng-show and ng-hide are quite simple. The block on which these attributes are will show or hide, depending on the truthiness of the value it is assigned.

For example, if ng-show is set to ng-show="true", that block will render into view. However, if it is false, that block will be hidden.

Vice-versa, ng-hide will hide a block or elements if the value set to it is true, although it doesn’t have to be true, just not a falsey value, such as null or 0.

In the above code, you are using the ng-hide directive to hide both blocks of options with a containing element, until the $rootScope.user variable is defined, regardless of whether is truthy or falsey.

After the $rootScope.user variable is defined, the two child blocks with these directives will either show or hide, and in this case, only one will be display at a time.

If the $rootScope.user is not populated with user data and set to false, then the first block of options to Login, Signup, or Continue As A Guest will be displayed.

Otherwise, a user is logged in, and we will display the block with the option to Logout or go to My Tasks.

The login, signup, and home states share the AccountController. This controller has three main methods:

angular.module('starter.controllers', [])
.controller('AccountController', ["AccountService", "$state", "$rootScope", "$ionicLoading", "$ionicPopup",
  function(AccountService, $state, $rootScope, $ionicLoading, $ionicPopup) {

  var vm = this;

  var errorHandler = function(options) {
    var errorAlert = $ionicPopup.alert({
      title: options.title,
      okType : 'button-assertive',
      okText : "Try Again"
    });
  }

  vm.login = function() {
    $ionicLoading.show();
    Stamplay.User.login(vm.user)
    .then(function(user) {
      $rootScope.user = user;
      $state.go("tasks");
    }, function(error) {
      $ionicLoading.hide();
      errorHandler({
        title : "<h4 class='center-align'>Incorrect Username or Password</h4>"
      })
    })
  }

  vm.signup = function() {
    $ionicLoading.show();
    Stamplay.User.signup(vm.user)
    .then(function(user) {
      $rootScope.user = user;
      $state.go("tasks");
    }, function(error) {
      errorHandler({
        title : "<h4 class='center-align'>A Valid Email and Password is Required</h4>"
      })
      $ionicLoading.hide();
    })
  }

  vm.logout = function() {
    $ionicLoading.show();
    var jwt = window.location.origin + "-jwt";
    window.localStorage.removeItem(jwt);
    AccountService.currentUser()
    .then(function(user) {
      $rootScope.user = user;
      $ionicLoading.hide();
    }, function(error) {
      console.error(error);
      $ionicLoading.hide();
    })
  }
}])

Login

The login method is used within the login state. The view for your login state includes a basic form that takes in a username and password. See login.html below:

<ion-view>
  <ion-content>
    <h1 class="account-title center-align">Login</h1>

    <div class="list">
      <label class="item item-input">
        <input type="text" placeholder="Email" ng-model="account.user.email">
      </label>
      <label class="item item-input">
        <input type="text" placeholder="Password" ng-model="account.user.password">
      </label>
    </div>

    <button class="button button-balanced button-full" ng-click="account.login()">Submit</button>
  </ion-content>
</ion-view>

Once submitted, the Stamplay SDK sends the email and password to Stamplay to authenticate a user and grants a JSON Web Token if successful.

This is stored in localStorage under the key hostname-jwt.

Signup

The signup method is used within the signup state of our application. The view for your signup state is a basic form similar to your login state view.

<ion-view>
  <ion-content>
    <h1 class="account-title center-align">Signup</h1>

    <div class="list">
      <label class="item item-input">
        <input type="text" placeholder="Email" ng-model="account.user.email">
      </label>
      <label class="item item-input">
        <input type="text" placeholder="Password" ng-model="account.user.password">
      </label>
    </div>

    <button class="button button-positive button-full" ng-click="account.signup()">Submit</button>
  </ion-content>
</ion-view>

On submission, the new account credentials are sent to Stamplay, an account is created, and a JSON Web Token is granted.

Again, this is stored in localStorage under the key hostname-jwt.

Logout

The logout method is used within home.html. This option is only displayed when a user is logged in. When the method is called, the JSON Web Token is removed from localStorage, the AccountService then fetches the current user, and the value of $rootScope.user is set again.

You can see this at the bottom of AccountController.

Tasks

The task state is accessible through the home state. Either option, Continue As A Guest or My Tasks on the home view, will navigate to the tasks state the same way.

Fetching tasks

The HomeController, connected to the tasks state, triggers a vm.fetch method on load.

This is done through an ng-init directive inside the tasks.html template. (See below)

<ion-view>
  <ion-nav-bar align-title="center" class="bar-light">
    <ion-nav-back-button></ion-nav-back-button>
    <ion-nav-title>
      TASKS {{ '(' + task.tasks.length + ')' }}
    </ion-nav-title>
    <ion-nav-buttons side="right">
      <a class="button button-icon icon ion-plus-circled" ui-sref="new"></a>
    </ion-nav-buttons>
  </ion-nav-bar>
  <ion-content ng-init="task.fetch()">
    <h1 class="account-title center-align">
      <div ng-if="user">
        Your Tasks
      </div>
      <div ng-if="!user && task.tasks !== undefined">
       Community Tasks
      </div>
    </h1>
    <ion-list can-swipe="true">
      <ion-item ng-repeat="item in task.tasks" ng-class="{ 'active' : item.id === task.active }">
        <div class="row">
          <div class="col col-20" style="display: flex;justify-content: flex-end;align-items: center;">
            <button class="button button-clear" style="font-size:2rem !important" ng-click="task.setStatus(item)">
              <i class="ion ion-checkmark-circled" ng-class="{ 'balanced' : item.complete }"></i>
            </button>
          </div>
          <div class="col col-80" on-tap="task.setActive(item._id)">
            <h2>{{ item.title }}</h2>
            <p>{{ item.body }}</p>
            <label class="dark">
              {{ item.dt_create | date : "short"}}
            </label>
        </div>
      </div>
      <ion-option-button class="button-energized ion-edit"
      ui-sref="edit({ id : item._id })"></ion-option-button>
      <ion-option-button class="button-assertive ion-trash-a"
      ng-click="task.deleteTask(item._id)"></ion-option-button>
      </ion-item>
    </ion-list>
    <div class="card" ng-show="task.tasks.length === 0">
      <div class="item item-body center-align">
        <h2 class="dark">
          Umm..it's empty in here..
        </h2>
        <h2 class="dark">Try adding a task by clicking the <i class="ion-plus-circled dark"></i>
        icon in the top right corner.</h2>
      </div>
    </div>
  </ion-content>
</ion-view>

If the value of $rootscope.user is false, the TaskService will execute the method to retrieve all the tasks that do not have an owner (see getGuestTasks method on the TaskService).

Otherwise, the TaskService executes the method, getUserTasks, and fetches all the tasks that the currently logged in user has created. The HomeController and TaskService can be seen below.

<br />.controller('HomeController', ["TaskService", "$ionicLoading", "$rootScope", "$state", function(TaskService,  $ionicLoading, $rootScope, $state) {
  var vm = this;

  var findIndex = function(id) {
    return vm.tasks.map(function(task) {
      return task._id;
    }).indexOf(id);
  }

  // Display loading indicator onload
  $ionicLoading.show();

  // Fetch Tasks
  vm.fetch = function() {
    if(!$rootScope.user) {
      // Get all tasks for guests.
      TaskService.getGuestTasks()
      .then(
        function(response) {
          var tasks = response.data;
          vm.tasks = [];
          tasks.forEach(function(item, idx, array) {
            item.dt_create = new Date(item.dt_create).getTime();
            vm.tasks.push(array[idx]);
          });
          $ionicLoading.hide();
        }, function(error) {
          $ionicLoading.hide();
        })
      } else {
        // Get only the user signed in tasks.
        TaskService.getUsersTasks()
        .then(
          function(response) {
            var tasks = response.data;
            vm.tasks = [];
            tasks.forEach(function(item, idx, array) {
              item.dt_create = new Date(item.dt_create).getTime();
              vm.tasks.push(array[idx]);
            });
            $ionicLoading.hide();
          }, function(error) {
            $ionicLoading.hide();
          })
        }
      }

      // Mark Complete a task.
      vm.deleteTask = function(id) {
        $ionicLoading.show();
        vm.tasks.splice(findIndex(id), 1);
        TaskService.deleteTask(id)
        .then(function() {
          $ionicLoading.hide();
        }, function(error) {
          $ionicLoading.hide();
        })
      }

      vm.setStatus = function(task) {
        task.complete = task.complete ? !task.complete : true;
        TaskService.patchTask(task)
        .then(function(task) {
        }, function(error) {
        })
      }
}])

Task Service

The TaskService contains the logic to manipulate the application data regarding tasks. This service is shared between most states in which you are dealing with task data.

.factory('TaskService', ["$rootScope", "$q", function($rootScope, $q) {

  return {
    getGuestTasks : function(query) {
      var deffered = $q.defer();
      Stamplay.Query("object", "task")
      .notExists("owner")
      .exec()
      .then(function(response) {
        deffered.resolve(response)
      }, function(error) {
        deffered.reject(err);
      })
      return deffered.promise;
    },

    getUsersTasks : function(query) {
      var deffered = $q.defer();

      Stamplay.Object("task")
      .findByCurrentUser(["owner"])
      .then(function(response) {
        deffered.resolve(response)
      }, function(err) {
        deffered.reject(err);
      })
      return deffered.promise;
    },

    getTask : function(id) {
        var deffered = $q.defer();
        Stamplay.Object("task").get({ _id : id })
        .then(function(response) {
          deffered.resolve(response)
        }, function(error) {
          deffered.reject(err);
        })
        return deffered.promise;
    },

    addNew : function(task) {
      var deffered = $q.defer();

      Stamplay.Object("task").save(task)
      .then(function(response) {
        deffered.resolve(response)
      }, function(err) {
        deffered.reject(err);
      })
      return deffered.promise
    },
    deleteTask : function(id) {
      var deffered = $q.defer();
      Stamplay.Object("task").remove(id)
      .then(function(response) {
        deffered.resolve(response)
      }, function(err) {
        deffered.reject(err);
      })
      return deffered.promise;
    },
    updateTask : function(task) {
      var deffered = $q.defer();
      Stamplay.Object("task").update(task._id, task)
      .then(function(response) {
        deffered.resolve(response)
      }, function(err) {
        deffered.reject(err);
      })
      return deffered.promise;
    },
    patchTask : function(task) {
      var deffered = $q.defer();
      Stamplay.Object("task").patch(task._id, { complete: task.complete})
      .then(function(response) {
        deffered.resolve(response)
      }, function(err) {
        deffered.reject(err);
      })
      return deffered.promise;
    }

  }
}]);

Deleting Tasks

To delete a Task, slide the item to the left, and select the delete option from the two revealed.

This will trigger the TaskService method to remove a record from Stamplay by its _id passed to it.

After its success, simply splice the record from the array of Tasks that still exist in your state memory.

The TaskService deleteTask method takes an id and uses the Stamplay.Object("task").remove() method to delete the record from Stamplay.

Marking Tasks Complete

Marking Tasks complete/incomplete is done by simply updating the record’s complete field. You can assign it to the opposite of what the current value is every time the checkmark button is selected.

Starting with a falsey value will allow you to mark it complete, and if you wish to mark it incomplete, you can use the same method to accomplish this. The method inside of your HomeController that you can use to do this is vm.setStatus.

The TaskService patchTask method takes an task and uses the patch() method to partially update the record from Stamplay. Here, you only update the complete field on the record. Use the Stamplay.Object("task").patch() method to accomplish this.

Updating A Task

After the initial tasks are loaded, from here, the methods to create, update, and delete tasks are the same across guest and user accounts.

The methods pertaining to updating the content of a task and creating a new task are, however, in a separate state, and will be attached to the TaskController (See below) between both states.

.controller('TaskController', ["TaskService", "$ionicLoading", "$rootScope", "$state", "$stateParams", function(TaskService,  $ionicLoading, $rootScope, $state, $stateParams) {
  var vm = this;

  if($stateParams.id) {
    $ionicLoading.show();
    TaskService.getTask($stateParams.id)
      .then(function(task) {
        $ionicLoading.hide();
        vm.task = task.data[0];
      }, function(err) {
        $ionicLoading.hide();
        console.error(err);
      })
  }

  // Add a task.
  vm.add = function() {
    $ionicLoading.show();
    TaskService.addNew(vm.task)
    .then(function(task) {
      $ionicLoading.hide();
      $state.go("tasks", {}, { reload: true });
    }, function(error) {
      $ionicLoading.hide();
    })
  }

  vm.save = function() {
    $ionicLoading.show();
    TaskService.updateTask(vm.task)
    .then(function(task) {
      $ionicLoading.hide();
      $state.go("tasks", {}, { reload: true });
    }, function(error) {
      $ionicLoading.hide();
    })
  }

}])

To update a task, you must slide the task to the left, after which two actions will be revealed: edit and delete.

Once you select edit, you will be taken to the edit state, where the fields will be populated with the existing data from the task you selected.

This is just simple data-binding to a view-model that on the click of submit will trigger the TaskService method, updateTask(), which calls the Stamplay.Object("task").update() method on the SDK to update the whole record.

Creating New Tasks

To create a new task, select the plus icon in the top right corner, and navigate to the new state, where you may submit a new record to Stamplay.

This is also very basic, just data-binding to a view-model and, on the click of the submit button, executing the TaskService method to create a new record in Stamplay.

To save new data to Stamplay, we simply use the Stamplay.Object("task").save(), and pass in a valid model as the first parameter as seen in our TaskService.

Conclusion

Using Ionic, this starter kit, and the Stamplay backend, you now have a fully functioning task application, without a single line of backend code! You can use Ionic and Stamplay to quickly build great mobile apps and integrate with a variety of well known APIs.


Isaiah Grey