Lesson 12

Getting Data From CouchDB into Ionic

Pulling data into Ionic, and pushing data back to CouchDB

PRO

Lesson Outline

Getting Data From CouchDB into Ionic

We've built a little bit of our application, and we've set up a little bit of our database, but up until this point, they have both existed entirely separately from each other. In this lesson, we are going to bring those two worlds together by doing the following:

  • Set up PouchDB and get it talking with our CouchDB database
  • Set up the Notices and Chats services so that we can retrieve and store data
  • Update the user interface to make use of the services

At the end of this lesson, we should be able to display real data in our application, and also create our own new notices and chats through the application.

Create the Design Documents

Before we start pulling data into our application, we are going to set up the views we need in our CouchDB database. As we discussed in the last lesson, we can create a view inside of a design document in CouchDB to create a list of specific data based on a map function.

Our views are going to be very simple. We want to be able to pull in a list of notices into our notices page, and we want to be able to pull in a list of chats into our chats page, so we will create two separate views to achieve this. One view will list all of the notices, and another view will list all of the chats.

We are going to create two separate design documents to do this, one for chats and one for notices. This is not necessary, we could just create a single 'app' design document that implemented both of the views, but creating separate design documents helps to keep things organised. We will also be adding other features to the design documents later, so it is going to be cleaner to keep things separated. But I do want to emphasise the fact that it doesn't matter if we have a "notices" design document and a "chats" design document, the views in these design documents will still run on all documents in the database (both chats and notices).

Add the following document to your CouchDB database (you can just create this as a normal document like any other, you don't need to create it anywhere specific):

{
   "_id": "_design/notices",
   "language": "javascript",
   "views": {
       "by_date_updated": {
           "map": "function(doc){ if(doc.type === 'notice'){emit(doc.dateUpdated);} }"
       }
   }
}

This is our notices design document, and we have created a by_date_updated view. We already discussed the purpose of this in the last lesson, but to quickly recap: this map function will create a view that contains all of the notices in the database, and it will use the dateUpdated field as the key. This will allow us to sort the notice documents by the date they were last updated.

NOTE: Be careful using quotation marks when adding functions to design documents. In the example above we use single quotes for 'notice' but if you use double quotes you need to make sure to escape it with a slash \" so that it doesn't break the string used as the map function.

Add the following document to your CouchDB database:

{
   "_id": "_design/chats",
   "language": "javascript",
   "views": {
       "by_date_created": {
           "map": "function(doc){ if(doc.type === 'chat'){emit(doc.dateCreated);} }"
       }
   }
}

This is the design document for chats and it is almost exactly the same as the notices design document, except that we use the dateCreated field as the key. It will not be possible to update chats once they are created, so we don't have a dateUpdated field.

Implement the Data Service

We've done everything that needs to be done on CouchDB's end to start pulling data into our application, so let's implement the Data provider now. As I mentioned earlier, we want the Data provider to handle most of the integration with PouchDB and CouchDB so that we can abstract the implementation details away from the rest of our application as much as possible. The data provider will be the middleman between our application and the database.

Let's create the data provider now.

Modify src/app/services/data.service.ts to reflect the following:

import { Injectable } from '@angular/core';
import PouchDB from 'pouchdb-browser';

@Injectable({
  providedIn: 'root',
})
export class DataService {
  public db: PouchDB.Database = null;
  private remote: string;

  constructor() {}

  initDatabase(remote: string): void {
    this.db = new PouchDB('hangz-app', {
      auto_compaction: true,
    });

    this.remote = remote;
    this.initRemoteSync();
  }

  initRemoteSync(): void {
    let options = {
      live: true,
      retry: true,
    };

    this.db.sync(this.remote, options);
  }

  createDoc(doc: any): Promise<any> {
    return this.db.post(doc);
  }

  updateDoc(doc: any): Promise<any> {
    return this.db.put(doc);
  }

  deleteDoc(doc: any): Promise<any> {
    return this.db.remove(doc);
  }
}

We make PouchDB's functionality available in this class by importing it:

import PouchDB from 'pouchdb-browser';

We use that to create a new local PouchDB database called hangz-app, with the auto_compaction option set to true (this basically helps keeps our database tidy over time by removing old revisions). If the database does not already exist locally it will create it, but if it does already exist then it will just use that existing database.

This is triggered within an initDatabase method that we will call at some point during the startup of the application, and we will pass in the URL for the remote CouchDB database into it. We then use that URL in the initRemoteSync method, which sets up the live sync between the local PouchDB database and the remote CouchDB database.

In the options for the sync, we set both live and retry to true. The live option will subscribe to changes so that when there are any changes they will automatically be replicated. The retry option will make PouchDB reattempt replication if a failure occurs. This is especially important for getting this to work in the case of a user being offline and then coming back online - the initial replication would fail whilst they are offline, but once they come online the connection to the remote database can be established.

We have also created three methods for creating, updating, and deleting documents. We will be able to pass in documents to these methods, and then the data provider will handle performing the operations.

There's a subtle difference between posting and putting a document, you may notice that for creating documents we use post but for updating documents we use put. You should use put in cases where you know the _id of a document, i.e. cases where you are updating an existing document, or when you are creating a new document but you want to specify the _id yourself. If you are creating a new document and not supplying an _id, you should use post.

We will need to trigger the initDatabase method at some point. Eventually we will trigger this as part of the login process, but for now, we are just going to trigger it in the applications root component.

Modify src/app/app.component.ts to reflect the following:

import { Component, NgZone } from '@angular/core';
import { SplashScreen } from '@capacitor/splash-screen';
import { StatusBar } from '@capacitor/status-bar';
import { DataService } from './services/data.service';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  constructor(private dataService: DataService, private zone: NgZone) {
    this.initializeApp();
  }

  initializeApp() {
    SplashScreen.hide().catch((err) => {
      console.warn(err);
    });

    StatusBar.hide().catch((err) => {
      console.warn(err);
    });

    this.zone.runOutsideAngular(() => {
      this.dataService.initDatabase('http://127.0.0.1:5984/hangz-app');
    });
  }
}

Inside of the initializeApp method, we make a call to the initDatabase method in the data provider, and we supply it with the URL to our remote CouchDB database. You might notice we are running this inside of a weird zone method:

this.zone.runOutsideAngular(() => {
	...
});

This isn't actually anything to do with the functionality of our application, this is purely to stop some of the tests for the application from breaking (as I have mentioned, I built this application using a Test Driven Development approach, and those tests are available for you to view and run if you wish, but we are not discussing them as part of this module). This method will cause any code inside of it to run outside of Angular's zone (i.e. the 'zone' that Angular watches in order to trigger change detection which updates the user interface). If we attempt to run this code inside of Angular's zone, the asynchronous nature of PouchDB causes the tests to hang indefinitely and eventually break.

In just a moment, we will actually use the opposite of this method to force certain parts of PouchDB to run inside of Angular's zone so that change detection is triggered and our interface updates appropriately (this one isn't just for tests, it actually is required for our application to function properly).

Implement the Notices Service

We have PouchDB set up now, and the data service gives us a way to interact with the database. Now we are going to implement our notices provider to create the functionality specific to notices.

We are going to make use of the TypeScript interfaces we have already created for a Notice and Chat with PouchDB, but our original implementation of the interface did not include all of the data that we have now added to our chats/notices in CouchDB. We are going to need to update these interfaces to reflect all of the data our documents will contain:

Update src/app/interfaces/notice.ts to reflect the following:

export interface Notice {
  _id: string;
  _rev: string;
  type: 'notice';
  title: string;
  message: string;
  author: string;
  dateCreated: string;
  dateUpdated: string;
}

Whilst we are here, let's also update the chat interface as well.

Update src/app/interfaces/chat.ts to reflect the following:

export interface Chat {
  _id: string;
  _rev: string;
  type: 'chat';
  message: string;
  author: string;
  dateCreated: string;
}

Modifying these interfaces is going to cause both our chat.page.ts page and our notices.page.ts page to complain about our dummy data being incorrect (because they don't contain all of the required fields now).

Remove the dummy data from src/app/notices/notices/page.ts and src/app/notices/notices/page.ts

Modify src/app/services/notices.service.ts to reflect the following:

PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).