Cross Platform Storage and Sync with Ionic Framework, Couchbase, and PouchDB
An example of how to switch the embedded NoSQL database powering your app.
The Ionic Framework is still one of the leaders in hybrid mobile application development. It allows you to create Android and iOS applications using only HTML, JavaScript, and CSS.
Previously I wrote about how to use Couchbase in an Ionic Framework mobile Android and iOS application, but it made use of Couchbase Lite as it's embedded NoSQL database. This time around we're going to look at replacing Couchbase Lite with PouchDB. Should you use one method over the other? No, it comes down to preference in the end.
If you haven't already seen my post regarding PouchDB and AngularJS with Couchbase, I encourage you to have a look as this tutorial will be using many of the same concepts and code.
What We'll Need
There are a few requirements to the application we're going to build. We'll see how to obtain them along the way, but here is a taste so you know what you're getting yourself into.
- Couchbase Sync Gateway
- PouchDB 4
- Ionic Framework 1
Getting the Couchbase Sync Gateway
This project will require the Couchbase Sync Gateway in order to succeed. If you're unfamiliar, the Couchbase Sync Gateway is a middleman service that handles processing data between the local application (your Ionic Framework application) and the Couchbase Server. We won't be using Couchbase Server in this example so the Sync Gateway will act as our in-memory storage solution in the cloud.
The Couchbase Sync Gateway can be found via the Couchbase downloads section.
Creating Our Ionic Framework Project
Before going any further it is good to note that if you're not using a Mac, you cannot add and build for the iOS platform. Windows, Mac, and Linux computers can build for Android, but only Mac can build for iOS.
From the Command Prompt (Windows) or Terminal (Mac and Linux), execute the following command to create a new Ionic Framework project:
ionic start PouchProject blank
cd PouchProject
ionic platform add android
ionic platform add ios
Our blank template project is now ready for working with.
Including The Dependencies
If you haven't already, download PouchDB 4 and make note of the min.js file as we'll be using it through the project. Copy the PouchDB min.js file into your Ionic project's www/js directory.
With the file in place, open your project's www/index.html file and include the following:
<script src="js/pouchdb-4.0.3.min.js"></script>
This script line should appear above the app.js include line and the version information should match that of your actual file rather than the version I included here.
Modifying The Index File
Before we jump into the AngularJS code we need to make a final revision to the project's www/index.html file. Open it and replace the <body> tags with the following:
<body ng-app="starter">
<ion-pane>
<ion-nav-bar class="bar-stable"></ion-nav-bar>
<ion-nav-view></ion-nav-view>
</ion-pane>
</body>
Because we're using the AngularJS UI-Router that ships with Ionic Framework, we only need a basic www/index.html file.
Creating Our PouchDB AngularJS Service
Before we start using PouchDB, we need to make a wrapper for it so it fits nicely with AngularJS and Ionic Framework. Out of the box PouchDB is a vanilla JavaScript library, so it isn't necessarily the easiest to use when it comes to AngularJS.
Inside your project's www/js/app.js file, include the following service code:
.service("$pouchDB", ["$rootScope", "$q", function($rootScope, $q) {
var database;
var changeListener;
this.setDatabase = function(databaseName) {
database = new PouchDB(databaseName);
}
this.startListening = function() {
changeListener = database.changes({
live: true,
include_docs: true
}).on("change", function(change) {
if(!change.deleted) {
$rootScope.$broadcast("$pouchDB:change", change);
} else {
$rootScope.$broadcast("$pouchDB:delete", change);
}
});
}
this.stopListening = function() {
changeListener.cancel();
}
this.sync = function(remoteDatabase) {
database.sync(remoteDatabase, {live: true, retry: true});
}
this.save = function(jsonDocument) {
var deferred = $q.defer();
if(!jsonDocument._id) {
database.post(jsonDocument).then(function(response) {
deferred.resolve(response);
}).catch(function(error) {
deferred.reject(error);
});
} else {
database.put(jsonDocument).then(function(response) {
deferred.resolve(response);
}).catch(function(error) {
deferred.reject(error);
});
}
return deferred.promise;
}
this.delete = function(documentId, documentRevision) {
return database.remove(documentId, documentRevision);
}
this.get = function(documentId) {
return database.get(documentId);
}
this.destroy = function() {
database.destroy();
}
}])
You might be thinking that code looks familiar. Well, it is the exact code I used in the previous PouchDB example for AngularJS. Now we can easily use PouchDB in our project.
Creating A Local Database And Start Syncing
The goal here is to create a local database when our application starts (if it doesn't already exist) and then start syncing with the Couchbase Sync Gateway. This can be accomplished in the AngularJS run() function of our www/js/app.js file:
.run(function($ionicPlatform, $pouchDB) {
$ionicPlatform.ready(function() {
if(window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if(window.StatusBar) {
StatusBar.styleDefault();
}
});
$pouchDB.setDatabase("nraboy-test");
if(ionic.Platform.isAndroid()) {
$pouchDB.sync("http://192.168.57.1:4984/test-database");
} else {
$pouchDB.sync("http://localhost:4984/test-database");
}
})
The IP addresses I used might vary for you in terms of simulators, but for production they will likely match for both iOS and Android.
Designing A Controller For Your Views
We haven't created our views yet, but let's go ahead and create the controller logic for them. Open your project's www/js/app.js file and include the following controller:
.controller("MainController", function($scope, $rootScope, $state, $stateParams, $ionicHistory, $pouchDB) {
$scope.items = {};
$scope.save = function(firstname, lastname, email) { }
$scope.delete = function(id, rev) { }
$scope.back = function() { }
})
As of right now we have a basic controller. We know we'll be saving and deleting items which is why we've defined a function for such tasks. We also have a function called back() that will pop an item (go back) in the history stack.
Let's go bottom up and start with the back() function. It should contain the following code:
$scope.back = function() {
$ionicHistory.goBack();
}
When it comes to deleting items from the database we'll need to provide a particular document id to delete as well as the particular revision we wish to delete. This will all be passed from the views, but the logic will be as follows:
$scope.delete = function(id, rev) {
$pouchDB.delete(id, rev);
}
The delete(id, rev) function makes a call to the PouchDB service that we made.
This leaves us with the save() function. Based on the simplicity of our application we'll only be saving three data properties, but it can easily be changed should you need to. Inside your controller, make the save() function like so:
$scope.save = function(firstname, lastname, email) {
var jsonDocument = {
"firstname": firstname,
"lastname": lastname,
"email": email
};
if($stateParams.documentId) {
jsonDocument["_id"] = $stateParams.documentId;
jsonDocument["_rev"] = $stateParams.documentRevision;
}
$pouchDB.save(jsonDocument).then(function(response) {
$state.go("list");
}, function(error) {
console.log("ERROR -> " + error);
});
}
This function does two things. It will prepare an insert or it will prepare an update should a document id and document revision be available.
We're not quite done yet though. Although we finished all our functions, we still need to handle listening for changes. When we call $pouchDB.startListening(); in our controller, our PouchDB service will start making use of the AngularJS $broadcast. While it is broadcasting we can listen for those broadcasts using something like:
$rootScope.$on("$pouchDB:change", function(event, data) {
$scope.items[data.doc._id] = data.doc;
$scope.$apply();
});
$rootScope.$on("$pouchDB:delete", function(event, data) {
delete $scope.items[data.doc._id];
$scope.$apply();
});
Our view controller logic is now good to go!
Defining Your Ionic Framework Views
The last part of our www/js/app.js file will be for defining our views. This is done in the AngularJS config() function like so:
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state("list", {
"url": "/list",
"templateUrl": "templates/list.html",
"controller": "MainController"
})
.state("item", {
"url": "/item/:documentId/:documentRevision",
"templateUrl": "templates/item.html",
"controller": "MainController"
});
$urlRouterProvider.otherwise("list");
})
We defined two views, one for all our list items and one for creating and updating new list items. The item state takes an optional document id and document revision parameter. When they are present, it means we are going to be updating a particular document.
All our AngularJS logic is complete now. We have initialized our database, started syncing, defined our views, and planned for interaction from our views.
Creating A List View
Here we will define how data is presented in the list. In your project's www/templates/list.html file, add the following code:
<ion-view title="Couchbase with PouchDB">
<ion-nav-buttons side="right">
<button class="right button button-icon icon ion-plus" ui-sref="item"></button>
</ion-nav-buttons>
<ion-content>
<ion-list show-delete="false" can-swipe="true">
<ion-item ng-repeat="(key, value) in items" ui-sref="item({documentId: key, documentRevision: value._rev})">
{{value.firstname}} {{value.lastname}}
<ion-option-button class="button-assertive icon ion-trash-a" ng-click="delete(key, value._rev)"></ion-option-button>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
When swiping a list item we are presented with a delete button that will call the delete() function of our controller.
Creating A Form View
Here we will define out documents will be inserted or updated in our database. Essentially, this view is only a form. In your project's www/templates/item.html file, add the following code:
<ion-view title="Couchbase with PouchDB">
<ion-nav-buttons side="left">
<button class="left button button-icon icon ion-arrow-left-c" ng-click="back()"></button>
</ion-nav-buttons>
<ion-content>
<div class="list">
<label class="item item-input">
<input type="text" ng-model="inputForm.firstname" placeholder="First Name">
</label>
<label class="item item-input">
<input type="text" ng-model="inputForm.lastname" placeholder="Last Name">
</label>
<label class="item item-input">
<input type="text" ng-model="inputForm.email" placeholder="Email">
</label>
</div>
<div class="padding">
<button class="button button-block button-positive" ng-click="save(inputForm.firstname, inputForm.lastname, inputForm.email)">
Save
</button>
</div>
</ion-content>
</ion-view>
The Sync Gateway Configuration
PouchDB and Ionic Framework are only half the story here. Sure they will create a nice locally running application, but we want things to sync. The Couchbase Sync Gateway is our endpoint for this and of course PouchDB works great with it.
Inside your project's sync-gateway-config.json file, add the following:
{
"log":["CRUD+", "REST+", "Changes+", "Attach+"],
"databases": {
"test-database": {
"server":"walrus:data",
"sync":`
function (doc) {
channel (doc.channels);
}
`,
"users": {
"GUEST": {
"disabled": false,
"admin_channels": ["*"]
}
}
}
},
"CORS": {
"Origin": ["http://localhost:9000"],
"LoginOrigin": ["http://localhost:9000"],
"Headers": ["Content-Type"],
"MaxAge": 17280000
}
}
This is one of the most basic configurations around. A few things to note about it:
- It uses walrus:data for storage which is in memory and does not persist. Not to be used for production.
- All data is synced via the GUEST user, so there is no authentication happening here, but there could be.
- We are fixing CORS issues by allowing requests on localhost:9000 in case you wanted to serve with Python for browser testing.
Testing The Application
At this point all our code is in place and our Sync Gateway is ready to be run. Start up the Sync Gateway by running the following in a Command Prompt or Terminal:
/path/to/sync/gateway/bin/sync-gateway /path/to/project/sync-gateway-config.json
The Sync Gateway should now be running and you can validate this by visiting http://localhost:4984 in your web browser.
Testing For Android
With a device connected or a simulator running, from the Command Prompt or Terminal, run the following two commands to build and install the APK file:
ionic build android
adb install -r platforms/android/build/outputs/apk/android-debug.apk
Testing For iOS
There are two good ways to do this. You can either build the project and open it with Xcode, or you can build and emulate the application without launching Xcode. The first can be done like so:
ionic build ios
Then open the project's platform/ios/ directory and launch the Xcode project file.
If you've installed the Node Package Manager (NPM) package ios-sim, you can do the following:
ionic build ios
ionic emulate ios
Conclusion
You saw now that there are two ways you can use Couchbase in your Ionic Framework mobile Android and iOS application. You can use the Apache Cordova Couchbase plugin as demonstrated in the previous blog series, or you can use PouchDB. Both of these are very suitable options when it comes to cross platform data storage and sync in your application.
You can obtain the full working source code to this blog post via our Couchbase Labs GitHub repository.
Comments