Not too long ago I created a Couchbase NoSQL plugin with Mehfuz Hossain from Telerik for the NativeScript framework. Since developing the plugin it has received a lot of positive feedback and great developer adoption. Shortly after its release I published a blog post on how to use the Couchbase plugin in a NativeScript app. The blog post I had written focused on using NativeScript’s proprietary vanilla framework. Since then, Angular has been released for NativeScript, so it makes sense to make an Angular version of the tutorial.
We’re going to see how to create a cross platform NativeScript application that syncs, using Angular and Couchbase.
There are a few requirements that must be met in order to be successful with this tutorial:
Angular support first became available in NativeScript 2.0. Anything higher will be good enough for this tutorial. For data synchronization we’ll need Couchbase Sync Gateway. It is open source. You can optionally use Couchbase Server, but we won’t go there for this tutorial.
Let’s start by creating a new project with all the dependencies. Using a Command Prompt (Windows) or Terminal (Mac and Linux), execute the following:
tns create CouchbaseProject --ng
cd CouchbaseProject
tns platform add ios
tns platform add android
A few things to note in the above. By using the --ng
tag we are saying that this will be an Angular project with TypeScript. Also note that if you’re not using a Mac, you cannot build for the iOS platform.
With the project created, we need to install the Couchbase NoSQL plugin. Using the Terminal or Command Prompt, execute the following:
tns plugin add nativescript-couchbase
This will install the plugin for both Android and iOS.
At this point we can focus on developing the actual application using Angular. Like most of the tutorials I have floating around, we’re going to keep things simple with a todo-like application. I’d rather not overwhelm anyone on the concepts I’m trying to prove. The application will have two screens, one for saving data to the NoSQL database and one for showing the data.
Before we jump into the development, we need to create a few files and directories:
mkdir app/components
mkdir app/components/list
mkdir app/components/create
touch app/components/list/list.component.ts
touch app/components/list/list.xml
touch app/components/create/create.component.ts
touch app/components/create/create.xml
touch app/couchbaseinstance.ts
If you’re using a Windows computer and not a Mac or Linux machine, you won’t have the mkdir
and touch
commands. Go ahead and make those files and directories manually.
To use Couchbase in our application we have to create a singleton instance, sometimes referred to as a shared service. If we don’t do this, we’re going to experience some strange behavior.
Open the project’s app/couchbaseinstance.ts file and include the following code:
import {Couchbase} from 'nativescript-couchbase';
export class CouchbaseInstance {
private database: any;
private pull: any;
private push: any;
constructor() { }
init() {
this.database = new Couchbase("nraboy-database");
this.database.createView("people", "1", (document, emitter) => {
emitter.emit(document._id, document);
});
}
getDatabase() {
return this.database;
}
startSync(continuous: boolean) {
this.push = this.database.createPushReplication("http://192.168.57.1:4984/test-database");
this.pull = this.database.createPullReplication("http://192.168.57.1:4984/test-database");
this.push.setContinuous(continuous);
this.pull.setContinuous(continuous);
this.push.start();
this.pull.start();
}
stopSync() {
this.push.stop();
this.pull.stop();
}
}
So what is going on in the above chunk of code?
We only want the Couchbase plugin initialized once which is why we don’t place the initialization code in the constructor. During the initialization we do a one time creation of the MapReduce view that will allow us to later query for data.
In each of our components we can make use of the getDatabase
, startSync
, and stopSync
functions. We want to make sure that we’re using the same database in all components. In terms of synchronization, we’re planning to continuously sync in two directions at the Sync Gateway defined. You’ll want to use the IP address of whatever your own Sync Gateway is.
The purpose of this component is for reading data from the database and displaying it on the screen. It will also start the synchronization of data in our application.
We’ll start by creating our TypeScript logic. Open the project’s app/components/list/list.component.ts file and include the following code:
import {Component, NgZone} from "@angular/core";
import {Router} from "@angular/router";
import {Location} from "@angular/common";
import {CouchbaseInstance} from "../../couchbaseinstance";
@Component({
selector: "my-app",
templateUrl: "components/list/list.component.html",
})
export class ListComponent {
private database: any;
private router: Router;
private ngZone: NgZone;
public personList: Array<Object>;
constructor(router: Router, location: Location, ngZone: NgZone, couchbaseInstance: CouchbaseInstance) {
this.router = router;
this.ngZone = ngZone;
this.database = couchbaseInstance.getDatabase();
this.personList = [];
couchbaseInstance.startSync(true);
this.database.addDatabaseChangeListener((changes) => {
let changeIndex;
for (var i = 0; i < changes.length; i++) {
let documentId = changes[i].getDocumentId();
changeIndex = this.indexOfObjectId(documentId, this.personList);
let document = this.database.getDocument(documentId);
this.ngZone.run(() => {
if (changeIndex == -1) {
this.personList.push(document);
} else {
this.personList[changeIndex] = document;
}
});
}
});
location.subscribe((path) => {
this.refresh();
});
this.refresh();
}
create() {
this.router.navigate(["/create"]);
}
private refresh() {
this.personList = [];
let rows = this.database.executeQuery("people");
for(let i = 0; i < rows.length; i++) {
this.personList.push(rows[i]);
}
}
private indexOfObjectId(needle: string, haystack: any) {
for (let i = 0; i < haystack.length; i++) {
if (haystack[i] != undefined && haystack[i].hasOwnProperty("_id")) {
if (haystack[i]._id == needle) {
return i;
}
}
}
return -1;
}
}
The above code is a lot to take in. Let’s start breaking it down.
The first thing you see is a bunch of import statements:
import {Component, NgZone} from "@angular/core";
import {Router} from "@angular/router";
import {Location} from "@angular/common";
import {CouchbaseInstance} from "../../couchbaseinstance";
The first three imports are part of the Angular framework and are responsible for the creation of our component and routing to other components. The fourth import is the import of our shared service for Couchbase.
In the @Component
section we define which XML template will be paired with the TypeScript.
Let’s work backwards on the methods. Starting with the indexOfObjectId
method:
private indexOfObjectId(needle: string, haystack: any) {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] != undefined && haystack[i].hasOwnProperty("_id")) {
if (haystack[i]._id == needle) {
return i;
}
}
}
return -1;
}
We’re going to use the above method for finding objects in our list view. This is useful when we are handling data changes. When data comes in we can use this function to determine if it is already in the list or not.
private refresh() {
this.personList = [];
let rows = this.database.executeQuery("people");
for(let i = 0; i < rows.length; i++) {
this.personList.push(rows[i]);
}
}
The refresh
function will query for all data in the database. This is useful for every time we navigate to the list component. All data queried will be pushed into the personList
variable which is bound to the XML layout.
create() {
this.router.navigate(["/create"]);
}
The create
function will use the Angular router to navigate to the soon to be created component for saving data.
Finally, this brings us to the constructor
method that does a lot of initialization. Essentially we are getting the database from the shared service and starting sync. However, we are also creating our change listener:
this.database.addDatabaseChangeListener((changes) => {
let changeIndex;
for (var i = 0; i < changes.length; i++) {
let documentId = changes[i].getDocumentId();
changeIndex = this.indexOfObjectId(documentId, this.personList);
let document = this.database.getDocument(documentId);
this.ngZone.run(() => {
if (changeIndex == -1) {
this.personList.push(document);
} else {
this.personList[changeIndex] = document;
}
});
}
});
Every time a change is detected, this listener will trigger. Changes could be local changes or changes that have come from Couchbase Sync Gateway. If the data already exists in the list, it will be updated, otherwise it will be added. Because these operations happen on a separate thread via Couchbase Lite, we need to use NgZone
in order to update the UI when a change is found.
The last critical piece of the constructor
method is the subscribe
found on the Location
component:
location.subscribe((path) => {
this.refresh();
});
We need this subscription for handling pop events on the navigation stack. After saving locally and returning to the list view, we want to query for data and display it in the list view.
This brings us to the XML layout that goes with the TypeScript component. Open the project’s app/components/list/list.xml file and include the following markup:
<ActionBar title="Person List">
<ActionItem text="Create" (tap)="create()" ios.position="right"></ActionItem>
</ActionBar>
<GridLayout>
<ListView [items]="personList">
<template let-item="item">
<Label [text]="item.firstname + ' ' + item.lastname"></Label>
</template>
</ListView>
</GridLayout>
Here we have a simple view that contains an action bar with a single button. This button will take us to the component for creating data. The core content is a ListView
that is bound to the personList
of the TypeScript file. Each object from that array is displayed on a per row basis.
Now we can start working on our second screen for creating new data to be saved into our NoSQL database.
Open the project’s app/components/create/create.component.ts file and include the following code:
import {Component} from "@angular/core";
import {Location} from "@angular/common";
import {CouchbaseInstance} from "../../couchbaseinstance";
@Component({
selector: "create",
templateUrl: "./components/create/create.component.html"
})
export class CreateComponent {
private couchbaseInstance: CouchbaseInstance;
private database: any;
private location: Location;
public firstname: string;
public lastname: string;
constructor(location: Location, couchbaseInstance: CouchbaseInstance) {
this.database = couchbaseInstance.getDatabase();
this.location = location;
this.firstname = "";
this.lastname = "";
}
save() {
if(this.firstname != "" && this.lastname != "") {
this.database.createDocument({
"firstname": this.firstname,
"lastname": this.lastname
});
this.location.back();
}
}
}
We’ll break this file down too.
Just like with the ListComponent
we are including some core Angular components and the Couchbase shared service. In the constructor
method we are initializing all of the variables to be used in this application.
This brings us to the save
method. We start by making sure that firstname
and lastname
are not empty. If this condition passes then we can call the createDocument
method and pass in an object to be saved. Since this is NoSQL you can pass any kind of complex object you want.
When the saving is done, we can pop back in the navigation to the previous component.
Now let’s take a look at the XML layout that is paired with the CreateComponent
. Open the project’s app/components/create/create.xml file and include the following markup:
<ActionBar title="Create">
<NavigationButton text="Back" ios.position="left"></NavigationButton>
<ActionItem text="Save" (tap)="save()" ios.position="right"></ActionItem>
</ActionBar>
<StackLayout>
<TextField hint="First Name" [(ngModel)]="firstname"></TextField>
<TextField hint="Last Name" [(ngModel)]="lastname"></TextField>
</StackLayout>
Nothing too fancy happening in the above. We have an action bar with a button for navigating backwards in the stack and a button for calling our save
method.
The two text fields are bound to our TypeScript file using the Angular [(ngModel)]
tag.
Angular requires some configuration logic to be put into place in order to perform routing between components. Lucky for us, this isn’t complicated to do.
Open the project’s app/app.component.ts file and include the following code:
import {Component} from "@angular/core";
import {NS_ROUTER_DIRECTIVES} from "nativescript-angular/router";
import {CouchbaseInstance} from './couchbaseinstance';
@Component({
selector: "my-app",
directives: [NS_ROUTER_DIRECTIVES],
template: "<page-router-outlet></page-router-outlet>"
})
export class AppComponent {
constructor(couchbaseInstance: CouchbaseInstance) {
couchbaseInstance.init();
}
}
All routes will pass through this file because of the <page-router-outlet>
tags. This file acts as the parent controller and it is called only when the application opens.
Because this component only triggers one time, take notice of the following:
constructor(couchbaseInstance: CouchbaseInstance) {
couchbaseInstance.init();
}
This routing component is called before any of our other components making it the perfect opportunity to initialize our shared Couchbase service. It should only be done once.
Although we have our outlet file, we don’t yet have our routes defined. This can be done in the app/main.ts file of our project. Open it and include the following code:
import {nativeScriptBootstrap} from "nativescript-angular/application";
import {RouterConfig} from "@angular/router";
import {nsProvideRouter} from "nativescript-angular/router";
import {AppComponent} from "./app.component";
import {CouchbaseInstance} from "./couchbaseinstance";
import {ListComponent} from "./components/list/list.component";
import {CreateComponent} from "./components/create/create.component";
export const AppRoutes: RouterConfig = [
{ path: "", component: ListComponent },
{ path: "create", component: CreateComponent }
]
nativeScriptBootstrap(AppComponent, [CouchbaseInstance, [nsProvideRouter(AppRoutes, {})]]);
Here we import all the possible components and give them a path. Any route with an empty path will be the default route, in other words it will be shown when the application opens.
If you wish to have synchronization support in your application you should have downloaded the open source Couchbase Sync Gateway.
The Sync Gateway requires a configuration file. We won’t get into the details here, but let’s use this super simple configuration file:
{
"log":["CRUD+", "REST+", "Changes+", "Attach+"],
"databases": {
"test-database": {
"server":"walrus:",
"sync":`
function (doc) {
channel (doc.channels);
}
`,
"users": {
"GUEST": {
"disabled": false,
"admin_channels": ["*"]
}
}
}
},
"CORS": {
"Origin": ["http://localhost:8100"],
"LoginOrigin": ["http://localhost:8100"],
"Headers": ["Content-Type"],
"MaxAge": 17280000
}
}
To sum up the above configuration file, we’re syncing all data with no read or write permissions. If you wish to know more about creating Couchbase Sync Gateway configuration files, check out the developer portal.
It can be run by executing the following from your Command Prompt or Terminal:
/path/to/sync/gateway/bin/sync-gateway sync-gateway-config.json
Your application now has cross platform synchronization support.
You just saw how to create a NativeScript Angular application that uses Couchbase NoSQL as its database. If you’re interested in seeing the version without Angular, see the previous blog post I wrote on the subject. Couchbase is an excellent solution because it is open source, and you’re not tied down to using a Backend as a Service (BaaS) that could potentially shut down like Facebook’s Parse service.
This project can be seen on GitHub along with the actual plugin. A video version of this article can be seen below.