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 Electrón that is powered by Angular 2, PouchDB, Ionic 2, and Couchbase.
Requisitos
There are numerous requirements that must be met to make this project a success. They are as follows:
- Node.js 4.0+
- Marco iónico 2.0
- Pasarela de sincronización Couchbase
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.
Configuración de 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+", "Cambios+", "Adjuntar+"], "bases de datos": { "ejemplo": { "servidor":"morsa:", "sync":` función (doc) { canal (doc.canales); } `, "usuarios": { "INVITADO": { "desactivado": falso, "admin_canales": ["*"] } } } } } |
In the above sample configuration, we’ll be using a database called ejemplo, 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 |
/ruta/a/sync_gateway /ruta/a/sincronizar-pasarela-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.
Desde el Símbolo del sistema o Terminal, ejecute lo siguiente:
1 2 |
iónico iniciar ElectronExample en blanco --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 instale pouchdb --guardar npm instale uuid @tipos/uuid --guardar npm instale electrón --guardar-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 instale @tipos/nodo --guardar |
The above dependency will give us access to the requiere
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 electrón = requiere('electron') const aplicación = electrón.aplicación const BrowserWindow = electrón.BrowserWindow const ruta = requiere(camino) const url = requiere('url') deje mainWindow función createWindow () { mainWindow = nuevo BrowserWindow({anchura: 800, altura: 600}) mainWindow.loadURL(url.formato({ pathname: ruta.únase a(__dirname, 'www/index.html'), protocolo: 'file:', slashes: verdadero })) mainWindow.en(cerrado, función () { mainWindow = null }) } aplicación.en(listo, createWindow) aplicación.en(ventana-todo-cerrado, función () { si (proceso.plataforma !== darwin) { aplicación.abandone() } }) aplicación.en('activate', función () { si (mainWindow === null) { createWindow() } }) |
Most of the above code was taken directly from the Electron Starter Template, with the exception of this line:
1 |
pathname: ruta.únase a(__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.
Abra el archivo paquete.json file and include this line:
1 |
"principal": "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 |
iónico g proveedor pouchdb-proveedor |
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 |
importar { Inyectable, Emisor de eventos } de @angular/core; var PouchDB = requiere("pouchdb"); @Inyectable() exportar clase PouchDBProvider { privado isInstantiated: booleano; privado base de datos: cualquier; privado oyente: Emisor de eventos = nuevo Emisor de eventos(); público constructor() { } público consiga(id: cadena) { } público poner(documento: cualquier, id: cadena) { } público sincronizar(remoto: cadena) { } público getChangeListener() { } } |
The provider will be injected in various pages and emit changes, hence the Inyectable
y Emisor de eventos
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
método:
1 2 3 4 5 6 7 8 9 10 11 12 |
público constructor() { si(!este.isInstantiated) { este.base de datos = nuevo PouchDB("nraboy"); este.base de datos.cambia({ en directo: verdadero, incluir_docs: verdadero }).en(cambio, cambiar => { este.oyente.emite(cambiar); }); este.isInstantiated = verdadero; } } |
En el 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 |
público consiga(id: cadena) { devolver este.base de datos.consiga(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 |
público poner(documento: cualquier, id: cadena) { documento._id = id; devolver este.consiga(id).entonces(resultado => { documento._rev = resultado._rev; devolver este.base de datos.poner(documento); }, error => { si(error.estado == "404") { devolver este.base de datos.poner(documento); } si no { devolver nuevo Promesa((resolver, rechace) => { rechace(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 |
público sincronizar(remoto: cadena) { deje remoteDatabase = nuevo PouchDB(remoto); este.base de datos.sincronizar(remoteDatabase, { en directo: verdadero, reintentar: verdadero }); } |
En sincronizar
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 |
público getChangeListener() { devolver este.oyente; } |
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
que se encuentra en el bloque src/app/app.module.ts archivo. Abra este archivo e incluya lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
importar { NgModule, ErrorHandler } de @angular/core; importar { IonicApp, IonicModule, IonicErrorHandler } de iónico-angular; importar { MyApp } de './app.component'; importar { Página de inicio } de '../páginas/hogar/hogar'; importar { PouchDBProvider } de "../providers/pouchdb-provider"; @NgModule({ declaraciones: [ MyApp, Página de inicio ], importaciones: [ IonicModule.forRoot(MyApp) ], arranque: [IonicApp], entryComponents: [ MyApp, Página de inicio ], proveedores: [{proporcionar: ErrorHandler, useClass: IonicErrorHandler}, PouchDBProvider] }) exportar clase AppModule {} |
Essentially we only imported the provider and added it to the proveedores
de la @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 e incluya lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
importar { Componente, NgZone } de @angular/core; importar { NavController, AlertController } de iónico-angular; importar { PouchDBProvider } de "../../providers/pouchdb-provider"; importar * como Uuid de "uuid"; @Componente({ selector: página de inicio, templateUrl: Inicio.html }) exportar clase Página de inicio { público artículos: Matriz; público constructor(público navCtrl: NavController, público alertCtrl: AlertController, privado zona: NgZone, privado base de datos: PouchDBProvider) { este.artículos = []; } público ionViewDidEnter() { } público insertar() { } } |
We’ve imported several Angular 2, Ionic 2, and custom components into the page and injected many of them into the constructor
método. En artículos
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
método:
1 2 3 4 5 6 7 8 |
público ionViewDidEnter() { este.base de datos.sincronizar("http://192.168.57.1:4984/example"); este.base de datos.getChangeListener().suscríbase a(datos => { este.zona.ejecute(() => { este.artículos.pulse(datos.doc); }); }); } |
En el 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 artículos
array. We are using NgZone
because change listeners are iffy in Angular 2 and we want to guarantee that the UI is updated correctly.
En insertar
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 |
público insertar() { deje consulte = este.alertCtrl.crear({ título: Todo, mensaje: "Añadir un nuevo elemento a la lista de tareas pendientes", entradas: [ { nombre: título, marcador de posición: Título }, ], botones: [ { texto: Cancelar, manipulador: datos => {} }, { texto: Guardar, manipulador: datos => { si(datos.título) { este.base de datos.poner({tipo: "lista", título: datos.título}, Uuid.v4()); } } } ] }); consulte.presente(); } |
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 e incluya el siguiente código HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<ion-cabecera> <ion-barra de navegación> <ion-título> Electrón w/ Couchbase </ion-título> <ion-botones fin> <botón ion-botón icono-sólo (haga clic en)="insert()"> <ion-icono nombre="añadir"></ion-icono> </botón> </ion-botones> </ion-barra de navegación> </ion-cabecera> <ion-contenido acolchado> <ion-lista> <ion-artículo *ngFor="let item of items"> {{ artículo.título }} </ion-artículo> </ion-lista> </ion-contenido> |
The UI has an action bar with a button that will execute the insertar
method when pressed. The content of the screen is a list that displays each element from the artículos
array as a row.
Seeing the Project in Action
There was a lot that we did in the Electron with Couchbase example. I’ve cargado un proyecto de trabajo en GitHub si quieres probarlo.
Download the project and execute the following command:
1 |
npm instale |
The above command restore all the project dependencies. With Sync Gateway running, execute the following to run the application with Electron:
1 |
npm ejecute electrón |
Note that you’ll probably have to change the Sync Gateway host in the src/pages/home/home.ts file to match your hostname.
Conclusión
You just saw how to create a desktop application that syncs with Couchbase. This application is powered by Electrón, 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).
¿Alguna idea?
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.
Hola,
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 .”
Véase 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!