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