I’ve been on a role when it comes to JavaScript and Couchbase. Over the past few weeks I’ve been creating content revolving around the Angular 2 framework, the JavaScript library PouchDB, and Ionic Framework for mobile. What I haven’t explored yet is Couchbase in a desktop application built with Angular 2.
Two years ago I wrote about using Couchbase in a desktop application using AngularJS 1.0 and Electron, but with technology there might as well have been dinosaurs in that time period. A lot has changed and that once great post deserves a refresh.
We’re going to see how to create a desktop application using Electron that is powered by Angular 2, PouchDB, Ionic 2, and Couchbase.
The Requirements
There are numerous requirements that must be met to make this project a success. They are as follows:
- Node.js 4.0+
- Ionic Framework 2.0
- Couchbase Sync Gateway
The focus and point of this tutorial is not Ionic 2. However, Ionic 2 has a mighty fine UI layer that will save us time over alternative solutions like Bootstrap or Foundation. Ionic 2 also has Angular 2 baked in. That said, we’ll need the Ionic 2 CLI installed. Node.js is a requirement because of the Node Package Manager (NPM) which we’ll use to gather dependencies.
We won’t be including Couchbase Server in this example, but instead Couchbase Sync Gateway and it’s in-memory prototyping database. It isn’t more than a single line to switch over to Couchbase Server from this example. PouchDB will communicate from our desktop application to Sync Gateway and in the other direction as well.
Configuring Couchbase Sync Gateway
Sync Gateway handles all the data orchestration and needs to be configured on a per application basis. For this particular application we’re going to allow all connecting devices to be able to read and write data.
Such a configuration would look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "log":["CRUD+", "REST+", "Changes+", "Attach+"], "databases": { "example": { "server":"walrus:", "sync":` function (doc) { channel (doc.channels); } `, "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } } } |
In the above sample configuration, we’ll be using a database called example, but it can really be called whatever you want. The configuration should be saved to a file, for example, sync-gateway-config.json.
With Couchbase Sync Gateway downloaded and installed, the configuration can be run as follows:
1 |
/path/to/sync_gateway /path/to/sync-gateway-config.json |
Sync Gateway can then be accessed from http://localhost:4985/_admin/.
Creating an Ionic 2 Project Destined for the PC
If you saw my previous guide on the topic of Couchbase with Ionic 2 and PouchDB, you’re probably wondering what will be different here. The truth is, nothing really. Ionic 2 applications that don’t use native Android and iOS features can be bundled into Electron applications without issue. However, some optimizations have been made to this application versus the previous.

In the above animation you can see a simple todo list Electron application where you can add items to the list and have them sync with Couchbase Sync Gateway and eventually other devices and Couchbase Server.
Let’s start creating that simple example.
Making a Fresh Project with the Dependencies
Before we can start developing the application and packaging it in Electron, we need to create a fresh project with each of the dependencies.
From the Command Prompt or Terminal, execute the following:
1 2 |
ionic start ElectronExample blank --v2 cd ElectronExample |
With the Ionic 2 project created we need to gather a few dependencies such as Electron and PouchDB. To do this execute the following:
1 2 3 |
npm install pouchdb --save npm install uuid @types/uuid --save npm install electron --save-dev |
Because this Angular 2 application will be using TypeScript, we are best off using JavaScript libraries with type definitions. However, PouchDB doesn’t have an official set of type definitions, rendering them out of date. We can get by this by including the following dependency:
1 |
npm install @types/node --save |
The above dependency will give us access to the require
keyword so we can import JavaScript libraries into our project.
Adding Electron for Desktop Support
With the project created we need to add Electron support. This is done by adding a special JavaScript configuration file that Electron processes at boot.
At the root of your project, create a file called electron.js with the following JavaScript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
const electron = require('electron') const app = electron.app const BrowserWindow = electron.BrowserWindow const path = require('path') const url = require('url') let mainWindow function createWindow () { mainWindow = new BrowserWindow({width: 800, height: 600}) mainWindow.loadURL(url.format({ pathname: path.join(__dirname, 'www/index.html'), protocol: 'file:', slashes: true })) mainWindow.on('closed', function () { mainWindow = null }) } app.on('ready', createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', function () { if (mainWindow === null) { createWindow() } }) |
Most of the above code was taken directly from the Electron Starter Template, with the exception of this line:
1 |
pathname: path.join(__dirname, 'www/index.html'), |
In the above line we are telling Electron which Ionic 2 page to load when the application starts. While the Electron bootstrapping is done, we need to get it bundled within our build scripts.
Open the project’s package.json file and include this line:
1 |
"main": "electron.js", |
The above line tells Electron which file is the configuration file. It is necessary because maybe you didn’t name your file electron.js like I did. We also need to add a particular script that will build the project and launch it with Electron:
1 2 3 4 5 |
"scripts": { "ionic:build": "ionic-app-scripts build", "ionic:serve": "ionic-app-scripts serve", "electron": "ionic-app-scripts build; electron ." }, |
It won’t package the application for deployment in an app store, but it will allow us to test it correctly with Electron on our computer.
At this point we can focus on the Angular 2 code which you may or may not have seen before.
Developing a PouchDB Provider for Angular 2
It is good practice to keep data related code separated from your page logic. In Angular 2 we can accomplish this separation through the use of a shared provider.
To create a provider in Ionic 2, execute the following from the CLI:
1 |
ionic g provider pouchdb-provider |
You should end up with src/providers/pouchdb-provider.ts or similar. The name isn’t really important as long as you remember what it is.
Open the provider file and include the following TypeScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Injectable, EventEmitter } from '@angular/core'; var PouchDB = require("pouchdb"); @Injectable() export class PouchDBProvider { private isInstantiated: boolean; private database: any; private listener: EventEmitter = new EventEmitter(); public constructor() { } public get(id: string) { } public put(document: any, id: string) { } public sync(remote: string) { } public getChangeListener() { } } |
The provider will be injected in various pages and emit changes, hence the Injectable
and EventEmitter
imports. We are also importing the PouchDB JavaScript library.
The provider will act as a singleton with one database instance open for the duration that our application is open. This setup can be created in the constructor
method:
1 2 3 4 5 6 7 8 9 10 11 12 |
public constructor() { if(!this.isInstantiated) { this.database = new PouchDB("nraboy"); this.database.changes({ live: true, include_docs: true }).on('change', change => { this.listener.emit(change); }); this.isInstantiated = true; } } |
In the constructor
method, if the databases isn’t already instantiated we are going to instantiate it, open it, and configure the change events. For every change against the database we will emit them and eventually pick them up by subscribing to the listener.
Say we want to get a particular NoSQL document by its id. We can create a function like the following:
1 2 3 |
public get(id: string) { return this.database.get(id); } |
For this particular application, the above method is more useful for our document creation method as we want to see if a document exists before we try to update it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public put(document: any, id: string) { document._id = id; return this.get(id).then(result => { document._rev = result._rev; return this.database.put(document); }, error => { if(error.status == "404") { return this.database.put(document); } else { return new Promise((resolve, reject) => { reject(error); }); } }); } |
If the document doesn’t exist, it will be created instead of updated.
Because the goal here is to use Couchbase in our desktop Electron application, we need to have a sync function:
1 2 3 4 5 6 7 |
public sync(remote: string) { let remoteDatabase = new PouchDB(remote); this.database.sync(remoteDatabase, { live: true, retry: true }); } |
The sync
method will take our Sync Gateway hostname and database and do a two-way sync between our local database and the remote database.
1 2 3 |
public getChangeListener() { return this.listener; } |
Finally we provide a way to obtain and subscribe to the change listener.
The PouchDB provider is done, but it is not ready to be used. To use it in each of our pages we need to add it to the application’s @NgModule
block found in the project’s src/app/app.module.ts file. Open this file and include the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 { PouchDBProvider } from "../providers/pouchdb-provider"; @NgModule({ declarations: [ MyApp, HomePage ], imports: [ IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp, HomePage ], providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, PouchDBProvider] }) export class AppModule {} |
Essentially we only imported the provider and added it to the providers
array of the @NgModule
block. Now the provider can be used in our application pages.
Adding the Page Logic for a Functional Application
This particular application only has one screen and that one screen is rather simple. Show a list of data and allow for new data to be input.
Starting with the TypeScript logic, open the project’s src/pages/home/home.ts file and include the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Component, NgZone } from '@angular/core'; import { NavController, AlertController } from 'ionic-angular'; import { PouchDBProvider } from "../../providers/pouchdb-provider"; import * as Uuid from "uuid"; @Component({ selector: 'page-home', templateUrl: 'home.html' }) export class HomePage { public items: Array; public constructor(public navCtrl: NavController, public alertCtrl: AlertController, private zone: NgZone, private database: PouchDBProvider) { this.items = []; } public ionViewDidEnter() { } public insert() { } } |
We’ve imported several Angular 2, Ionic 2, and custom components into the page and injected many of them into the constructor
method. The items
array will hold our synchronized data that will be displayed on the screen. The constructor
method only initializes our variables.
To load data into our variables we should use the ionViewDidEnter
method:
1 2 3 4 5 6 7 8 |
public ionViewDidEnter() { this.database.sync("http://192.168.57.1:4984/example"); this.database.getChangeListener().subscribe(data => { this.zone.run(() => { this.items.push(data.doc); }); }); } |
In the ionViewDidEnter
method we are starting the synchronization with our Sync Gateway and subscribing to the change events. As changes come in they will be added to the items
array. We are using NgZone
because change listeners are iffy in Angular 2 and we want to guarantee that the UI is updated correctly.
The insert
method is pretty much all Ionic 2 logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public insert() { let prompt = this.alertCtrl.create({ title: 'Todo Items', message: "Add a new item to the todo list", inputs: [ { name: 'title', placeholder: 'Title' }, ], buttons: [ { text: 'Cancel', handler: data => {} }, { text: 'Save', handler: data => { if(data.title) { this.database.put({type: "list", title: data.title}, Uuid.v4()); } } } ] }); prompt.present(); } |
When the method is called, a prompt will display allowing for user input. When saved, the data will be saved as a document in PouchDB and synced to Couchbase Server.
Designing the User Interface
The UI behind the TypeScript logic is short and sweet. Open the project’s src/pages/home/home.html file and include the following HTML markup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<ion-header> <ion-navbar> <ion-title> Electron w/ Couchbase </ion-title> <ion-buttons end> <button ion-button icon-only (click)="insert()"> <ion-icon name="add"></ion-icon> </button> </ion-buttons> </ion-navbar> </ion-header> <ion-content padding> <ion-list> <ion-item *ngFor="let item of items"> {{ item.title }} </ion-item> </ion-list> </ion-content> |
The UI has an action bar with a button that will execute the insert
method when pressed. The content of the screen is a list that displays each element from the items
array as a row.
Seeing the Project in Action
There was a lot that we did in the Electron with Couchbase example. I’ve uploaded a working project to GitHub if you’d like to take it for a spin.
Download the project and execute the following command:
1 |
npm install |
The above command restore all the project dependencies. With Sync Gateway running, execute the following to run the application with Electron:
1 |
npm run electron |
Note that you’ll probably have to change the Sync Gateway host in the src/pages/home/home.ts file to match your hostname.
Conclusion
You just saw how to create a desktop application that syncs with Couchbase. This application is powered by Electron, but uses Angular 2, and PouchDB. While the UI layer was Ionic 2, it didn’t need to be. You could use your own UI framework like Bootstrap or similar. The goal here was more to demonstrate Electron with Couchbase and Angular 2. This guide was a step up from my previous on the topic of AngularJS 1.0 with Couchbase and Electron.
When running
npm run electron
The result is an error:
[19:46:53] ionic-app-scripts 0.0.47
[19:46:53] ionic-app-script task: “build;”
[19:46:53] Error: Cannot find module ‘../dist/build;’
(And this is from your own github example, downloaded, did npm install, and npm run electron).
Any ideas?
Thanks :)
Hi Martín and Nic,
I’m getting the same error, using Ionic 2.1.12
[15:44:02] ionic-app-scripts 1.1.4
[15:44:02] ionic-app-script task: “build;”
[15:44:02] Error: Cannot find module ‘../dist/build;’
Did one of you solved it?
Thanks :)
What version of Ionic are you using? I believe Ionic had major changes between the releases of the current and when I published it.
The content and logic should remain accurate, but the build command might be slightly different.
Hi,
First, thank you Nic for this post!
I got the same error (“Error: Cannot find module ‘../dist/build;’”), and I think it’s because the script “electron” supposes the electron npm module is installed globally.
With electron installed locally, you should use something like this:
“./node_modules/.bin/electron .”
See https://electron.atom.io/docs/tutorial/quick-start/
So, I changed the electron script with this (I’m under Windows OS):
“scripts”: {
…
“electron”: “ionic-app-scripts build && .\\node_modules\\.bin\\electron .”
}
Finally, executing “npm run electron” works for me!