Cross-Platform Apps with Ionic and Stamplay
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.
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.
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.
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
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
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.