Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Jeremy Mount
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Jeremy Mount

Experimenting With Simple CRUD Operations Using PouchDB In Angular 2.1.1

By on

As we enter December, I've been trying hard to materialize my desire to build an Angular 2 application by the end of 2016. The app that I want to build is an "Offline First" app. And, after doing a bunch of research, it looks like PouchDB is going to be my storage mechanism. The great thing about PouchDB is that is uses CouchDB's replication protocol which means that in the future, I'll be able to [easily] sync locally-stored data on my mobile device with CouchDB-powered remote replicas. That said, I've never actually used CouchDB or PouchDB before; so, I thought I should try to build a simple CRDU (Create, Read, Update, Delete) demo using Angular 2.1.1.

Run this demo in my JavaScript Demos project on GitHub.

PouchDB is a JavaScript implementation of the NoSQL database, CouchDB. This means that it provides the CouchDB API in your JavaScript application. PouchDB can write to a remote database or to a locally-persisted database. In the case of local persistence, PouchDB will use whatever persistence mechanism is available, whether it be IndexedDB or WebSQL (or another storage mechanism made available through PouchDB's plugin architecture).

At first, I just assumed that PouchDB was going to be like MongoDB, which is the other NoSQL database that I actually have some experience with. So, as I was reading through PouchDB and CouchDB tutorials, I kept trying to map the concepts onto my MongoDB mental model. Unfortunately, the two NoSQL databases have very little in common. And, doofus that I am, it took me more than two days before I realized that PouchDB didn't even have "collections."

In PouchDB / CouchDB, all documents are stored in a single key-space. So, if you want to have "collections", as you likely will in every application, you need to implement them on top of the existing key model. From what I have read, there are two ways to accomplish this:

  1. Have a "type" property in each document so that you can query for documents with a given type value.
  2. Use and abuse the doc key so that the key "prefix" indicates the type of document.

For this PouchDB experiment, I went with the second option. I created a CRUD app that allows me to update a list of friends. And, each friend in the list is stored with a key that starts with the prefix, "friend:". This way, when I want to retrieve all of the Friends from the database, I can get all of the keys that start with, "friend:". It's kind of like using an Index Prefix in a SQL database. Kind of.

Another huge difference between PouchDB / CouchDB and MongoDB is that PouchDB forces you to manage "revisions". PouchDB doesn't actually overwrite data. Instead, it stores revisions to documents (often analogized to Git). Apparently this does wonders for replication and conflict management. And, from a consumption standpoint, all it means is that we have to provide both the "key" and the "revision" of the document we want to update (or delete).

I'm extremely new to PouchDB and CouchDB, so I don't want to dive too deeply into the pros and cons of various PouchDB patterns. I'm still learning all of this myself and cannot speak authoritatively on the subject. This post is really just a chance for me to get my feet wet with PouchDB and look at how it might integrate with an Angular 2 application.

So, to start, let's look at our root Angular 2 component before we actually look at the service that consumes PouchDB. What I want to demonstrate in this file is that there's really nothing in the way we interact with the service that marries us to PouchDB. What I hope is that the following code looks just like any other component that might interact with a service that abstracts data persistence.

In this case, PouchDB provides a Promise-based API, which I have exposed through my FriendService. To be honest, it was rather nice to get back to Promises after having done a lot of work with RxJS - but, that's a post for another day.

// Import the core angular services.
import { Component } from "@angular/core";
import { OnInit } from "@angular/core";

// Import the application components and services.
import { FriendService } from "./friend.service";
import { IFriend } from "./friend.service";

interface IEditForm {
	id: string;
	name: string;
};

interface IAddForm {
	name: string;
}

@Component({
	selector: "my-app",
	template:
	`
		<ul>
			<li
				*ngFor="let friend of friends"
				[class.selected]="( editForm.id === friend.id )">

				{{ friend.name }} &mdash;

				<a (click)="editFriend( friend )">Edit</a> or
				<a (click)="deleteFriend( friend )">Delete</a>

				<div *ngIf="( editForm.id === friend.id )" class="form">

					<input
						type="text"
						[value]="editForm.name"
						(input)="editForm.name = $event.target.value"
						(keydown.Enter)="processEditForm()"
					/>
					<button type="button" (click)="processEditForm()">Update Friend</button>

				</div>
			</li>
		</ul>

		<div class="form">

			<input
				type="text"
				[value]="addForm.name"
				(input)="addForm.name = $event.target.value"
				(keydown.Enter)="processAddForm()"
			/>
			<button type="button" (click)="processAddForm()">Add Friend</button>

		</div>
	`
})
export class AppComponent implements OnInit {

	public addForm: IAddForm;
	public editForm: IEditForm;
	public friends: IFriend[];

	private friendService: FriendService;


	// I initialize the component.
	constructor( friendService: FriendService ) {

		this.friendService = friendService;

		this.addForm = {
			name: ""
		};
		this.editForm = {
			id: null,
			name: ""
		};
		this.friends = [];

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I delete the given friend from the list.
	public deleteFriend( friend: IFriend ) : void {

		this.friendService
			.deleteFriend( friend.id )
			.then(
				() : void => {

					this.loadFriends();

				},
				( error: Error ) : void => {

					console.log( "Error:", error );

				}
			)
		;

	}


	// I toggle the edit form for the given friend.
	public editFriend( friend: IFriend ) : void {

		// If the method is being called for the already-selected friend, then let's
		// toggle the form closed.
		if ( this.editForm.id === friend.id ) {

			this.editForm.id = null;
			this.editForm.name = "";

		} else {

			this.editForm.id = friend.id;
			this.editForm.name = friend.name;

		}

	}


	// I get called once after the component has been initialized and the inputs have
	// been bound for the first time.
	public ngOnInit() : void {

		this.loadFriends();

	}


	// I process the "add" form, creating a new friend with the given name.
	public processAddForm() : void {

		if ( ! this.addForm.name ) {

			return;

		}

		this.friendService
			.addFriend( this.addForm.name )
			.then(
				( id: string ) : void => {

					console.log( "New friend added:", id );

					this.loadFriends();
					this.addForm.name = "";

				},
				( error: Error ) : void => {

					console.log( "Error:", error );

				}
			)
		;

	}


	// I process the "edit" form, updating the selected friend with the given name.
	public processEditForm() : void {

		// If the name has been removed, then treat this as a "cancel".
		if ( ! this.editForm.name ) {

			this.editForm.id = null;
			this.editForm.name = "";
			return;

		}

		this.friendService
			.updateFriend( this.editForm.id, this.editForm.name )
			.then(
				() : void => {

					this.editForm.id = null;
					this.editForm.name = "";
					this.loadFriends();

				},
				( error: Error ) : void => {

					console.log( "Error:", error );

				}
			)
		;

	}


	// ---
	// PRIVATE METHODS.
	// ---


	// I load the persisted friends collection into the list.
	private loadFriends() : void {

		this.friendService
			.getFriends()
			.then(
				( friends: IFriend[] ) : void => {

					// NOTE: Since the persistence layer is not returning the data
					// in any particular order, we're going to explicitly sort the
					// collection by name.
					this.friends = this.friendService.sortFriendsCollection( friends );

				},
				( error: Error ) : void => {

					console.log( "Error", error );

				}
			)
		;

	}

}

As you can see, there's nothing particularly interesting about this code. It provides some basic CRUD (Create, Read, Update, Delete) operations which allow me to load the list of friends, add a new friend, change a friend's name, and delete an existing friend. Each CRUD operation is powered by the FriendService injectable and returns a promise. Since I know that this data is coming from local persistence, I'm not bother with any "optimistic updates" and, instead, I'm just reloading the data after every operation.

The FriendService is where we actually consume the PouchDB database. In this demo, I'm creating the PouchDB instance in the FriendService; but, in a non-trivial application, you'd probably want to share your PouchDB instance across many services. As such, in a more robust demo, I'd actually provide the PouchDB instance as an injectable which I would then inject into my FriendService. But, since this is my first experiment with PouchDB, I'm erring on the side of simplicity.

This Angular 2 demo is written in TypeScript. But, unfortunately, there are no up-to-date type definition files for PouchDB. As such, I added a few minor TypeScript interfaces here to try and outline - and more importantly learn about - the way data crosses the PouchDB API.

// The PouchDB library is delivered as a CommonJS module and I am not yet sure how to
// configure my System.js setup to allow for a more simple import statement. This is the
// only thing that I can get to work at this time.
// --
// CAUTION: TypeScript still complains, "Cannot find module 'pouchdb'."
import * as PouchDB from "pouchdb";


export interface IFriend {
	id: string;
	name: string;
}

// CAUTION: There is currently no up-to-date "Definitely Typed" set of interfaces for
// PouchDB. So, in an effort to help me learn about the PouchDB API, I'm providing a few
// tiny interfaces here so I can get a better idea of what data is available.

interface IPouchDBAllDocsResult {
	offset: number;
	total_rows: number;
	rows: IPouchDBRow[];
}

interface IPouchDBGetResult {
	_id: string;
	_rev: string;
}

interface IPouchDBPutResult {
	ok: boolean;
	id: string;
	rev: string;
}

interface IPouchDBRemoveResult {
	ok: boolean;
	id: string;
	rev: string;
}

interface IPouchDBRow {
	id: string;
	key: string;
	value: { rev: string };

	// Only included if include_docs is set to true during query.
	doc?: any;
}

interface IPouchDBGetFriendResult extends IPouchDBGetResult {
	name: string;
}


export class FriendService {

	private pouch: any;


	// I initialize the Friend service.
	constructor() {

		this.pouch = new PouchDB(
			"javascript-demos-pouchdb-angular2",
			{
				// PouchDB doesn't overwrite data - it creates revisions (like Git).
				// For the purposes of this app, however, we don't need those revisions
				// to stay around, taking up storage space. By enabling auto_compaction,
				// PouchDB will only keep the most current revision in storage.
				auto_compaction: true
			}
		);

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I add a new friend with the given name. Returns a promise of the generated id.
	public addFriend( name: string ) : Promise<string> {

		// NOTE: All friends are given the key-prefix of "friend:". This way, when we go
		// to query for friends, we can limit the scope to keys with in this key-space.
		var promise = this.pouch
			.put({
				_id: ( "friend:" + ( new Date() ).getTime() ),
				name: name
			})
			.then(
				( result: IPouchDBPutResult ) : string => {

					return( result.id );

				}
			)
		;

		return( promise );

	}


	// I delete the friend with the given id. Returns a promise.
	public deleteFriend( id: string ) : Promise<void> {

		this.testId( id );

		// When we delete a document, we have to provide a document that contains, at the
		// least, the "_id" and the "_rev" property. Since the calling context doesn't
		// have this, we'll use the .get() method to get the current doc, then use that
		// result to delete the winning revision of the document.
		var promise = this.pouch
			.get( id )
			.then(
				( doc: IPouchDBGetFriendResult ) : any => {

					return( this.pouch.remove( doc ) );

				}
			)
			.then(
				( result: IPouchDBRemoveResult ) : void => {

					// Here, I'm just stripping out the result so that the PouchDB
					// response isn't returned to the calling context.
					return;

				}
			)
		;

		return( promise );

	}


	// I get the collection of friends (in no particular sort order). Returns a promise.
	public getFriends() : Promise<IFriend[]> {

		var promise = this.pouch
			.allDocs({
				include_docs: true,

				// In PouchDB, all keys are stored in a single collection. So, in order
				// to return just the subset of "Friends" keys, we're going to query for
				// all documents that have a "friend:" key prefix. This is known as
				// "creative keying" in the CouchDB world.
				startkey: "friend:",
				endKey: "friend:\uffff"
			})
			.then(
				( result: IPouchDBAllDocsResult ) : IFriend[] => {

					// Convert the raw data storage into something more natural for the
					// calling context to consume.
					var friends = result.rows.map(
						( row: any ) : IFriend => {

							return({
								id: row.doc._id,
								name: row.doc.name
							});

						}
					);

					return( friends );

				}
			)
		;

		return( promise );

	}


	// I sort the given collection of friends (in place) based on the name property.
	public sortFriendsCollection( friends: IFriend[] ) : IFriend[] {

		friends.sort(
			function( a: IFriend, b: IFriend ) : number {

				if ( a.name.toLowerCase() < b.name.toLowerCase() ) {

					return( -1 );

				} else {

					return( 1 );

				}

			}
		);

		return( friends );

	}


	// I test the given id to make sure it is valid for the Friends key-space. Since all
	// PouchDB documents are stored in a single collection, we have to ensure that the
	// given ID pertains to the subset of documents that represents Friends. If the id is
	// valid, I return quietly; otherwise, I throw an error.
	public testId( id: string ) : void {

		if ( ! id.startsWith( "friend:" ) ) {

			throw( new Error( "Invalid Id" ) );

		}

	}


	// I update the friend with the given id, storing the given name. Returns a promise.
	public updateFriend( id: string, name: string ) : Promise<void> {

		this.testId( id );

		// When we update an existing document in PouchDB, we have to provide the "_rev"
		// of the document we're updating, otherwise PouchDB will throw a conflict.
		// However, since the calling context does not have the "_rev", we'll fetch the
		// document first, then update it in place, and put the resultant document back
		// into PouchDB (which will create a new revision).
		var promise = this.pouch
			.get( id )
			.then(
				( doc: IPouchDBGetFriendResult ) : Promise<IPouchDBPutResult> => {

					doc.name = name;

					return( this.pouch.put( doc ) );

				}
			)
			.then(
				( result: IPouchDBPutResult ) : void => {

					// Here, I'm just stripping out the result so that the PouchDB
					// response isn't returned to the calling context.
					return;

				}
			)
		;

		return( promise );

	}

}

As you can see, PouchDB provides a fairly simple API. But, there are definitely a few details to take note of. First, I am explicitly providing the ID for new documents that I create. PouchDB does have the ability to generate UUIDs automatically for you (with a post() operation); but, as stated above, providing an explicit ID allows us to use key-prefixes as a way to create pseudo-collections. That's why each new friend document is created with an id that starts with "friend:" and ends with the UTC milliseconds to ensure uniqueness.

NOTE: I know that I am sort of interchanging "id" and "key" terminology. I believe that these are actually two different - but related - concepts. Unfortunately, I currently lack the mental model necessary to clearly articulate the difference.

Second, the "update" and "delete" operations both start with a .get() call for the document in question. Since PouchDB is based on document revisions, mutation operations have to provide both a document ID and a revision ID in order to resolve any possible conflicts. Since the calling context (ie, the root component) doesn't know anything about revisions (at least not in this demo), I have to start each mutation by first retrieving the document from the database. This gives the FriendService a version of the document that contains both the doc ID and the revision ID, which I can then mutate (if necessary) and pass back to PouchDB for persistence.

Other than that, getting this basic CRUD app up and running with PouchDB was rather painless. And, when we run the above code, we get output that looks something like this:

Creating a simple PouchDB CRUD application in Angular 2.1.1.

When I first started reading up on PouchDB, it was a bit confusing because I kept trying to see the similarities to MongoDB. The more I read, however, the more I realized that - while both database are "NoSQL" databases - the similarities are fewer that you might expect. That said, PouchDB looks like a great database for "Offline First" applications because of its unique ability to employ master-master replication. And, from what I've seen here, consuming PouchDB within an Angular 2 application looks to be straightforward.

Want to use code from this post? Check out the license.

Reader Comments

6 Comments

Hello, Ben!
I just wanna ask about TypeScript in Angular 2. Should I use TS instead of ES6 for developing Angular 2 projects or it's not necessary? And what do you prefer? TS or ES6?
Thanks.

15,640 Comments

@FlamesoFF,

When I first started learning Angular 2, I was actually using *ES5* to write (which everyone seemed to hate me for doing ;)). But, ES5 was nice because it basically had no dependencies except the NG2 binary. But, eventually I switched over to TypeScript because it made the dependency-injection so much easier.

With TypeScript, you get use type-annotations on the Constructor arguments, like:

constructor( http: Http, friendService: FriendService ) { ... }

.... which allows Angular to know which things to inject. Without this, you would have to set a .parameters[] array on the Constructor after the fact. Which is possible, but a bit of pain.

So, for me, if you're gonna go the ES6 route, you might as well go the extra mile and use TypeScript. Even if you *only use* the type-annotations for DI, I think it's worth it.

That said, if you have a build process, your build process has to know about TypeScript. I am not very good at building things yet, so that's actually a big piece of complexity for me. But, it's doable, just likely not as straightforward with TS vs. ES6.

Hope that makes sense.

15,640 Comments

@Juana,

I feel that Promises more closely align with my intent. I have tried to articulate some my feelings in a more recent post on my evolving understanding of async workflows:

www.bennadel.com/blog/3202-my-evolving-angular-2-mental-model-promises-and-rxjs-observables.htm

... I am not against Observables; and, I think they will have a place in my application. But, I see them as predominantly living in the "controller" layer of my app. I see the "core" of my app mostly providing Promises.

2 Comments

@Ben,
Very nice article.

I have one question: When replication comes to play isn't it more natural to use Observables (to notify GUI when data is initialy created on server), rather than promises ?

15,640 Comments

@Mirza,

I think Observables do make sense for that. And for something like that, I think I would expose a stream from the service. Something like:

friendService.updates (or updateStream or something)

Then, anyone could bind to it and listen for changes. But, those changes are parallel to the concept of one-off commands that I am making against the FriendService. At least, that's how I'm starting to see it in my world.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel