Using PouchDB with Ionic - part 1

December 10, 2016, 5:08 pm Categories:

Categories

Prerequisites

** UPDATED FOR IONIC 3 **

This tutorial will form part of a series where we explore different data storage solutions that can be used with Ionic to help persist data across our apps.

PouchDB is just one of those solutions and it's this particular software tool that we'll use to kick this series off with.

So what is PouchDB and why should we consider using this as a data storage solution when iOS and Android already provide support for WebSQL and IndexedDB?

A little background

PouchDB is an open-source, JavaScript, NoSQL database solution that runs on the client-side using the browser's underlying data storage mechanism - typically IndexedDB or, if this is unavailable, defaulting to WebSQL instead.

This essentially makes PouchDB a sort of abstraction layer, providing developers with a single API through which the software then determines the appropriate database storage mechanism to use. As you can imagine this helps simplify the process of handling data within web/mobile apps as developers don't have to choose between writing for WebSQL or IndexedDB - PouchDB simply manages that in the background.

Like all software tools one of the first questions will be, can we use this for our clients/target audience?

In the majority of cases this shouldn't be too much of a problem as PouchDB is supported in the following browsers :

  • Firefox 29+ (Including Firefox OS and Firefox for Android)
  • Chrome 30+
  • Safari 5+
  • Internet Explorer 10+
  • Opera 21+
  • Android 4.0+
  • iOS 7.1+
  • Windows Phone 8+

The only time where this will clearly be an issue is where older browsers need to be supported (and, as ridiculous as it might sound, I still know of certain web based projects that need to support IE6 of all browsers! Scary, I know.)

Where this might be the case the PouchDB website provides help on how to implement legacy support.

As this tutorial deals with Ionic only then we're definitely in a good place here as iOS 7.1+, Android 4.0+ and Windows Phone 8+ are all supported - so, all things being equal, that should pretty much cover our target audience!

Great - so what does PouchDB look like?

As a NoSQL database solution the API comes in the form of JavaScript which should look somewhat familiar to Ionic and front-end developers as demonstrated in the following examples:

// Create a PouchDB instance
var _db = new PouchDB("name-of-database-here", {size: 50})



// Add a document to the database
var snippet = {
   _id: new Date().toISOString(),
   content: title
   };

_db.put(snippet, function callback(err, result) 
{
   if (!err) 
   {
      console.log('Whoo! We successfully posted an entry to the database!');
   }
   else
   {
      console.log('Dang! We messed up somewhere');
      console.dir(err);
   }
});



// Remove a document from the database
var snippet = { 
   _id: id, 
   _rev: rev 
};

_db.remove(snippet, function callback(err, result) 
{
   if (!err) 
   {
      console.log('Whoo! We successfully removed an entry from the database!');
   }
   else
   {
      console.log('Dang! We messed up somewhere');
      console.dir(err);
   }
});

Nothing particularly odd or out of the ordinary there right?

We're not going to dwell on the syntax for the API here as we'll be covering that while we're developing our app but if you're feeling a little impatient you can always visit the online docs for further information.

What we'll be developing

Our Ionic app will be used to store details of favourite characters from comics dating back to the 1980's (yeah, yeah, I'm showing my age I know!) and will provide the following functionality:

  • List all saved entries
  • Add an entry
  • Update an entry
  • Remove an entry

We'll add, and make use of the following Cordova/Ionic Native plugins:

  • cordova-plugin-camera
  • cordova-plugin-sqlite-2

As well as the following UI components:

  • ion-select
  • ion-item-group
  • ion-item-divider
  • ion-range
  • ion-input
  • ion-textarea

By the end of this tutorial, once the coding magic has been weaved, you should end up with an app that looks something like the following (your choice of comic characters may, no doubt, be different to mine):

Ionic app displaying comic entries stored using PouchDB API

So, with that in mind let's start coding!

Setting the foundations

This tutorial assumes that your system environment has the following software installed:

If not, make sure your system is up to date before proceeding with this tutorial.

In your console of choice (developing on Mac OS X I always use the Terminal app) create a new Ionic app using the blank template:

ionic start comics blank

Once the Ionic app has finished being created change into the comics directory and run the following commands consecutively:

npm install pouchdb --save
ionic cordova plugin add cordova-plugin-sqlite-2
ionic cordova plugin add cordova-plugin-camera
npm install --save @ionic-native/camera

Here the PouchDB database has been installed via npm followed by the cordova SQLite2 plugin (which is a native SQLite database API for Cordova/PhoneGap/Ionic, modeled upon the WebSQL specification) and the Cordova Camera plugin (which will be used to capture/select photographs for use in the app).

Developing from this it won't hurt to install the TypeScript definitions for PouchDB so the TypeScript compiler in Ionic knows how to work with the database API:

typings install --global --save dt~pouchdb dt~pouchdb-adapter-websql dt~pouchdb-browser dt~pouchdb-core dt~pouchdb-http dt~pouchdb-mapreduce dt~pouchdb-node dt~pouchdb-replication

Notice that we install these globally so we can use them in other projects.

Next we'll create some services for our app:

ionic g provider database
ionic g provider image

The Database service, as you probably guessed, will be used to manage the PouchDB API calls for adding, editing, deleting and retrieving data.

Whereas, surprise, surprise the Image service will handle all camera related operations used by the app such as taking a picture or selecting pre-existing images from the device photo library.

Next we create an additional page for the app which will be used for data entry and editing:

ionic g page add

Finally, to complete setting up the foundations for the app, we'll register our services and Ionic Native plugins with the root module for the Comics App in the comics/src/app/app.module.ts file - like so:

import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { Camera } from '@ionic-native/camera';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';


import { Image } from '../providers/image';
import { Database } from '../providers/database';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    BrowserModule,
    HttpModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    Camera,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    Database,
    Image
  ]
})
export class AppModule {}

IMPORTANT - For some bizarre and unknown reason Ionic automatically imports the Http module for EVERY provider generated through the CLI but DOESN'T import the HttpModule for the application root module (situated at -comics/src/app/app.module.ts).

This means that you WILL have to add the HttpModule import manually to this file or watch your Ionic builds fail.

It sucks I know but that's how it currently is.

Now that we've set up the structure for the app and configured the root module let's turn our attention to scripting the Database Service...

Database handling

Open the comics/src/providers/database.ts file and implement the following code:

import { Injectable } from '@angular/core';
import { AlertController } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import PouchDB from 'pouchdb';


@Injectable()
export class Database {

   private _DB     : any;
   private success : boolean = true;

   constructor(public http      : Http,
               public alertCtrl : AlertController) 
   {
      this.initialiseDB();
   }



   initialiseDB()
   {
      this._DB = new PouchDB('comics');
   }



   addComic(title, character, rating, note, image)
   {
      var timeStamp   = new Date().toISOString(),
          base64String  = image.substring(23),
          comic   = {
           _id           : timeStamp,
           title           : title,
           character       : character,
           rating        : rating,
           note            : note,
           _attachments    : {
              "character.jpg" : {
                 content_type : 'image/jpeg',
                 data       : base64String
              }
           }
        };

      return new Promise(resolve =>
      {
         this._DB.put(comic).catch((err) =>
         {
            this.success = false;
         });

         resolve(true);

      });
   }



   updateComic(id, title, character, rating, note, image, revision)
   {
      var base64String   = image.substring(23),
          comic    = {
             _id       : id,
             _rev        : revision,
             title       : title,
             character   : character,
             rating      : rating,
             note        : note,
             _attachments: {
                "character.jpg" : {
                   content_type : 'image/jpeg',
                   data       : base64String
                }
             }
          };

      return new Promise(resolve =>
      {
         this._DB.put(comic)
         .catch((err) =>
         {
            this.success = false;
         });

         if(this.success)
         {
            resolve(true);
         }
      });
   }



   retrieveComic(id)
   {
      return new Promise(resolve =>
      {
         this._DB.get(id, {attachments: true})
         .then((doc)=>
         {
            var item    = [],
    dataURIPrefix = 'data:image/jpeg;base64,',
    attachment;

            if(doc._attachments)
            {
               attachment   = doc._attachments["character.jpg"].data;
            }

            item.push(
            {
               id            :  id,
               rev           :  doc._rev,
               character     :  doc.character,
               title       :  doc.title,
               note          :  doc.note,
               rating      :  doc.rating,
               image         :  dataURIPrefix + attachment
            });
            resolve(item);
         })
      });
   }




   retrieveComics()
   {
      return new Promise(resolve => 
      {
         this._DB.allDocs({include_docs: true, descending: true, attachments: true}, function(err, doc) 
   {
      let k,
          items   = [],
          row   = doc.rows;

      for(k in row)
      {
         var item            = row[k].doc,
             dataURIPrefix   = 'data:image/jpeg;base64,',
             attachment;

         if(item._attachments)
         {
            attachment     = dataURIPrefix + item._attachments["character.jpg"].data;
         }
  
         items.push(
         {
            id      :   item._id,
            rev     :   item._rev,
            character : item.character,
            title     : item.title,
            note      : item.note,
            rating    : item.rating,
            image     :   attachment
         });
      }
            resolve(items);
         });
      });
   }



   removeComic(id, rev)
   {
      return new Promise(resolve => 
      {
         var comic   = { _id: id, _rev: rev };

         this._DB.remove(comic)
         .catch((err) =>
         {
            this.success = false;
         });

         if(this.success)
         {
            resolve(true);
         }
      });
   }



   errorHandler(err)
   {
      let headsUp = this.alertCtrl.create({
         title: 'Heads Up!',
         subTitle: err,
         buttons: ['Got It!']
      });

      headsUp.present();
   }


}

Breaking this down, stage by stage, here's how our Database service is constructed.

Initially we import the necessary Ionic, Angular and third-party packages required by the service. Following from this we define 2 private properties inside the top of the Database class; one of which will be used to reference the PouchDB object and then, in our constructor, we call the initialiseDB method to set up our PouchDB class:

import { Injectable } from '@angular/core';
import { AlertController } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import PouchDB from 'pouchdb';


@Injectable()
export class Database {

   private _DB     : any;
   private success : boolean = true;

   constructor(public http      : Http,
               public alertCtrl : AlertController) 
   {
      this.initialiseDB();
   }

The initialiseDB method is incredibly simply - we just create a basic PouchDB class called comics:

initialiseDB()
{
   this._DB = new PouchDB('comics');
}

We could pass additional configuration options into the new PouchDB constructor if we wanted to but, for this example, we're just keeping it simple.

Next up is the addComic method which, as the name implies, is used to add an entry for the app.

This accepts 5 parameters, each of which signifies the individual data item to be saved in the database. These items are then collected into a JSON object named comic.

IMPORTANT - There are a few things to note here:

  • Entries added to the database using PouchDB's put method require an _id key - this provides a unique value for each entry (similar to how a Primary Key would work in a SQL database)
  • The image is supplied as a Data URL NOT an object or base64 string so, in order to be able to save this within PouchDB, we HAVE to remove the first 23 characters (I.e. data:image/jpeg;base64,)
  • Once converted the supplied image is added through an _attachments wrapper as per PouchDB's API requirements

We then wrap the call to PouchDB's put method within a Promise so we can make the success/failure of the operation available to the script calling the addComic method:

addComic(title, character, rating, note, image)
{
   var timeStamp  = new Date().toISOString(),
       base64String = image.substring(23),
       comic      = {
          _id            : timeStamp,
        title        : title,
        character    : character,
        rating         : rating,
        note           : note,
        _attachments   : {
         "character.jpg" : {
          content_type : 'image/jpeg',
          data       : base64String
       }
    }
   };

   return new Promise(resolve =>
   {
      this._DB.put(comic).catch((err) =>
      {
         this.success = false;
      });

      resolve(true);
   });
}

The updateComic method is essentially a slightly modified version of the AddComic method so I won't waste time explaining that here.

Next up is the retrieveComic method which is used to return a single entry from the database based on the supplied id.

This calls PouchDB's get method (instructing the API call that we want to retrieve all attachments associated with the requested record through the {attachments: true } option) and, amongst other things, re-attaches the DataURL prefix for the image base64 string value so this can be subsequently displayed on the view template for app.

The matching record for the supplied ID is then returned within an array named item courtesy of a Promise:

retrieveComic(id)
{
   return new Promise(resolve =>
   {
      this._DB.get(id, {attachments: true})
      .then((doc)=>
      {
         var item              = [],
           dataURIPrefix       = 'data:image/jpeg;base64,',
           attachment;

         if(doc._attachments)
         {
            attachment          = doc._attachments["character.jpg"].data;
         }

         item.push(
         {
            id                  : id,
            rev                 : doc._rev,
            character           : doc.character,
            title             : doc.title,
            note              : doc.note,
            rating              : doc.rating,
            image               : dataURIPrefix + attachment
         });
         resolve(item);
      })
   });
}

Next up is the retrieveComics method which is used to retrieve all saved entries from the database.

This implements the allDocs method of the PouchDB API, using the following options to determine how the data is received:

  • include_docs (include the document itself in the doc field)
  • descending (return documents in order of most recent first)
  • attachments (return attachment data as base64-encoded string)

Once returned the entries are then iterated through and assigned as key/values pairs in objects stored within the items array.

This array, once fully populated, is then made available to the calling script through use of a Promise:

retrieveComics()
{
   return new Promise(resolve => 
   {
      this._DB.allDocs({include_docs: true, descending: true, attachments: true}, function(err, doc) 
      {
         let k,
           items  = [],
           row  = doc.rows;

         for(k in row)
         {
            var item           = row[k].doc,
            dataURIPrefix  = 'data:image/jpeg;base64,',
            attachment;

          if(item._attachments)
          {
             attachment      = dataURIPrefix + item._attachments["character.jpg"].data;
            }
  
          items.push(
          {
             id           :   item._id,
             rev          :   item._rev,
             character    : item.character,
             title      : item.title,
             note         : item.note,
             rating       : item.rating,
             image        :   attachment
          });
       }
         resolve(items);
       });
   });
}

No prizes for guessing what our penultimate method, removeComic is designed to accomplish.

The PouchDB remove method accepts an object consisting of the _id and _rev values for the record that we want to delete (the _id and _rev values, which exist for every record, are mandatory parameters required when deleting a record).

Once again this is wrapped within a Promise to make the result of the operation available to the script calling the removeComic method:

removeComic(id, rev)
{
   return new Promise(resolve => 
   {
      var comic   = { _id: id, _rev: rev };

      this._DB.remove(comic)
      .catch((err) =>
      {
         this.success = false;
      });

      if(this.success)
      {
         resolve(true);
      }
   });
}

Finally, the errorHandler method is used to manage informing the user of any problems that may occur during database related operations:

errorHandler(err)
{
   let headsUp = this.alertCtrl.create({
       title: 'Heads Up!',
       subTitle: err,
       buttons: ['Got It!']
   });

   headsUp.present();
}

Now that we've covered the logic for interacting with PouchDB let's finish part 1 of this tutorial by implementing the code for our Image service...

Handling Images

Within the comics/src/providers/image.ts file implement the following code:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { Camera, CameraOptions } from '@ionic-native/camera';


@Injectable()
export class Image {

   public cameraImage : String

   constructor(public http     : Http,
               private _CAMERA : Camera)
   {

   }



   takePhotograph()
   {
      return new Promise(resolve =>
      {
         this._CAMERA.getPicture(
       {
          destinationType : this._CAMERA.DestinationType.DATA_URL,
          targetWidth   : 320,
          targetHeight  : 240
       })
       .then((data) =>
       {
          this.cameraImage  = "data:image/jpeg;base64," + data;
          resolve(this.cameraImage);
       });
      });
   }



   selectPhotograph()
   {
      return new Promise(resolve =>
      {
         let cameraOptions : CameraOptions = {
             sourceType         : this._CAMERA.PictureSourceType.PHOTOLIBRARY,
             destinationType    : this._CAMERA.DestinationType.DATA_URL,
           quality            : 100,
           targetWidth        : 320,
           targetHeight       : 240,
           encodingType       : this._CAMERA.EncodingType.JPEG,
           correctOrientation : true
         };

         this._CAMERA.getPicture(cameraOptions)
         .then((data) =>
         {
            this.cameraImage  = "data:image/jpeg;base64," + data;
      resolve(this.cameraImage);
         });

      });
   }

}

This is a fairly simple service consisting of 2 methods: takePhotograph and selectPhotograph which are essentially variations on the same theme - capturing an image to be used with a record we are looking to add to the database.

The first method takePhotograph is used to open the device camera to capture an image as a Data URL returning that back to the calling script through a Promise.

The selectPhotograph method differs only slightly to the first method in that instead of using the device camera it opens the device photo library to select an image from. Once again the image is returned to the calling script through a Promise as a Data URL.

There's not really a whole lot more we need to say about the Image service so here would be a good point to conclude part 1 of this tutorial.

Summary

During this tutorial we've covered the basics of PouchDB, scaffolded the basic architecture for our Comics App and created 2 services with logic implemented for managing database interactions and working with the device camera/photo library.

In part 2 of this tutorial we'll cover the logic, templating and styling for the HomePage and AddPage components and conclude building the Comics App.

If you enjoyed this tutorial please consider signing up to my mailing list and, if you haven't done so already, take a look at my e-book: Mastering Ionic for information about working with alternative data storage methods in Ionic.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top