Online and offline syncing of data in Ionic using PouchDB & CouchDB

December 22, 2016, 11:15 pm Categories:

Categories

A little context

** NOW UPDATED FOR IONIC 3 **

CouchDB is an open source NoSQL document oriented database which means that, unlike the traditional SQL model of tables and records, data is stored as JSON key/value pairs in the form of documents.

This particular approach allows for data to be structured in a far more flexible way and, more importantly, is able to be accessed through JavaScript, which is probably one of the most attractive features of working with this database for web/mobile app developers.

As a very quick overview CouchDB offers the following core features:

  • Implements data storage in the form of JSON key/value pairs
  • Provides a HTTP API to allow remote connection/management of the database
  • Comes with a built in administration application called Fauxton
  • Supports offline/online synchronisation between database and device(s)
  • Data scalability and replication
  • Strong integration with PouchDB

The last point is particularly important as managing data between a local PouchDB database abstraction layer and a remote CouchDB instance makes this a perfect solution for implementing local/remote data synchronisation with our Ionic apps.

It's this particular feature that we're going to be exploring throughout the remainder of this tutorial.

Getting Started

You should already have PouchDB installed (if you followed this earlier tutorial) so now it's simply a question of installing CouchDB too.

There are a number of ways we can accomplish this but I prefer to use Homebrew to handle the software installation for Mac OS X. If you're on a Mac system and aren't familiar with, or don't use Homebrew, then you'll need to start by installing that with the following command:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Once installed you can then do the same for CouchDB with the following command:

brew install couchdb

If you should receive this error while attempting to install CouchDB:

Error: You must `brew link autoconf automake` before couchdb can be installed

Simply run the following:

brew link autoconf automake

And then re-attempt the installation of CouchDB:

brew install couchdb

Once successfully installed the database service should start automatically. You can test whether or not this is the case by issuing the following command:

curl http://127.0.0.1:5984/

As CouchDB uses port 5984 by default you should be greeted with output akin to the following:

{"couchdb":"Welcome","uuid":"12be567d1c12e34ce987654123ab51e","version":"1.6.1","vendor":{"name":"Homebrew","version":"1.6.1_7"}}

If so great - CouchDB is up and running on your system!

Starting CouchDB for future sessions can be done in one of 2 ways:

1. Simply run the following from the command line every time you want to use the database service:

couchdb

2. Run CouchDB as a background service at login:

brew services start couchdb

CORS

As we're going to be accessing CouchDB remotely (from an Ionic app running on a mobile device) we have one final hurdle to clear before we can start coding: overcoming problems with CORS (Cross Origin Resource Sharing).

Cross Origin Resource Sharing allows websites and applications to access resources from different domains (such as when embedding Google Maps or Tweets in your apps for example) but not all software enables this by default for security reasons.

If we don't implement CORS support then PouchDB won't work unless it's served from exactly the same domain as CouchDB. This obviously wouldn't bode well for deploying such a solution online.

Thankfully this is easily overcome by installing the following script:

npm install -g add-cors-to-couchdb

Which we then need to run (one time only) from the command line in order to implement this:

add-cors-to-couchdb

Once completed you can kiss goodbye to CORS related issues with CouchDB!

Now we can move onto setting up our database.

The database

CouchDB comes with its own administration software called Fauxton which can be accessed through your web browser with the following URL: http://localhost:5984/_utils/fauxton/:

CouchDB administration software Fauxton home screen

The first task you should undertake is to create an admin user for the database.

Click onto the portrait icon at the bottom left-hand side of the screen to access the User Management console where you can then create a database administrator:

CouchDB add administrator screen

Performing this step will allow us, in a little while, to make some important configuration changes that will allow us to access the database remotely through our Ionic app.

Once you've created an administrator user log into the software, click onto the Databases link at the top left hand side of the screen and create a new database named comics:

Creating a new CouchDB database

If you click on the comics link in the databases list you'll see, as expected, that there are no documents stored in our newly created comics database:

Empty CouchDB database

We'll change this very shortly but first we need to make a configuration amendment so we can access our CouchDB database remotely/over a local network using an Ionic app installed on our mobile device.

Within the Fauxton administrative interface click onto the Config menu option from the left hand side of the screen.

On the configuration screen, under the httpd section edit the bind_address option so that it has a value of 0.0.0.0 as shown in the following screen capture:

Enabling remote access for CouchDB

Now let's return to the code for our previously built Ionic app and start implementing the logic to synchronise the local PouchDB database instance with our remote CouchDB comics database.

The Comics App

In part 1 and part 2 of the integrating PouchDB with Ionic tutorial we developed an Ionic app that allowed us to add, edit, remove and list details of comic characters from 80's British comics:

British Comic characters from the 80's

Let's begin by revisiting the code from that tutorial as we'll be building on top of this over the remainder of this tutorial.

comics/src/pages/home/home.ts

The home.ts script retrieves all stored comic character records and provides the functionality to determine whether a user is requesting to add or edit a comic character record:

import { Component } from '@angular/core';
import { AlertController, NavController, ToastController } from 'ionic-angular';
import { Database } from '../../providers/database';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

   public hasComics   : boolean = false;
   public comics      : any;

   constructor(	public navCtrl   : NavController,
               public alertCtrl  : AlertController,
               public toastCtrl  : ToastController, 
               public DB         : Database) 
   {
		
   }



   ionViewWillEnter()
   {
      this.displayComics();
   }



   displayComics()
   {
      this.DB.retrieveComics().then((data)=>
      {

         let existingData = Object.keys(data).length; 
         if(existingData !== 0)
         {
            this.hasComics 	= true;
            this.comics 	= data;
         }
         else
         {
            console.log("we get nada!");
         }

      });
   }



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

      headsUp.present();
   }



   addCharacter()
   {
      this.navCtrl.push('Add');
   }



   viewCharacter(param)
   {
      this.navCtrl.push('Add', param);
   }

}

comics/src/pages/home/home.html

The home.html page displays comic character records retrieved from the app database allowing them to be individually viewed/edited as well as providing the ability to create new records:

<ion-header>
   <ion-navbar>
      <ion-title>
         80's Comic Faves
      </ion-title>
   </ion-navbar>
</ion-header>

<ion-content padding>
  

   <ion-item>
      <button 
         class="add"
         ion-button 
         item-right
         icon-right
         margin-bottom
         color="primary"
         (click)="addCharacter()">
            Add a character
            <ion-icon name="add"></ion-icon>
      </button>
   </ion-item>



   <div *ngIf="hasComics">
      <ion-list>

         <ion-item *ngFor="let comic of comics">
            <ion-thumbnail item-left>
               <img [src]="comic.image">
            </ion-thumbnail>
            <h2>{{ comic.character }} </h2>
            <button 
               ion-button 
               clear 
               item-right
               (click)="viewCharacter({'key':comic.id, 'rev':comic.rev })">
                  View
            </button>
         </ion-item>

      </ion-list>
   </div>


</ion-content>

comics/src/pages/add/add.ts

The add.ts script provides the functionality for handling the creating/editing of comic character records:

import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, ToastController } from 'ionic-angular';
import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';
import { Image } from '../../providers/image';
import { Database } from '../../providers/database';


@IonicPage()
@Component({
  selector: 'page-add',
  templateUrl: 'add.html'
})
export class Add {

   public form             : FormGroup;
   public comicCharacter   : any;
   public comicTitle       : any;
   public comicRating      : any;
   public comicNote        : any;
   public comicImage       : any;
   public characterImage   : any;
   public recordId         : any;
   public revisionId       : any;
   public isEdited         : boolean = false;
   public hideForm         : boolean = false;
   public pageTitle        : string;
    

   constructor(public navCtrl    : NavController,
               public NP         : NavParams,
               public fb         : FormBuilder,
               public IMAGE      : Image,
               public DB         : Database,
               public toastCtrl  : ToastController) 
   {

      this.form = fb.group({
         "character"            : ["", Validators.required],
         "title"                : ["", Validators.required],
         "rating"               : ["", Validators.required],
         "image"                : ["", Validators.required],
         "note"                 : ["", Validators.required]
      });

      this.resetFields();


      if(NP.get("key") && NP.get("rev"))
      {
         this.recordId 			= NP.get("key");
         this.revisionId 		= NP.get("rev");
         this.isEdited 			= true;
         this.selectComic(this.recordId);
         this.pageTitle 		= 'Amend entry';
      }
      else
      {
         this.recordId 			= '';
         this.revisionId 		= '';
         this.isEdited 			= false;
         this.pageTitle 		= 'Create entry';
      }
   }



   selectComic(id)
   {
      this.DB.retrieveComic(id)
      .then((doc)=>
      {
         this.comicCharacter 		= doc[0].character;
         this.comicTitle			= doc[0].title;
         this.comicRating 			= doc[0].rating;
         this.comicNote 			= doc[0].note;
         this.comicImage 			= doc[0].image;
         this.characterImage 		= doc[0].image;
         this.recordId 				= doc[0].id;
         this.revisionId 			= doc[0].rev;
      });
   }

  


   saveComic()
   {
      let character	: string		= this.form.controls["character"].value,
          title 	: string 		= this.form.controls["title"].value,
          rating  	: number		= this.form.controls["rating"].value,
          image	  	: string		= this.form.controls["image"].value,
          note	  	: string		= this.form.controls["note"].value,
          revision	: string 		= this.revisionId,
  	       id 		: any 			= this.recordId;

  		
      if(this.recordId !== '')
      {
         this.DB.updateComic(id, title, character, rating, note, image, revision)
         .then((data) =>
         {
            this.hideForm 			= true;
            this.sendNotification(`${character} was updated in your comic characters list`);   
         });
      }
      else
      {
         this.DB.addComic(title, character, rating, note, image)
         .then((data) =>
         {
            this.hideForm 			= true;
            this.resetFields();
            this.sendNotification(`${character} was added to your comic characters list`); 
         });
      }
   }



   takePhotograph()
   {
      this.IMAGE.takePhotograph()
      .then((image)=>
      {
         this.characterImage 	= image.toString();
         this.comicImage 		= image.toString();
      })
      .catch((err)=>
      {
         console.log(err);
      });
   }



   selectImage()
   {
      this.IMAGE.selectPhotograph()
      .then((image)=>
      {
         this.characterImage 	= image.toString();
         this.comicImage 		= image.toString();
      })
      .catch((err)=>
      {
         console.log(err);
      });
   }




   deleteComic()
   {
      let character;

      this.DB.retrieveComic(this.recordId)
      .then((doc) =>
      {
         character            = doc[0].character;
         return this.DB.removeComic(this.recordId, this.revisionId);
      })
      .then((data) =>
      {
         this.hideForm 	= true;
         this.sendNotification(`${character} was successfully removed from your comic characters list`);  	 
      })
      .catch((err) =>
      {
         console.log(err);
      });
   }



   resetFields() : void
   {
      this.comicTitle 		= "";
      this.comicRating  	= "";
      this.comicCharacter 	= "";
      this.comicNote 		= "";
      this.comicImage		= ""; 
      this.characterImage	= "";  
   }




   sendNotification(message)  : void
   {
      let notification = this.toastCtrl.create({
         message 	: message,
         duration 	: 3000
      });
      notification.present();
   }



}

comics/src/pages/add/add.html

The add.html page provides a data entry form for creating/editing comic character records:

<ion-header>
   <ion-navbar>
      <ion-title>{{ pageTitle }}</ion-title>
   </ion-navbar>
</ion-header>


<ion-content padding>


   <div>
      <ion-item *ngIf="isEdited && !hideForm">
         <button 
            ion-button 
            item-right
            color="secondary" 
            text-center 
            block 
            (click)="deleteComic()">Remove this Entry?</button>
      </ion-item>



      <div *ngIf="hideForm">
         <ion-item class="post-entry-message" text-wrap>
            <h2>Success!</h2>
            <p>Maybe you'd like to edit an existing entry or add a new record?</p>
            <p>Simply go back to the home page and select the option you want to pursue.</p>
         </ion-item>
      </div>



      <div *ngIf="!hideForm">
         <form [formGroup]="form" (ngSubmit)="saveComic()">

            <ion-list>
               <ion-item-group>
                  <ion-item-divider color="light">Publication Name</ion-item-divider>
                  <ion-item>
                     <ion-label>Please select: </ion-label>
                     <ion-select 
                        class="select"
                        interface="action-sheet"
                        formControlName="title" 
                        block
                        [(ngModel)]="comicTitle">
                        <ion-option value="Battle/Action Force">Battle/Action Force</ion-option>
                        <ion-option value="Eagle">Eagle</ion-option>
                        <ion-option value="2000AD">2000AD</ion-option>
                        <ion-option value="Scream">Scream</ion-option>
                        <ion-option value="Other">Other</ion-option>
                     </ion-select>
                  </ion-item>
               </ion-item-group>



               <ion-item-group>
                  <ion-item-divider color="light">Character Name</ion-item-divider>
                  <ion-item>
                     <ion-input 
                        type="text" 
                        placeholder="Enter a name..." 
                        formControlName="character" 
                        [(ngModel)]="comicCharacter"></ion-input>
                  </ion-item>
               </ion-item-group>



               <ion-item-group>
                  <ion-item-divider color="light">Character Image</ion-item-divider>
                  <ion-item>
                     <a 
                        ion-button 
                        block
                        margin-bottom
                        color="primary"
                        (click)="takePhotograph()">
                           Take a photograph
                     </a>
                  </ion-item>


                  <ion-item>
                     <a 
                        ion-button 
                        block
                        margin-bottom
                        color="secondary"
                        (click)="selectImage()">
                           Select an existing image
                     </a>
                  </ion-item>


                  <ion-item>
                     <img [src]="characterImage">
                     <input 
                        type="hidden" 
                        name="image" 
                        formControlName="image" 
                        [(ngModel)]="comicImage">
                  </ion-item>
               </ion-item-group>



               <ion-item-group>
                  <ion-item-divider color="light">Character Rating</ion-item-divider>
                  <ion-item>
                     <ion-label text-left>Rating for this character?</ion-label>
                     <ion-range 
                        class="textarea"
                        formControlName="rating" 
                        min="1" 
                        max="5" 
                        step="1" 
                        snaps="true" 
                        secondary 
                        [(ngModel)]="comicRating">
                        <ion-label range-left>1</ion-label>
                        <ion-label range-right>5</ion-label>
                     </ion-range>
                  </ion-item>
               </ion-item-group>


               <ion-item-group>
                  <ion-item-divider color="light">Character Description</ion-item-divider>
                  <ion-item>
                     <ion-textarea 
                        placeholder="Additional notes..." 
                        formControlName="note" 
                        rows="6"
                        [(ngModel)]="comicNote"></ion-textarea>
                  </ion-item>
               </ion-item-group>


               <ion-item>
                  <button 
                     ion-button 
                     color="primary" 
                     text-center 
                     block 
                     [disabled]="!form.valid">Save Entry</button>
               </ion-item>

            </ion-list>


         </form>
      </div>


   </div>


</ion-content>

comics/src/providers/database.ts

The Database provider handles all of the data storage operations for the Comics app using PouchDB:

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 {

   public _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) =>
         {
            console.log('error is: ' + 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) =>
         {
            console.log('error is: ' + 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;
            }
            else
            {
               console.log("we do NOT have attachments");
            }

			    
            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 		= item._attachments["character.jpg"].data;
               }
               else
               {
                  console.log("we do NOT have attachments");
               }

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

            resolve(items);
         });
      });
   }



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

         this._DB.remove(comic)
         .catch((err) =>
         {
            console.log('error is: ' + 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();
   }


}

comics/src/providers/image.ts

The Image provider handles working with the Ionic Native Camera plugin for taking photographs and selecting images from the photo library of our mobile device:

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) 
   {
      console.log('Hello Camera Provider');
   }



   takePhotograph()
   {
      return new Promise(resolve => 
      {
         this._CAMERA.getPicture(
         {
            destinationType 	 : this._CAMERA.DestinationType.DATA_URL,
            targetWidth 	     : 320,
            targetHeight	     : 240
         })
         .then((data) => 
         {
            // imageData is a base64 encoded string
            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);
         }); 

      }); 		
   }


}

comics/src/app/app.module.ts

Finally we have the root module for the comics app which contains the necessary configurations for bootstrapping the required components and providers:

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

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


@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 {}

Building the next steps

Now that we've revisited the code from the previous Pouch DB tutorial let's implement some additional logic to allow our Comics app to synchronise data between our local PouchDB database and our remote CouchDB database.

Thankfully this will require only a minimal set of additions to the comics/src/providers/database.ts provider.

We begin by adding two private properties towards the top of the class:

  • _remoteDB (the URL to the remote CouchDB database)
  • _syncOpts (the synchronisation options for managing data between PouchDB and CouchDB)
@Injectable()
export class Database {

   public _DB 		    : any;
   private success 	    : boolean = true;
   private _remoteDB 	: any;
   private _syncOpts 	: any;

We then modify the initialiseDB method, which is called within the class constructor, to make use of the sync method from the PouchDB API, which allows the Comics app to replicate data, in both directions, between the local PouchDB database and the remote CouchDB database.

In our _syncOpts object we declare the following options:

  • live (track future changes and replicate them automatically)
  • retry (attempt to retry replication in the case of failure - due to being offline)
  • continuous (make replication continuous)

We subsequently handle the different replication states with change event handlers so that we can manage when the synchronisation process is paused, denied, completed etc.

Currently these only use console logs to signify the nature of that state but we could, if we wanted to extend this further, implement something more useful such as a Toast notification or a progress manager:

initialiseDB()
{
   this._DB 			= new PouchDB('comics');
   this._remoteDB 		= 'http://192.168.1.70:5984/comics';
   this._syncOpts 		= { live 	    : true, 
                            retry 	    : true, 
                            continuous 	: true };
   this._DB.sync(this._remoteDB, this._syncOpts)
   .on('change', (info) =>
   {
      console.log('Handling syncing change');
      console.dir(info);
   })
   .on('paused', (info) =>
   {
      console.log('Handling syncing pause');
      console.dir(info); 
   })
   .on('active', (info) =>
   {
      console.log('Handling syncing resumption');
      console.dir(info);
   })
   .on('denied', (err) => 
   {
      console.log('Handling syncing denied');
      console.dir(err);
   })
   .on('complete', (info) =>
   {
      console.log('Handling syncing complete');
      console.dir(info);
   })
   .on('error', (err)=>
   {
      console.log('Handling syncing error');
      console.dir(err);
   });
}

You'll notice that our remote URL consists of the IP address for our computer, followed by the port for accessing the CouchDB software (by default this is 5984) and then the name of the database that we want to read/write data to and from (which in this instance is the comics database).

To determine the IP address of your computer simply open up a command line window and type out the following commands:

// If running on Windows
ipconfig


// If running on Mac OS X
ifconfig

Our next additional method, handleSyncing, manages what happens every time a document changes within the database (including any attachments).

Once again we simply console log the nature of each change which could, of course, be changed to something more useful such as a Toast notification for example:

handleSyncing()
{	
   this._DB.changes({
      since 		: 'now',
      live 		    : true,
      include_docs 	: true,
      attachments 	: true
   })
   .on('change', (change) =>
   {
      // handle change
      console.log('Handling change');
      console.dir(change);
   })
   .on('complete', (info) =>
   {
      // changes() was canceled
      console.log('Changes complete');
      console.dir(info);
   })
   .on('error',  (err) =>
   {
      console.log('Changes error');
      console.log(err);
   });
}

The handleSyncing method is subsequently called within the addComic and updateComic methods as shown below:

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;
      });

	  
      if(this.success)
      {	
         this.handleSyncing();	
         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) =>
      {
         console.log('error is: ' + err);
         this.success = false;
      });

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

Our completed database provider

With those amendments implemented the comics/src/providers/database.ts script should now appear as follows:

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 {

   public _DB          : any;
   private success     : boolean = true;
   private _remoteDB   : any;
   private _syncOpts   : any;


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



   initialiseDB()
   {
      this._DB 			     = new PouchDB('comics');
      this._remoteDB 		 = 'http://192.168.1.70:5984/comics';
      this._syncOpts 		 = { live 	         : true, 
                                 retry 	         : true, 
                                 continuous 	 : true };
      this._DB.sync(this._remoteDB, this._syncOpts)
      .on('change', (info) =>
      {
         console.log('Handling syncing change');
         console.dir(info);
      })
      .on('paused', (info) =>
      {
         console.log('Handling syncing pause');
         console.dir(info); 
      })
      .on('active', (info) =>
      {
         console.log('Handling syncing resumption');
         console.dir(info);
      })
      .on('denied', (err) => 
      {
         console.log('Handling syncing denied');
         console.dir(err);
      })
      .on('complete', (info) =>
      {
         console.log('Handling syncing complete');
         console.dir(info);
      })
      .on('error', (err)=>
      {
         console.log('Handling syncing error');
         console.dir(err);
      });
   }



   handleSyncing()
   {	
      this._DB.changes({
         since 		     : 'now',
         live 		     : true,
         include_docs 	 : true,
         attachments 	 : true
      })
      .on('change', (change) =>
      {
         // handle change
         console.log('Handling change');
         console.dir(change);
      })
      .on('complete', (info) =>
      {
         // changes() was canceled
         console.log('Changes complete');
         console.dir(info);
      })
      .on('error',  (err) =>
      {
         console.log('Changes error');
         console.log(err);
      });
   }



   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) =>
         {
            console.log('error is: ' + err);
            this.success = false;
         });

	  
         if(this.success)
         {	
            this.handleSyncing();	
            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) =>
         {
            console.log('error is: ' + err);
            this.success = false;
         });

         if(this.success)
         {
            this.handleSyncing();
            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;
            }
            else
            {
               console.log("we do NOT have attachments");
            }

			    
            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 		= item._attachments["character.jpg"].data;
               }
               else
               {
                  console.log("we do NOT have attachments");
               }

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

            resolve(items);
         });
      });
   }



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

         this._DB.remove(comic)
         .catch((err) =>
         {
            console.log('error is: ' + 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();
   }


}

Synchronising databases

If we rebuild our app and then publish that to a connected mobile device we should see the CouchDB comics database being populated with data like so:

With individual records able to be updated from CouchDB itself:

Individual record view in CouchDB Fauxton administration console

As well as from within our Ionic app which means that, thanks to the bi-directional replication of data, the local PouchDB and the remote CouchDB databases are constantly in sync with one another.

Make an amendment or addition in either one and the changes will be mirrored in both.

In summary

As you can see synchronising local and remote data for our Ionic applications is both quick and easy to implement with the combination of PouchDB and CouchDB.

The simple API of both databases allows for rapid integration of these into our applications with a very small learning curve attached to doing so (always a blessing when we have project deadlines and what feels like a million and one tasks to get out of the way!)

For a local to remote data management solution that is able to work offline/online the combination of PouchDB and CouchDB is, for freely available software, surprisingly effective.

As always feel free to use the above code in your own Ionic projects as you see fit and, if you're feeling charitable, consider leaving a comment on this tutorial using the form below.

If you enjoyed what you've read here then please sign 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