Since I started travelling around Australia I’ve tried to keep things pretty minimal. I got rid of all the clothes I didn’t need, shoes, kitchen ware, furniture, cleaning products and so on. Basically, I tried to get rid of everything I didn’t need… yet I still have 2 iPhones, an iPad, 3 Android phones, a Mac and a PC.
To be fair, I’m a mobile developer, so although I probably don’t need these they certainly make my life easier. Other people might not have that many devices, but I think it would be pretty rare to find someone who just has a single “smart” device. So if you’re building an app that only stores data locally on one device, you might be causing issues for a user who would like to use your app on their iPhone and their iPad.
The solution is to store the data remotely on a server somewhere, which will allow the user to access the data from any device. However, this introduces a new problem: not everybody is connected to the Internet all the time (I should know, I’m writing this from the middle of the Australian outback). So we’re going to tackle two issues:
- How to store data remotely, and;
- How to provide offline functionality with online syncing
In this tutorial, we will be creating a todo list application called ‘ClouDO’. Unlike a previous tutorial which only stored the todo data locally, this todo application will store the data in a remote database and locally. The local data will be synced to the remote database when an Internet connection is available, and any new data from the remote database will also be synced to the local database if there is new data available.
The result will be a todo application where the user can access their todos from any device, and they will even be able to view and edit the todos on their device even when no Internet connection is available. In the end, it’s going to look like this:
Syncing offline and online data sounds like quite the task (and it is), but two bits of technology are going to make this process pretty straightforward for us: CouchDB and PouchDB.
CouchDB is a document style NoSQL database that is built for the web. It is very similar to MongoDB which we used in Building a Review App with Ionic 2, MongoDB & Node. Perhaps the biggest advantage CouchDB has over MongoDB is its ability to easily replicate databases across devices (CouchDB can run just about anywhere), which is great for facilitating offline functionality with online sync. If this is not a requirement for your application, then MongoDB may be the better choice (which has better support for ad hoc queries).
PouchDB was inspired by CouchDB (hence the name), but it is designed for storing local data and then syncing to a CouchDB database when a connection is available. In this case we will be using CouchDB as the remote database, but you don’t have to use CouchDB specifically, you can use any database that supports the CouchDB protocol (of which there are many).
Before we Get Started
Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.
If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.
Introduction to CouchDB
We’re not going to go into too much detail here because we will cover most of what we need to know while building out the app. I do want to cover some key concepts, though.
CouchDB uses a REST API for interfacing with the database, which means we use HTTP methods like PUT, POST and DELETE to interact with the database.
All documents stored in a CouchDB database must have an _id
field, which you can specify manually or CouchDB can generate it automatically for you. After creating a document, CouchDB will also assign it a _rev
(revision) field, which changes each time the document is changed. In order to update a document you must supply both the _id
and the _rev
, if either of these are incorrect then the update will fail. This is the key concept to ensure data is not corrupted in a CouchDB database. If you were to supply the _rev
you retrieved, but the document had been updated by someone else after you retrieved that _rev
, the update would fail since there has already been an update that we didn’t know about.
Another key concept is the replication. A CouchDB database can easily be replicated to another database (which we will be making use of in this tutorial), and this replication can be:
- One way (PouchDB database is replicated to the CouchDB database)
- Bi-directional (PouchDB database is replicate to the CouchDB database, and vice versa)
- Ad hoc (replication is triggered manually)
- Continuous (database is continually replicated as necessary, changes are replicated instantly)
In this tutorial, we will be using PouchDB to interface with CouchDB, not CouchDB itself, but the concepts are the same. If you wanted to, you could also just interact with the CouchDB database directly using the REST API rather than using PouchDB (but then you would lose the awesome offline sync functionality).
We will be setting up a bi-directional and continuous replication, so as soon as we make a change to our local data it will be reflected in the remote database, and as soon as we make a change to the remote data it will be replicated in the local data (it’s quite cool to play around with).
Setting up CouchDB
The first thing we need to do is get our CouchDB database set up. We will be using a locally installed version of CouchDB for ease of development, so you won’t be able to access the data outside of the machine you are developing on. However, if you are following this tutorial for a real application you are building, you will just need to set up CouchDB on your server, rather than on your local machine.
To set up CouchDB you need to do is head to the CouchDB website, download it, and then it should be as simple as extracting the files and opening the CouchDB application.
Once you have it installed you should be able to able to navigate to:
http://127.0.0.1:5984/_utils/
or
http://localhost:5984/_utils/
to open up Futon, which is CouchDB’s built in administration interface. Which will look like this:
NOTE: If you are running this on a server and not a local development machine, make sure to fix the “Welcome to the admin party!” message.
What we need to do now is create a new database for our application. Click the Create Database
option in the top left to create a new database called cloudo
. Once you create it, you will automatically be taken inside of the database where you can create a new document. If you click ‘Add Document’ you will see something like this:
As I mentioned earlier, you can either manually create your own _id
field or accept the default from CouchDB. We will just use the default, so click the tick icon to the right to accept the _id
.
Once you have done that you can click the Add Field
button to add some fields to this document. Create a new field called title
and give it a value of hello
. Once you are done click Save Document
. Now you will see something like this:
Notice that the _rev
field has been automatically added now, and it is prefixed with 1-
to indicate that this is the first revision of the document. If you now change the hello
value to hello world
and then click Save Document
again, that _rev
field will change to 2-xxxx
.
If you would like, you can create some more documents in this database but that’s all we need to do for our application. We will be creating the ability to add documents through the application so there’s no need to manually modify anything in here.
Generating a new Ionic 2 Application
Now that we have our backend ready to use, let’s start building the front end. We will start by generating a new Ionic 2 application.
Generate a new Ionic 2 application with the following command:
ionic start cloudo blank --v2
Once it has finished generating, we will need to switch into it.
Run the following command to make the new project your working directory:
cd cloudo
We are going to be creating a provider to handle interfacing with the database, so let’s create that now.
Create a Todos provider with the following command:
ionic g provider Todos
and we are also going to need to install PouchDB.
Install PouchDB with the following command:
npm install pouchdb --save
The TypeScript compiler doesn’t know what PouchDB is, so it’s going to throw some errors at us if we try to use it. To get around this we need to install the types for PouchDB.
Run the following command to install the types for PouchDB:
npm install @types/pouchdb --save --save-exact
In order to be able to make use of the provider we created, we will need to add it to the app.module.ts file.
Modify src/app/app.module.ts to reflect the following:
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { Todos } from '../providers/todos';
@NgModule({
declarations: [MyApp, HomePage],
imports: [IonicModule.forRoot(MyApp)],
bootstrap: [IonicApp],
entryComponents: [MyApp, HomePage],
providers: [{ provide: ErrorHandler, useClass: IonicErrorHandler }, Todos],
})
export class AppModule {}
There’s one more thing we need to do to set up our application. By default, we are going to run into CORS (Cross Origin Resource Sharing) issues when trying to interact with CouchDB. You may get an error like this:
XMLHttpRequest cannot load http://localhost:5984/clouddo/?_nonce=1466856096255. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8100' is therefore not allowed access.
To fix this, you can simply install the add-cors-to-couchdb
package.
Run the following command:
npm install -g add-cors-to-couchdb
Then run the following command:
add-cors-to-couchdb
If it has worked, you should get a message saying “Success”. This will configure CouchDB correctly, and you only ever need to run this once (if you create another project using CouchDB you won’t need to do this again).
Creating the Front End
Now we’ve got all the theory and configuration out of the way, it’s time to jump into the fun stuff. We’ll start off with the most interesting thing which is the Todos provider.
Modify
todos.ts
to reflect the following:
import { Injectable } from '@angular/core';
import PouchDB from 'pouchdb';
@Injectable()
export class Todos {
data: any;
db: any;
remote: any;
constructor() {
this.db = new PouchDB('cloudo');
this.remote = 'http://localhost:5984/cloudo';
let options = {
live: true,
retry: true,
continuous: true,
};
this.db.sync(this.remote, options);
}
getTodos() {}
createTodo(todo) {}
updateTodo(todo) {}
deleteTodo(todo) {}
handleChange(change) {}
}
We’ve set up the basic structure for our provider above. The first thing to note is that we have imported PouchDB, which lets us create a new PouchDB database in the constructor, which we set up as db
. We will be able to use that database reference throughout this provider to interact with its various methods.
In the constructor we also define our remote database, which is just http://localhost:5984/
followed by the name of the database, which in this case is cloudo
. Then we call PouchDB’s sync
method, which will set up two way replication between our local PouchDB database, and the remote CouchDB database. If you only wanted one way replication you could instead use this.db.replicate.to('http://localhost:5984/cloudo')
.
Then we have defined a bunch of functions which will perform various tasks for our application, we are going to go through those one by one now.
Modify
getTodos()
to reflect the following:
getTodos() {
if (this.data) {
return Promise.resolve(this.data);
}
return new Promise(resolve => {
this.db.allDocs({
include_docs: true
}).then((result) => {
this.data = [];
let docs = result.rows.map((row) => {
this.data.push(row.doc);
});
resolve(this.data);
this.db.changes({live: true, since: 'now', include_docs: true}).on('change', (change) => {
this.handleChange(change);
});
}).catch((error) => {
console.log(error);
});
});
}
This function will return a promise containing the data from our database. If the data has already been fetched then it just returns it right away, otherwise it fetches the data from our database. We use the this.db.allDocs
method to return all of the documents in our database, and then we process the result by pushing all of the data into our this.data
array.
We also set up a db.changes
listener here, which will trigger every time there is a change to the data (i.e. if we manually edited the data in Futon). It will sent the change through to the handleChange
function, which we will define shortly.
Modify the
handleChange
function to reflect the following:
handleChange(change){
let changedDoc = null;
let changedIndex = null;
this.data.forEach((doc, index) => {
if(doc._id === change.id){
changedDoc = doc;
changedIndex = index;
}
});
//A document was deleted
if(change.deleted){
this.data.splice(changedIndex, 1);
}
else {
//A document was updated
if(changedDoc){
this.data[changedIndex] = change.doc;
}
//A document was added
else {
this.data.push(change.doc);
}
}
}
This function is provided information about the change that occurred. What we want to do with that change is reflect it in our this.data
array, but it get’s a little bit tricky. The change object that is sent back could either be a document that has been updated, a new document, or a deleted document.
Detecting a deleted document is easy enough because it will contain the deleted property. But to see if it is an update, we need to check if we already have a document with the same id, if we don’t then we know it is a new document.
Now if there is a change in the remote data, we are going to see it reflected immediately in our local this.data
array. Let’s finish off the rest of the functions now.
Modify the
createTodo
,updateTodo
, anddeleteTodo
functions to reflect the following:
createTodo(todo){
this.db.post(todo);
}
updateTodo(todo){
this.db.put(todo).catch((err) => {
console.log(err);
});
}
deleteTodo(todo){
this.db.remove(todo).catch((err) => {
console.log(err);
});
}
These functions are quite straight forward, we simply call PouchDB’s methods to create, delete or update a document. Remember how I said that you need to provide both the _id
and _rev
when updating a document? You can just supply these manually, or you can do what we have done here and just provide the whole document (which will contain both the _id
and the _rev
).
PouchDB does all the heavy lifting behind the scenes for us, but you could also interact with CouchDB directly by using the Http service and the POST, PUT, and DELETE methods.
Now all we need to do is create our interface, which is going to be a simple one page list. We will handle adding new todos and updating todos with Alerts.
Modify
home.ts
to reflect the following:
import { Component } from "@angular/core";
import { NavController, AlertController } from 'ionic-angular';
import { Todos } from '../../providers/todos';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
todos: any;
constructor(public navCtrl: NavController, public todoService: Todos, public alertCtrl: AlertController) {
}
ionViewDidLoad(){
this.todoService.getTodos().then((data) => {
this.todos = data;
});
}
createTodo(){
let prompt = this.alertCtrl.create({
title: 'Add',
message: 'What do you need to do?',
inputs: [
{
name: 'title'
}
],
buttons: [
{
text: 'Cancel'
},
{
text: 'Save',
handler: data => {
this.todoService.createTodo({title: data.title});
}
}
]
});
prompt.present();
}
updateTodo(todo){
let prompt = this.alertCtrl.create({
title: 'Edit',
message: 'Change your mind?',
inputs: [
{
name: 'title'
}
],
buttons: [
{
text: 'Cancel'
},
{
text: 'Save',
handler: data => {
this.todoService.updateTodo({
_id: todo._id,
_rev: todo._rev,
title: data.title
});
}
}
]
});
prompt.present();
}
deleteTodo(todo){
this.todoService.deleteTodo(todo);
}
}
In this class we are importing our Todos service and loading in the data from it. We create two methods for creating and updating todos through an Alert, which both call our Todos service, as well as a method for deleting todos. Notice that when we create the todo create a JSON object only containing the title, but when we update it we supply the _id
, _rev
, and title
. When deleting we just pass through the entire document from our template.
Now let’s get the template sorted
Modify
home.html
to reflect the following:
<ion-header no-border>
<ion-navbar color="secondary">
<ion-title> ClouDO </ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="createTodo()">
<ion-icon name="cloud-upload"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list no-lines>
<ion-item-sliding *ngFor="let todo of todos">
<ion-item> {{todo.title}} </ion-item>
<ion-item-options>
<button ion-button icon-only color="light" (click)="updateTodo(todo)">
<ion-icon name="create"></ion-icon>
</button>
<button ion-button icon-only color="primary" (click)="deleteTodo(todo)">
<ion-icon name="checkmark"></ion-icon>
</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</ion-content>
Pretty straightforward here, we just create a simple list to display the data, with sliding items to reveal both the Edit
and Delete
functions.
We’re pretty much done now but let’s add a bit of styling to pretty things up a bit.
Modify home.scss to reflect the following:
.ios,
.md {
page-home {
.scroll-content {
background-color: #ecf0f1;
display: flex !important;
justify-content: center;
}
ion-list {
width: 90%;
}
ion-item-sliding {
margin-top: 20px;
border-radius: 20px;
}
ion-item {
border: none !important;
font-weight: bold !important;
}
}
}
Modify the
$colors
in src/theme/variables.scss to reflect the following:
$colors: (
primary: #95a5a6,
secondary: #3498db,
danger: #f53d3d,
light: #f4f4f4,
dark: #222,
favorite: #69bb7b
);
You should now have something that looks like this:
Go ahead and add some items to your todo list, and what’s even cooler is that you can open up Futon again, change some data in the database, and watch the data update live in your app!
Summary
I think it’s pretty clear to see the benefit that replicating databases and offline syncing provides, and the PouchDB + CouchDB combo makes this really easy to pull off. In a later tutorial we will cover how to do some more advanced things with CouchDB (like using MapReduce).