How To Use PouchDB + SQLite For Local Storage In Ionic 2

A year ago I wrote a tutorial on how to use PouchDB + SQLite for an Ionic 1 app. Now that Ionic 2 is in beta, I've updated the tutorial for Ionic 2 and the recently released Cordova SQLite Plugin 2.

Update: In this tutorial I'm using Promises for the CRUD operations on PouchDB. I recommend you read my new tutorial using ngrx and Observables after you're finished with this one.

The source code can be found on GitHub.


What is PouchDB?

PouchDB is an open-source JavaScript library that uses IndexedDB or WebSQL to store data in the browser. It's inspired by Apache CouchDB and allows you to sync your local data with a CouchDB server.

What I like about PouchDB is that it uses a NoSQL approach to database storage, which greatly simplifies the code you need to write. And then there is the out-of-the-box syncing with a server, but in this tutorial we'll only focus on local storage.

There are storage limits for IndexedDB and WebSQL databases, so if you want unlimited and reliable storage on a mobile device, you're better off using SQLite. PouchDB will automatically use SQLite if you have installed a Cordova plugin for it and have configured it to use a WebSQL adapter.

Note: SQLite is slower than IndexedDB/WebSQL as mentioned in this article by Nolan Lawson.

Set up the libraries

Let's start by creating our Ionic 2 app.

$ ionic start ionic2-tutorial-pouchdb blank --v2 --ts
$ cd ionic2-tutorial-pouchdb

We'll have to install a couple of libraries into our app to get PouchDB working with SQLite.

To install SQLite Plugin 2 execute the following command in your Ionic app directory:

$ ionic plugin add cordova-plugin-sqlite-2

Next, we'll install PouchDB.

$ npm install pouchdb --save

Because this tutorial is written in TypeScript, it would be nice to have type definitions for the PouchDB library, but the ones on DefinitelyTyped are over 2 years old, so let's skip that.

If we want to import the PouchDB library without the help of type definitions, we need to use require(), so let's install the type definition for it from DefinitelyTyped:

$ typings install dt~require --global --save

Tip: If you want to know more about using external libraries in Ionic 2, check out this blog post by Mike Hartington.

We are done with setting up the necessary libraries, you now have everything you need to start writing code!

What are we going to build?

Our app is going to be a birthday registration app that will have add, update, delete and read functionality.

Screenshot Birthday App

Create database service

Let's go ahead and create a service to encapsulate our PouchDB calls in app/services/birthday.service.ts.

import {Injectable} from '@angular/core';

let PouchDB = require('pouchdb');

@Injectable()
export class BirthdayService {  
    private _db;
    private _birthdays;

    initDB() {
        this._db = new PouchDB('birthday2', { adapter: 'websql' });
    }
}

We need to initialize the database, if it doesn't exist, a new database will be created.

As you can see we're setting the adapter to WebSQL, the way PouchDB works is that if you have the SQLite plugin installed, it will automatically use that, otherwise it will fall back to WebSQL.

Add a birthday

Let's write the code for adding a birthday to our database.

add(birthday) {  
    return this._db.post(birthday);
}   

Is that all? Yes, that is all you have to do!

We don't need to write a SQL INSERT statement and map the data to a SQL table. In PouchDB the birthday object is simply serialized into JSON and stored in the database.

There are 2 ways to insert data, the post method and the put method. The difference is that if you add something with the post method, PouchDB will generate an _id for you, whereas if you use the put method you're generating the _id yourself.

I'm keeping it simple for this tutorial and using the post method, but you should really read this 12 pro tips for better code with PouchDB article.

Update a birthday

update(birthday) {  
    return this._db.put(birthday);
}

Delete a birthday

delete(birthday) {  
    return this._db.remove(birthday);
}

Get all birthdays

Let's get all the birthdays saved in the database.

getAll() {  

    if (!this._birthdays) {
        return this._db.allDocs({ include_docs: true})
            .then(docs => {

                // Each row has a .doc object and we just want to send an 
                // array of birthday objects back to the calling controller,
                // so let's map the array to contain just the .doc objects.

                this._birthdays = docs.rows.map(row => {
                    // Dates are not automatically converted from a string.
                    row.doc.Date = new Date(row.doc.Date);
                    return row.doc;
                });

                // Listen for changes on the database.
                this._db.changes({ live: true, since: 'now', include_docs: true})
                    .on('change', this.onDatabaseChange);

                return this._birthdays;
            });
    } else {
        // Return cached data as a promise
        return Promise.resolve(this._birthdays);
    }
}

We use the allDocs function to get an array back of all the birthday objects in the database. I don't want the code that will be calling this service to know anything about docs or PouchDB, so I've mapped the rows array to a new array that only contains the row.doc objects.

As you can see there is also a conversion of the row.doc.Date property to an actual Date, because unfortunately, the dates in JSON will not be automatically converted back to Date objects.

I also save the output in the _birthdays array so the data will be cached and I will only have to get the data from the database one time on start of the app.

"But...", you ask, "how will I keep that cached data in sync with the database when there is data added or changed?"

Well, I'm glad you asked, that's where the onDatabaseChange function comes in.

private onDatabaseChange = (change) => {  
    var index = this.findIndex(this._birthdays, change.id);
    var birthday = this._birthdays[index];

    if (change.deleted) {
        if (birthday) {
            this._birthdays.splice(index, 1); // delete
        }
    } else {
        change.doc.Date = new Date(change.doc.Date);
        if (birthday && birthday._id === change.id) {
            this._birthdays[index] = change.doc; // update
        } else {
            this._birthdays.splice(index, 0, change.doc) // insert
        }
    }
}

// Binary search, the array is by default sorted by _id.
private findIndex(array, id) {  
    var low = 0, high = array.length, mid;
    while (low < high) {
    mid = (low + high) >>> 1;
    array[mid]._id < id ? low = mid + 1 : high = mid
    }
    return low;
}

Inspired by this post: Efficiently managing UI state with PouchDB.

This function allows you to update the _birthdays array whenever there is a change in your database. The input for this method is a change object that contains an id and the actual data in a doc object. If this id is not found in the _birthdays array it means that it is a new birthday and we will add it to the array, otherwise, it's either an update or a delete and we make our changes to the array accordingly.

Let's build the UI

OK, so we have the service set up which does most of the heavy work, let's have a look at the UI.

We'll create 2 pages for our app, one to display the list of birthdays (HomePage) and one to add or edit a birthday (DetailsPage).

Before we do the implementation of the pages, we need to set up our service as a provider. We can do that per page but that means we will get a new instance of the service per page and we won't be able to use the cached data.

So, let's add the service at the parent level, which in this case is app.ts.

import {BirthdayService} from './services/birthday.service';

@Component({
  template: '<ion-nav [root]="rootPage"></ion-nav>',
  providers: [BirthdayService]
})

Now we'll get a shared instance of BirthdayService in our HomePage and DetailsPage.

HomePage

We'll implement the template home.html first, which uses an <ion-list> to display all the birthdays.

<ion-header>  
  <ion-navbar>
    <ion-title>
      🎂  Birthdays  🎉
    </ion-title>
    <ion-buttons end>
      <button (click)="showDetail()">
          <ion-icon name="add"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content class="home">  
      <ion-list inset>
          <ion-item *ngFor="let birthday of birthdays" (click)="showDetail(birthday)">
            <div item-left>{{ birthday.Name }}</div>
            <div item-right>{{ birthday.Date | date:'yMMMMd' }}</div>
          </ion-item>
       </ion-list>
</ion-content>  

Angular 2 uses the Internationalization API to do date formatting, which is pretty cool, but doesn't work in Safari. So you have 2 options, you can either use this polyfill or write your own date formatting pipes. For this tutorial, we'll use the polyfill, which means that you need to add this line to your index.html.

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>  

Now it's time to open up home.ts and write the code to get the data from the service.

We'll wait for the platform.ready() event to fire before we try to access the database. We then call birthdayService.getAll() and because it's an asynchronous call, we need to use zone.run() to let Angular know that it needs to do change detection and update the view.

As some commenters have pointed out below, we can also just use Observable.fromPromise, which will automatically update Angular. However, for this post I wanted to try out and see how it would work without Observables.

Tip: Watch the talk Angular 2 Change Detection Explained by Pascal Precht.

import {Component, NgZone} from "@angular/core";  
import {Modal, NavController, Platform} from 'ionic-angular';  
import {BirthdayService} from '../../services/birthday.service';  
import {DetailsPage} from '../details/details';  

@Component({
  templateUrl: 'build/pages/home/home.html'
})
export class HomePage {  
    public birthdays = [];

    constructor(private birthdayService: BirthdayService,
        private nav: NavController,
        private platform: Platform,
        private zone: NgZone) {

    }

    ionViewLoaded() {
        this.platform.ready().then(() => {
            this.birthdayService.initDB();

            this.birthdayService.getAll()
                .then(data => {
                    this.zone.run(() => {
                        this.birthdays = data;
                    });
                })
                .catch(console.error.bind(console));
        });
    }

    showDetail(birthday) {
        let modal = Modal.create(DetailsPage, { birthday: birthday });
        this.nav.present(modal);

        modal.onDismiss(() => {

        });
    }
}

DetailsPage

Add a new page with this command:

$ ionic g page details --ts

Add the following code in details.html.

<ion-header>  
  <ion-navbar>
    <ion-title>{{ action }} Birthday</ion-title>
    <ion-buttons end *ngIf="!isNew">
      <button (click)="delete()">
          <ion-icon name="trash"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content padding class="details">  
    <ion-list inset>
        <ion-item>
            <ion-label>Name</ion-label>
            <ion-input type="text" [(ngModel)]="birthday.Name"></ion-input>
        </ion-item>
        <ion-item>
            <ion-label>Birthday</ion-label>
            <ion-datetime displayFormat="MMMM D, YYYY" pickerFormat="MMMM D YYYY" [(ngModel)]="isoDate"></ion-datetime>
        </ion-item>
    </ion-list>
    <button block (click)="save()">Save</button>
</ion-content>  

Add the following code in details.ts.

import {Component} from '@angular/core';  
import {Modal, NavParams, ViewController} from 'ionic-angular';  
import {BirthdayService} from '../../services/birthday.service';

@Component({
  templateUrl: 'build/pages/details/details.html',
})
export class DetailsPage {  
    public birthday;
    public isNew = true;
    public action = 'Add';
    public isoDate = '';

    constructor(private viewCtrl: ViewController,
        private navParams: NavParams,
        private birthdayService: BirthdayService) {
    }

    ionViewLoaded() {
        this.birthday = this.navParams.get('birthday');

        if (!this.birthday) {
            this.birthday = {};
        }
        else {
            this.isNew = false;
            this.action = 'Edit';
            this.isoDate = this.birthday.Date.toISOString().slice(0, 10);
        }
    }

    save() {
        this.birthday.Date = new Date(this.isoDate);

        if (this.isNew) {
            this.birthdayService.add(this.birthday)
                .catch(console.error.bind(console));
        } else {
            this.birthdayService.update(this.birthday)
                .catch(console.error.bind(console));
        }

        this.dismiss();
    }

    delete() {
        this.birthdayService.delete(this.birthday)
            .catch(console.error.bind(console));

        this.dismiss();
    }

    dismiss() {
        this.viewCtrl.dismiss(this.birthday);
    }
}

We're Done!

You can now test the app in the browser (where it will use WebSQL).

$ ionic serve

And on your iOS and Android devices (where it will use SQLite).

$ ionic run ios
$ ionic run android

Inspecting the database

There is a Chrome extension called PouchDB Inspector that allows you to view the contents of the database in the Chrome Developer Tools.

The PouchDB Inspector only works for IndexedDB databases and you'll need to expose PouchDB as a property on the window object for it to work. Add this line of code to the BirthdayService implementation.

window["PouchDB"] = PouchDB;  

Screenshot PouchDB Inspector

You can not use the PouchDB Inspector if you loaded the app with ionic serve --lab because it uses iframes to display the iOS and the Androw views. The PouchDB Inspector needs to access PouchDB via window.PouchDB and it can't access that when the window is inside an <iframe>.

Troubleshooting

Keep in mind that if you haven't specified an adapter to use for PouchDB, it will use an IndexedDB or WebSQL adapter, depending on which browser you use. If you'd like to know which adapter is used by PouchDB, you can look it up:

var db = new PouchDB('birthdays2');  
console.log(db.adapter);  

On a mobile device the adapter will be displayed as websql even if it is using SQLite, so to confirm that it is actually using SQLite you'll have to do this (see answer on StackOverflow):

var db = new PouchDB('birthdays2');  
db.info().then(console.log.bind(console));  

This will output an object with a sqlite_plugin set to true or false.

Check out the Common Errors section on the PouchDB website for more troubleshooting tips.

Delete database

var db = new PouchDB('birthdays2');  
db.destroy().then(function() { console.log('ALL YOUR BASE ARE BELONG TO US') });  

The source code can be found on GitHub.


Update: In this tutorial I'm using Promises to interact with PouchDB. I recommend you read my new tutorial using ngrx and Observables after you're finished with this one.


I hope this tutorial was helpful to you, leave a comment if you have any questions. For more information on PouchDB and NoSQL check out the links below.

Read More

Red Pill or Blue Pill? Choosing Between SQL & NoSQL
Introduction to PouchDB
Efficiently managing UI state with PouchDB
12 pro tips for better code with PouchDB
PouchDB Blog
Syncing Data with PouchDB and Cloudant in Ionic 2

Follow me on Twitter @ashteya and sign up for my weekly emails to get new tutorials.

If you found this article useful, could you hit the share buttons so that others can benefit from it, too? Thanks!

comments powered by Disqus