I recently wrote about using Couchbase as a key-value store in a NativeScript iOS and Android application built with Angular. In this example we saw how to collect user profile information to be saved or loaded from Couchbase on the mobile devices. However, we were saving and loading based on a particular key, not querying based on document properties.
What if you have NoSQL documents where you don’t know the key and you need to query based on the document properties? In this example we’re going to take the previous article to the next level.
We’re going to bring our single page NativeScript application to multiple pages where the first page is a list of previously saved profiles and the second page allows you to create new profiles. Navigation between the pages will happen with the Angular Router.
The Requirements
The requirements between this guide and the previous remain the same. You’ll need the following:
- NativeScript CLI 2.0+
- Android SDK and / or Xcode for Mac
The NativeScript CLI which is obtained through the Node Package Manager (NPM) will allow us to create and build mobile projects. To build and deploy actual Android and iOS applications you’ll need either the Android SDK or Xcode or both.
Starting From Where We Left Off
We won’t be building our project from scratch, but instead we’ll be referencing the previous NativeScript with Couchbase tutorial. However, a lot of what you see here will be a review.
The previous project should be a basic one page application with the nativescript-couchbase plugin installed. We need to bring our single page into multiple pages.
Preparing Multiple Application Pages for Navigation
We will be using two pages in addition to the modal that we had previously created. Within your project, create the following files and directories:
1 2 3 4 5 6 |
app/components/profile-list/ app/components/profile-list/profile-list.ts app/components/profile-list/profile-list.html app/components/profile/ app/components/profile/profile.ts app/components/profile/profile.html |
The above files will represent our two pages. To be successful with the Angular Router we need to create a routing file that links these files together.
Create the following within your project:
1 |
app/app.routing.ts |
While we haven’t created our page classes yet, we’re going to focus on linking them together. Open the project’s app/app.routing.ts file and include the following:
1 2 3 4 5 6 7 8 9 10 11 12 |
import { ProfileListComponent } from "./components/profile-list/profile-list"; import { ProfileComponent } from "./components/profile/profile"; export const appRoutes: any = [ { path: "", component: ProfileListComponent }, { path: "profile", component: ProfileComponent } ]; export const appComponents: any = [ ProfileListComponent, ProfileComponent ]; |
As you can guess we’re going to have a ProfileListComponent
and a ProfileComponent
. When it comes to appRoutes
, the default page is the one with an empty route path. The second page will be navigate-able via the profile
path.
Adding each of the components to an array is a convenience for the next step.
Open the project’s app/app.module.ts file because we need to configure the router to use the routing file we had just created.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; import { NativeScriptModule } from "nativescript-angular/platform"; import { NativeScriptFormsModule } from "nativescript-angular/forms"; import { NativeScriptRouterModule } from "nativescript-angular/router"; import { ModalDialogService } from "nativescript-angular/modal-dialog"; import { appComponents, appRoutes } from "./app.routing"; import { AppComponent } from "./app.component"; import { ModalComponent } from "./app.modal"; @NgModule({ declarations: [AppComponent, ModalComponent, ...appComponents], entryComponents: [ModalComponent], bootstrap: [AppComponent], imports: [ NativeScriptModule, NativeScriptFormsModule, NativeScriptRouterModule, NativeScriptRouterModule.forRoot(appRoutes) ], providers: [ModalDialogService], schemas: [NO_ERRORS_SCHEMA] }) export class AppModule { } |
Notice in the above that we still have all the modal related items from the previous tutorial. This time around we’ve imported from the app/app.routing.ts file and injected into the imports
and declarations
array of the @NgModule
block.
The parent page for our routing system is the AppComponent
class we were using in the previous example. We need to wipe it out.
Open the project’s app/app.component.html file and include the following XML markup:
1 |
To pair with this we can remove most of the code within the app/app.component.ts file. This file should look similar to the following in the end:
1 2 3 4 5 6 7 |
import { Component } from "@angular/core"; @Component({ selector: "my-app", templateUrl: "app.component.html", }) export class AppComponent { } |
At this point we can start developing each of our application pages. For more information on routing using the Angular Router, visit a previous article I wrote on the subject.
Moving the Code from the Previous Couchbase NativeScript Example
Remember all the code I just told you to wipe out? I probably shouldn’t have said that, but lucky for us you can copy and paste it from the previous example, or better yet, below.
We need to take the code that was previously in the app/app.component.ts and app/app.component.html files and move it to the new app/components/profile/profile.ts and app/components/profile/profile.html files.
Open the app/components/profile/profile.html file and make it look like the following:
1 2 3 4 5 |
<label class="label"></label> <label class="label"></label> <button class="btn btn-primary w-full"></button> |
The only thing that has changed in the above is that I removed the button in the action bar that was used for loading data. Now open the project’s app/components/profile/profile.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 |
import { Component, ViewContainerRef } from "@angular/core"; import { Location } from "@angular/common"; import { ModalDialogService } from "nativescript-angular/directives/dialogs"; import { Couchbase } from "nativescript-couchbase"; import { ModalComponent } from "../../app.modal"; @Component({ selector: "profile", templateUrl: "./components/profile/profile.html", }) export class ProfileComponent { public profile: any; private database: any; public constructor(private modal: ModalDialogService, private vcRef: ViewContainerRef, private location: Location) { this.profile = { photo: "~/kitten1.jpg", firstname: "", lastname: "" } this.database = new Couchbase("data"); } public showModal(fullscreen: boolean) { let options = { context: { promptMsg: "Pick your avatar!" }, fullscreen: fullscreen, viewContainerRef: this.vcRef }; this.modal.showModal(ModalComponent, options).then((res: string) => { this.profile.photo = res || "~/kitten1.jpg"; }); } public save() { this.database.createDocument(this.profile); this.location.back(); } } |
To be fair, I removed the load
method that we had and included the Angular Location service for navigating backwards in the navigation stack. Everything, with the exception of paths, should be the same.
Remember, my application has avatar images of kittens that I found on the internet.
You should probably find your own avatar images and use them as appropriate. The ~/ represents that the images should be found in the project’s app directory.
Now we need to have a look at the yet to be created page for listing data on the screen.
Querying for Profiles and Adding Them to a List on the UI
Our new page should, in theory, be simpler than everything we’ve done so far and in the previous guide. Open the project’s app/components/profile-list/profile-list.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 |
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { Router } from "@angular/router"; import { Couchbase } from "nativescript-couchbase"; @Component({ selector: "profile-list", templateUrl: "./components/profile-list/profile-list.html", }) export class ProfileListComponent implements OnInit { public profiles: Array; private database: any; public constructor(private router: Router, private location: Location) { } public ngOnInit() { } public refresh() { } public create() { } } |
In the above we have a ProfileListComponent
class with a public variable to hold all our UI bound data and a private instance to our Couchbase database.
We need to be able to navigate forward in the navigation stack and detect navigation backwards in the stack so we inject the Router
and Location
services into the constructor
method. The constructor
method also does the following:
1 2 3 4 5 6 7 |
public constructor(private router: Router, private location: Location) { this.database = new Couchbase("data"); this.database.createView("profiles", "1", function(document, emitter) { emitter.emit(document._id, document); }); this.profiles = []; } |
In the above constructor
method we open our database and create a new MapReduce view. This view is very simplistic in the sense it will return a key-value pair of all documents in the database. There is no conditional logic around what it returns in this scenario. The profiles
MapReduce view is the foundation of our querying.
To query that view, we have the refresh
method:
1 2 3 4 5 6 7 |
public refresh() { this.profiles = []; let rows = this.database.executeQuery("profiles"); for(let i = 0; i < rows.length; i++) { this.profiles.push(rows[i]); } } |
After querying we push each of the results into our public profiles
variable. Don’t get confused, the profiles
variable is not the same as the profiles
view.
1 2 3 4 5 6 |
public ngOnInit() { this.location.subscribe(() => { this.refresh(); }); this.refresh(); } |
The ngOnInit
triggers after the constructor
method. Not only do we execute our query, but we need to subscribe to navigation events so we can execute the query when returning to the page. This is because the constructor
and the ngOnInit
methods only trigger at load, not navigation backwards events.
Our last method is the create
method:
1 2 3 |
public create() { this.router.navigate(["profile"]); } |
This will eventually be triggered by button press and it will take us to the second screen.
The UI for this page, found in the app/components/profile-list/profile-list.html, will look like the following:
1 2 3 |
<label class="label"></label> |
The will iterate over each item in our public array and convert them into rows. Each row will be of two columns where the first image column has a 50 width, and the second column stretches to fit.
Conclusion
You just saw how to take your key-value NativeScript with Couchbase application to the next level by adding MapReduce view queries for querying data based on document properties. While the profile data doesn’t do much, what if you wanted to sync what is stored in Couchbase on the device to other devices? We’ll take a look at that next time.