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

18 April 2016PouchDB, SQLite, Ionic 2+, Angular 2+, TypeScript, Local Storage

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: The code in this tutorial is now up to date with Ionic 2.0.0-rc.4 (December 2016)==

==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.==

###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. Since PouchDB version 6.0.0. this is no longer the case, I've updated this tutorial to include the extra steps needed to use SQLite on the mobile devices.

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
$ 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 and the PouchDB SQLite adapter.

$ npm install pouchdb pouchdb-adapter-cordova-sqlite --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 src/services/birthday.service.ts.

import { Injectable } from '@angular/core';
import * as PouchDB from 'pouchdb';
import cordovaSqlitePlugin from 'pouchdb-adapter-cordova-sqlite';

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

    initDB() {
        PouchDB.plugin(cordovaSqlitePlugin);
        this._db = new PouchDB('birthdays.db', { adapter: 'cordova-sqlite' });
    }
}

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 'cordova-sqlite', 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.

In other words, on the mobile device/emulator it will use SQLite, but in the browser, while you're developing it will use WebSQL.

There are more options you can specify for SQLite storage, have a look here.

####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.module.ts. I've also already added DetailsPage which we will build later on.

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 { DetailsPage } from '../pages/details/details';
import { BirthdayService } from '../services/birthday.service';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    DetailsPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    DetailsPage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, BirthdayService]
})
export class AppModule {}

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 ion-button (click)="showDetail()">
          <ion-icon name="add"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content>
    <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>

== Update: The following is not necessary anymore because Safari 10 supports the Internationalization API.==

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 app.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 { ModalController, NavController, Platform } from 'ionic-angular';
import { BirthdayService } from '../../services/birthday.service';
import { DetailsPage } from '../details/details';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
    public birthdays = [];

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

    }

    ionViewDidLoad() {
        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 = this.modalCtrl.create(DetailsPage, { birthday: birthday });
        modal.present();
    }
}

####DetailsPage

Add a new page with this command:

$ ionic g page details

Add the following code in details.html.

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

<ion-content padding>
    <ion-list>
        <ion-item>
            <ion-label>Name</ion-label>
            <ion-input text-right 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 ion-button block (click)="save()">Save</button>
</ion-content>

Add the following code in details.ts.

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

@Component({
  selector: 'page-details',
  templateUrl: 'details.html'
})
export class DetailsPage {
    public birthday: any = {};
    public isNew = true;
    public action = 'Add';
    public isoDate = '';

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

    ionViewDidLoad() {
        let editBirthday = this.navParams.get('birthday');

        if (editBirthday) {
            this.birthday = editBirthday;
            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 Check out the Common Errors section on the PouchDB website for troubleshooting tips.

==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

WRITTEN BY
profile
Ashteya Biharisingh

Full stack developer who likes to build mobile apps and read books.