I recently hosted a Couchbase Meetup in Mountain View, California, on the topic of NativeScript, Angular and NoSQL development. With special guest, TJ VanToll from Progress, we discussed mobile application development and how Couchbase can be included for NoSQL storage and data synchronization.
There was a good turnout at the event and per popular request, I wanted to share and review the code used to make the Couchbase project possible.
Assuming you have the NativeScript CLI and either Xcode or the Android SDK installed and configured on your machine, we can create a new project from the Command Prompt or Terminal:
1 |
tns create couchbase-project --ng |
The --ng
flag in the above command indicates that we’re creating an Angular project rather than a Core project.
The project we create will consist of two pages and a data service.
Within the application you’ll be able to show a list of movies stored in your database as well as add movies to that database. This is all managed through the data service. With Couchbase Sync Gateway available, synchronization will be able to happen.
Add the following directories and files to your fresh NativeScript project:
1 2 3 4 5 6 7 8 |
mkdir -p app/components/create mkdir -p app/components/list mkdir -p app/providers touch app/components/create/create.component.ts touch app/components/create/create.component.html touch app/components/list/list.component.ts touch app/components/list/list.component.html touch app/providers/database.service.ts |
In Angular development, each component will have a TypeScript and HTML file. Each service will only have a TypeScript file.
Let’s start by designing our data service which will handle interactions with the locally installed Couchbase Lite database. Open the project’s app/providers/database.service.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 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import { Injectable, EventEmitter } from "@angular/core"; import { Couchbase } from "nativescript-couchbase"; @Injectable() export class DatabaseService { private database: any; private pushReplicator: any; private pullReplicator: any; private listener: EventEmitter<any> = new EventEmitter(); public constructor() { this.database = new Couchbase("movie-db"); this.database.createView("movies", "1", function(document, emitter) { if(document.type == "movie") { emitter.emit(document._id, document); } }); } public query(viewName: string): Array<any> { return this.database.executeQuery(viewName); } public startReplication(gateway: string, bucket: string) { this.pushReplicator = this.database.createPushReplication("http://" + gateway + ":4984/" + bucket); this.pullReplicator = this.database.createPullReplication("http://" + gateway + ":4984/" + bucket); this.pushReplicator.setContinuous(true); this.pullReplicator.setContinuous(true); this.database.addDatabaseChangeListener(changes => { this.listener.emit(changes); }); this.pushReplicator.start(); this.pullReplicator.start(); } public getDatabase() { return this.database; } public getChangeListener() { return this.listener; } } |
There is a lot happening in the above service, so we should break it down.
After importing all the component dependencies and defining our variables, we have our constructor
method:
1 2 3 4 5 6 7 8 |
public constructor() { this.database = new Couchbase("movie-db"); this.database.createView("movies", "1", function(document, emitter) { if(document.type == "movie") { emitter.emit(document._id, document); } }); } |
In the constructor
method we create and open the Couchbase NoSQL database as well as create a view which will be used for querying data. The view logic says that when queried, return a key-value pair for every document that has a property named type
that equals movie
. Any other documents that don’t match this condition will not be included in the results.
1 2 3 |
public query(viewName: string): Array<any> { return this.database.executeQuery(viewName); } |
When querying the view, we’ll receive an array of results which we can choose to display on the screen. This is something we’ll do in the appropriate component.
To leverage the power and awesomeness of Couchbase, we want to have replication / synchronization support within the application. Within the startReplication
method, we have the following:
1 2 3 4 5 6 7 8 9 10 11 |
public startReplication(gateway: string, bucket: string) { this.pushReplicator = this.database.createPushReplication("http://" + gateway + ":4984/" + bucket); this.pullReplicator = this.database.createPullReplication("http://" + gateway + ":4984/" + bucket); this.pushReplicator.setContinuous(true); this.pullReplicator.setContinuous(true); this.database.addDatabaseChangeListener(changes => { this.listener.emit(changes); }); this.pushReplicator.start(); this.pullReplicator.start(); } |
If we provide the information for our Couchbase Sync Gateway instance, we can replicate the data in both directions continuously. Since the mobile application never reads the remote data, we set up a change listener when the local data changes. These changes are emitted via an Angular emitter.
To be able to inject this data service into each of our components, we need to import it into the project’s @NgModule
block. Open the project’s app/app.module.ts file and make it look like the following:
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 |
import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; import { NativeScriptModule } from "nativescript-angular/nativescript.module"; import { NativeScriptFormsModule } from "nativescript-angular/forms"; import { AppRoutingModule } from "./app.routing"; import { AppComponent } from "./app.component"; import { ListComponent } from "./components/list/list.component"; import { CreateComponent } from "./components/create/create.component"; import { DatabaseService } from "./providers/database.service"; @NgModule({ bootstrap: [ AppComponent ], imports: [ NativeScriptModule, NativeScriptFormsModule, AppRoutingModule ], declarations: [ AppComponent, ListComponent, CreateComponent ], providers: [DatabaseService], schemas: [ NO_ERRORS_SCHEMA ] }) export class AppModule { } |
Notice that the service was imported and included in the providers
array of the @NgModule
block. Getting ahead of ourselves, we’ve also imported each of the components that we’re creating.
Now let’s jump into the component for adding new movies to the database. Open the project’s app/components/create/create.component.ts file and include the following TypeScript 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 |
import { Component } from "@angular/core"; import { Location } from "@angular/common"; import { DatabaseService } from "../../providers/database.service"; @Component({ moduleId: module.id, selector: "ns-create", templateUrl: "create.component.html", }) export class CreateComponent { private database: any; public input: any; public constructor(private location: Location, private couchbase: DatabaseService) { this.database = this.couchbase.getDatabase(); this.input = { "title": "", "genre": "", "type": "movie" } } public save() { if(this.input.title && this.input.genre) { this.database.createDocument(this.input); this.location.back(); } } } |
In the above code we are injecting the DatabaseService
previously created along with the Angular Location
service. Using the DatabaseService
we can obtain the database instance and save the input
object to it when the save
method is called. The input
object is bound to a form in the UI.
The UI to this component can be described in the project’s app/components/create/create.component.html file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<ActionBar title="{N} Couchbase Example"> <ActionItem text="Save" ios.position="right" (tap)="save()"></ActionItem> </ActionBar> <StackLayout class="form"> <StackLayout class="input-field"> <Label text="Movie Title" class="label font-weight-bold m-b-5"></Label> <TextField class="input" [(ngModel)]="input.title"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Label text="Movie Genre" class="label font-weight-bold m-b-5"></Label> <TextField class="input" [(ngModel)]="input.genre"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> </StackLayout> |
In the above XML, notice the two TextField
tags are bound to the input
variable via the Angular ngModel
attributes.
The final part of the NativeScript application involves listing the movies that are in our database. Open the project’s app/components/list/list.component.ts file and include the following TypeScript 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 |
import { Component, NgZone, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { isAndroid } from "platform"; import { DatabaseService } from "../../providers/database.service"; @Component({ moduleId: module.id, selector: "ns-list", templateUrl: "list.component.html", }) export class ListComponent implements OnInit { public movies: Array<any>; public constructor(private location: Location, private zone: NgZone, private couchbase: DatabaseService) { this.movies = []; } public ngOnInit() { this.location.subscribe(() => { this.movies = this.couchbase.query("movies"); }); this.movies = this.couchbase.query("movies"); this.couchbase.startReplication(isAndroid ? "10.0.2.2" : "localhost", "movie-db"); this.couchbase.getChangeListener().subscribe(data => { for (let i = 0; i < data.length; i++) { let documentId = data[i].getDocumentId(); let document = this.couchbase.getDatabase().getDocument(documentId); this.zone.run(() => { this.movies.push(document); }); } }); } } |
Again, we are injecting the DatabaseService
, Location
, and NgZone
services into the component’s constructor
method.
It is never a good idea to load data in the constructor
method, so we’re using the ngOnInit
method instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public ngOnInit() { this.location.subscribe(() => { this.movies = this.couchbase.query("movies"); }); this.movies = this.couchbase.query("movies"); this.couchbase.startReplication(isAndroid ? "10.0.2.2" : "localhost", "movie-db"); this.couchbase.getChangeListener().subscribe(data => { for (let i = 0; i < data.length; i++) { let documentId = data[i].getDocumentId(); let document = this.couchbase.getDatabase().getDocument(documentId); this.zone.run(() => { this.movies.push(document); }); } }); } |
A few things are happening in the ngOnInit
method. We want to load the data from the database, but it needs to be done two different ways. We need to load the data when the application opens and we need to load the data when navigating backwards from the creation screen.
Because the ngOnInit
method doesn’t trigger when navigating backwards, we need to subscribe to the location events. In both scenarios we query the view that we had created.
Since we want synchronization support, we can call the startReplication
service and pass the Sync Gateway information. If you’re testing locally, make sure to provide appropriate host information for Android and iOS.
While listening for changes, any data that come through should be looked up by id and added to the list.
The UI that pairs with the component for listing movies can be found in the app/components/list/list.component.html file:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<ActionBar title="{N} Couchbase Example"> <ActionItem text="Add" ios.position="right" [nsRouterLink]="['/create']"></ActionItem> </ActionBar> <GridLayout> <ListView [items]="movies" class="list-group"> <ng-template let-movie="item"> <StackLayout class="list-group-item"> <Label text="{{ movie.title }}" class="h2"></Label> <Label text="{{ movie.genre }}"></Label> </StackLayout> </ng-template> </ListView> </GridLayout> |
In the above XML, we have a simple ListView
where each row contains information from the objects that we’re storing in Couchbase.
Bringing the NativeScript application to a close, we have to fix up our routing file which is responsible for navigation. Open the project’s app/app.routing.ts file and include the following TypeScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { NgModule } from "@angular/core"; import { NativeScriptRouterModule } from "nativescript-angular/router"; import { Routes } from "@angular/router"; import { ListComponent } from "./components/list/list.component"; import { CreateComponent } from "./components/create/create.component"; const routes: Routes = [ { path: "", redirectTo: "/list", pathMatch: "full" }, { path: "list", component: ListComponent }, { path: "create", component: CreateComponent } ]; @NgModule({ imports: [NativeScriptRouterModule.forRoot(routes)], exports: [NativeScriptRouterModule] }) export class AppRoutingModule { } |
In the above code we’re only importing the two components and listing them as possible routes within the application. More information on routing with a NativeScript with Angular application can be found in a previous article that I wrote titled, Navigating a NativeScript App with the Angular Router.
Remember, our project not only consists of NativeScript, but it also consists of Sync Gateway which is a separate entity. We need to define a configuration file on how synchronization should work.
Create a file called, sync-gateway-config.json and include 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": { "movie-db": { "server":"walrus:data", "sync":` function (doc) { channel (doc.channels); } `, "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } } } |
When launching Couchbase Sync Gateway, the above configuration should be used. It is only a basic example of what you can accomplish with data synchronization.
Conclusion
I personally believe NativeScript is a great technology for mobile development. When using the community supported Couchbase plugin for NativeScript, you can include NoSQL and data synchronization support within your application.
If you’re not currently registered to the Couchbase Silicon Valley group and are in the Mountain View area, I recommend taking a moment to register. If you’re interested in seeing more Couchbase with NativeScript in action, have a look at a previous article I wrote about here.
For more information on Couchbase Lite, check out the Couchbase Developer Portal.