CouchDB and Ionic

CouchDB, PouchDB, and Ionic 2: Adding Posts and Live Data Updates



·

Up until this point in the series, I have placed a heavy emphasis on the theory behind what we are doing, like how CouchDB works and why we would want to use PouchDB.

In this tutorial, we are going to focus on building out the blogging application we are creating with Ionic 2, CouchDB, and PouchDB. We will focus on adding the ability to actually add new blog posts to the application (rather than adding them manually through the database), view blog posts, and also have the data in the application update live.

At the end of this tutorial, we should have something that looks like this:

CouchDB PouchDB Blog Gif

Before We Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

You should also have already completed the previous CouchDB tutorial in this series.

1. Create Additional Pages and Providers

We’re adding a few new things to the application in this tutorial, including new pages so that we can add and view posts, and a new provider to help us out with PouchDB. For now, we are just going to generate them and get them set up in the application, we will focus on implementing them later.

Run the following commands to generate the required components:

ionic g page ViewPost
ionic g page AddPost
ionic g provider Data

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

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 { ViewPostPage } from '../pages/view-post/view-post';
import { AddPostPage } from '../pages/add-post/add-post';
import { Data } from '../providers/data';
import { Posts } from '../providers/posts';

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

2. Create a Generic Data Provider

When it comes to creating providers in your application, it’s generally a good idea to have each provider serve a single purpose. In our application, we will want to save and retrieve both posts and comments, so it’s probably a good idea to have a dedicated provider to handle stuff related to posts, and another provider to handle stuff dedicated to comments. When putting everything into one single giant provider things can get a little messy.

A good concept to keep in mind is that you want your providers to take a “black box” approach. I want to be able to make a call to the Posts provider and have it return me the posts, the component that makes the call doesn’t care how that happens behind the scenes.

Right now we have a single provider called Posts and that is what handles setting up our PouchDB database. When we implement the functionality for comments, we would create another provider called Comments which would also need access to PouchDB, so it doesn’t make much sense to handle the set up in Posts. Instead, we are going to implement another dedicated provider that handles setting up the PouchDB database for us and remove that code from the Posts provider.

Modify src/providers/data.ts to reflect the following:

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

@Injectable()
export class Data {

    db: any;
    remote: string = 'http://127.0.0.1:5984/couchblog';

    constructor() {

        this.db = new PouchDB('couchblog');

        let options = {
          live: true,
          retry: true,
          continuous: true
        };

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

    }

}

There’s nothing new going on here, we are just separating out the code that was once in the Posts provider into this provider instead.

3. Modify the Posts Provider

Now we are going to update the Posts provider to remove the database set up code, and we will modify it so that it uses the new Data provider instead. We are also going to add the ability to add new posts to the database, and we’re going to implement a better method for retrieving posts.

Modify src/providers/posts.ts to reflect the following:

import { Injectable, NgZone } from '@angular/core';
import { Data } from './data';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class Posts {

    postSubject: any = new Subject();   

    constructor(public dataService: Data, public zone: NgZone) {

        this.dataService.db.changes({live: true, since: 'now', include_docs: true}).on('change', (change) => {
            if(change.doc.type === 'post'){
                this.emitPosts();
            }
        });

    }

    getPosts(){

        this.emitPosts();

        return this.postSubject;

    }

    addPost(post): void {
        this.dataService.db.put(post);
    }

    emitPosts(): void {

        this.zone.run(() => {

            this.dataService.db.query('posts/by_date_published').then((data) => {

                let posts = data.rows.map(row => {
                    return row.value;
                });

                this.postSubject.next(posts);

            });

        });

    }

}

We are injecting the Data provider into this provider, and instead of referencing this.db to interact with the database, we instead reference the db member variable set up in the Data provider with this.dataService.db.

The functionality to add a post is quite straightforward, all we do is pass in an object to the addPost function and then it will call the put method on the database which will handle inserting the document into the database (we will go through creating this document in a moment).

The more interesting thing here is the changes to the way we retrieve posts. Instead of returning the posts directly, the getPosts function will return a Subject (which is basically a simplified observable) that can be updated with new posts at any time. If you are a little unfamiliar with observable and Subjects specifically, I would recommend watching An Introduction to Observables for Ionic 2.

The reason we use this observable approach is because we don’t want to just grab the post data once, we want to grab it and then get notified every time there are new posts. The emitPosts function will handle re-fetching the posts from our view every time there is a new post, and then triggering the next method on the Subject which will send the post data to whoever is subscribed to it (which we will be doing shortly). We trigger this function once manually, and then we trigger it every time there is a change that involves a post with this code:

        this.dataService.db.changes({live: true, since: 'now', include_docs: true}).on('change', (change) => {
            if(change.doc.type === 'post'){
                this.emitPosts();
            }
        });

The changes method allows us to listen for any changes to the database, and then we just check the type of the document to see if it is a post, if it is then we re-fetch the posts data.

I mentioned before that we want to use a “black box” approach for our providers, and this is exactly what this provider does. We have a function called getPosts that will return all of the posts. It is also an observable that updates every time there is a new post as well, so we only ever have to call this function once and we will get a constant stream of posts back.

You will likely notice that the emitPosts function is wrapped up inside of a zone, this is to force this code to run inside of Angular’s zone so that change detection is triggered (i.e. your user interface will update) when a change occurs. Change detection is a pretty complex topic, but if you would like to know a little more you can read Understanding Zones and Change Detection in Ionic 2 & Angular 2

4. Adding a Post

We’ve done most of the hard bit already, but now we need to add the ability for the user to navigate to a page to add a new post, and then we need to send that post through to our Posts provider.

Modify src/pages/add-post/add-post.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Posts } from '../../providers/posts';

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

    post: any = {
        _id: null,
        author: 'Josh Morony',
        content: '',
        datePublished: '',
        dateUpdated: '',
        title: '',
        type: 'post'
    };

    constructor(public navCtrl: NavController, public navParams: NavParams, public postService: Posts) {}

    ionViewDidLoad() {

    }

    save(){

        // Generate computed fields
        this.post._id = this.post.title.toLowerCase().replace(/ /g,'-').replace(/[^\w-]+/g,'');
        this.post.datePublished = new Date().toISOString();
        this.post.dateUpdated = new Date().toISOString();

        this.postService.addPost(this.post);

        this.navCtrl.pop();

    }

}

This is the class for the page where the user will create a new post. We create a post object that we will bind our inputs to. For now, the user will only be able to modify the content and title fields. You may notice that this object has the same structure as our post documents in the database do, and that is because this is exactly what we will be sending to the database to be added as a new document.

The user will be able to modify what the title and content are before it is sent off to the database through the addPost call in the save() function, but we also set some values of our own. We generate the slug to be used as the documents _id by converting the title to lowercase, replacing any spaces with hyphens, and removing any non-alphanumeric characters. We also set the values for the datePublished and dateUpdated fields.

Once we have done all this, we send the document off to be added, and then pop the view so that the user is taken back to the main page. It’s worth noting that we aren’t performing any error checking or any kind of data sanitisation here. It would be quite trivial for the user to modify the date values and more before sending this off to the database, so this isn’t quite at a “production” level quite yet.

Modify src/pages/add-post/add-post.html to reflect the following:

<ion-header>

  <ion-navbar>

  </ion-navbar>

</ion-header>

<ion-content>

    <ion-list>

      <ion-item>
        <ion-label floating>Title</ion-label>
        <ion-input [(ngModel)]="post.title" type="text"></ion-input>
      </ion-item>

      <ion-item>
        <ion-label floating>Content</ion-label>
        <ion-input [(ngModel)]="post.content" type="text"></ion-input>
      </ion-item>

    </ion-list>   

</ion-content>

<ion-footer>

    <button (click)="save()" color="light" ion-button full>Save</button>

</ion-footer>

This is the template for the same page, and all it does is set up a couple of inputs that are tied to the title and content properties of our post object using [(ngModel)]. We also have a save button that will trigger the save() function.

Now all we need is a way to get to this page.

Modify src/pages/home/home.html to reflect the following:

<ion-header>
  <ion-navbar>
    <ion-title>
      Couch Blog
    </ion-title>
    <ion-buttons end>
        <button ion-button icon-only (click)="pushAddPostPage()"><ion-icon name="add"></ion-icon></button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content>

    <ion-list>
        <ion-item *ngFor="let post of posts">
            {{post.title}}
        </ion-item>
    </ion-list>

</ion-content>

Modify src/page/home/home.ts to reflect the following:

import { Component } from '@angular/core';
import { Posts } from '../../providers/posts';
import { NavController } from 'ionic-angular';
import { AddPostPage } from '../add-post/add-post';

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

    posts: any;

    constructor(public navCtrl: NavController, public postsService: Posts) {

    }

    ionViewDidLoad(){

        this.postsService.getPosts().subscribe((posts) => {
            this.posts = posts;
        });

    }

    pushAddPostPage(){
        this.navCtrl.push(AddPostPage);
    }

}

All we have done is add a button in the navbar to launch the Add Post page, and the corresponding event binding. You should be able to add posts to the application now, and they should be instantly reflected in the list on the home page as soon as you add them.

5. Viewing a Post

The last thing we have left to implement is to add the page for viewing a specific post. All we will need to do is pass in a specific post from the Home Page, and then display its content on the View Post page.

Modify src/pages/view-post/view-post.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

@Component({
  selector: 'page-view-post',
  templateUrl: 'view-post.html'
})
export class ViewPostPage {

    post: any;

    constructor(public navCtrl: NavController, public navParams: NavParams) {}

    ionViewDidLoad() {

        this.post = this.navParams.get('post');

    }

}

Modify src/pages/view-post/view-post.html to reflect the following:

<ion-header>

  <ion-navbar>
    <ion-title>{{post?.title}}</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

    <h2>{{post?.title}}</h2>

    <p>{{post?.content}}</p>

</ion-content>

This will handle displaying the post, but of course, we also need to be able to navigate to this page from the home page.

Modify src/pages/home/home.html to reflect the following:

<ion-header>
  <ion-navbar>
    <ion-title>
      Couch Blog
    </ion-title>
    <ion-buttons end>
        <button ion-button icon-only (click)="pushAddPostPage()"><ion-icon name="add"></ion-icon></button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content>

    <ion-list>
        <ion-item (click)="viewPost(post)" *ngFor="let post of posts">
            {{post.title}}
        </ion-item>
    </ion-list>

</ion-content>

Modify src/pages/home/home.ts to reflect the following:

import { Component } from '@angular/core';
import { Posts } from '../../providers/posts';
import { NavController } from 'ionic-angular';
import { ViewPostPage } from '../view-post/view-post';
import { AddPostPage } from '../add-post/add-post';

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

    posts: any;

    constructor(public navCtrl: NavController, public postsService: Posts) {

    }

    ionViewDidLoad(){

        this.postsService.getPosts().subscribe((posts) => {
            this.posts = posts;
        });

    }

    viewPost(post){
        this.navCtrl.push(ViewPostPage, {
            post: post
        });
    }

    pushAddPostPage(){
        this.navCtrl.push(AddPostPage);
    }

}

Now whenever a user clicks one of the posts in the list, the viewPost function will be triggered and the post will be passed along to the View Post page.

Summary

We’ve made a lot of progress into actually building something in this tutorial, and we have a pretty nice structure set up. Not only do we have a stream of posts that updates automatically every time the user adds a new post, it will also update automatically any time the remote database is changed as well – so if someone else were to push some change to the database, it would be reflected live as well.

We will continue to build this application in future tutorials, applying more CouchDB and PouchDB concepts as we go.

What to watch next...