Working with RESTful Data in Angular 2 and NativeScript

NativeScript and Angular 2 provide you with the tooling to accomplish some pretty great things in the native mobile space. For example, I wrote an article not too long ago demonstrating how to create a barcode scanning application with a local inventory, which used a variety of technologies including NativeScript and Angular 2.

Creating our own local dataset is great, but what if we want to work with a dataset that someone else created? Angular 2 and RxJS makes it very easy to consume RESTful API data to be used in any application. In this example, we're going to consume an API that provides stock quotes for actual companies.

You can download a zip of the source code for the project here.

Creating a New NativeScript Project

To keep things simple, we're going to create a fresh NativeScript project and work our way up. From the Terminal (Linux and Mac) or Command Prompt (Windows), execute the following:

tns create StockTicker --ng
cd StockTicker
tns platform add ios
tns platform add android

Notice that we're using the --ng tag to create an Angular 2 project. Also take note that we're adding the iOS build platform, but we can't actually build and test for iOS unless we're using a Mac.

The application we are building is demonstrated in the following animated image.

NativeScript Stock Quotes

In the above, you'll notice that we're building a single page application where we can input stock symbols via a prompt and those symbols populate a list with quotes and descriptions.

What you don't see is that the information displayed is cached in a local NoSQL database.

Creating a Local Database Provider with Angular 2

We don't want to be left with a blank slate every time we open our application. To prevent this, we can store all data in a database and query it as necessary.

Since this is a JavaScript-based application, NoSQL works incredibly well because objects can be mapped directly to JSON in the database rather than having to run SQL and parse the result set. We're going to use the open source NoSQL database, Couchbase, because it is easy to use and works well for this job.

From the command line, execute the following command:

tns plugin add nativescript-couchbase

The above command will install our database. As with any database in an Angular 2 application, it is a good idea to wrap all interactions in an Angular 2 provider.

Create the following directories and files in your NativeScript project:

mkdir -p app/providers/database
touch app/providers/database/database.ts

If you don't have the mkdir or touch commands, go ahead and create those files and directories manually.

Before we get into the code, we have to declare that we plan to use TypeScript type definitions for the Couchbase plugin. This can be done by opening the project's references.d.ts file and adding the following line:

/// <reference path="./node_modules/nativescript-couchbase/couchbase.d.ts" />

Now we can proceed to developing the Angular 2 provider for handling the database. Open the project's app/providers/database/database.ts file and include the following TypeScript:

import { Injectable } from "@angular/core";
import { Couchbase } from "nativescript-couchbase";

Injectable()
export class Database {

    private storage: any;
    private isInstantiated: boolean;

    public constructor() {
        if(!this.isInstantiated) {
            this.storage = new Couchbase("stock-ticker");
            this.storage.createView("stocks", "1", (document, emitter) => {
                emitter.emit(document._id, document);
            });
            this.isInstantiated = true;
        }
    }

    public getDatabase() {
        return this.storage;
    }

}

So what is happening in the above Database class?

The goal is to be able to inject this provider in any page of our application, hence the use of the Injectable component. We're also importing the Couchbase plugin that was previously installed.

To be successful, we only want to use one instance of the database, so we're treating it like a singleton. Inside the constructor method we check to see if it has already been instantiated. If it has not been, we open the database and create our MapReduce view that can later be used for querying data.

For more information on using Couchbase in a NativeScript application, check out a previous article I wrote on the subject. You can also check here for more information on Angular 2 providers in NativeScript applications.

Before this provider can be used we need to add it to the applications @NgModule block. Open the project's app/main.ts file and include the following:

import { platformNativeScriptDynamic, NativeScriptModule } from "nativescript-angular/platform";
import { NativeScriptHttpModule } from "nativescript-angular/http";
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";
import { Database } from "./providers/database/database";

@NgModule({
    declarations: [AppComponent],
    bootstrap: [AppComponent],
    imports: [
        NativeScriptModule,
        NativeScriptHttpModule
    ],
    providers: [Database]
})
class AppComponentModule {}

platformNativeScriptDynamic().bootstrapModule(AppComponentModule);

Essentially we've only added an import to the Database provider and added it to the providers array in the @NgModule block.

This brings us into actually building the stock quote application.

Building the Stock Ticker List for Quotes

Again, this is going to be a single page application powered by a list and a prompt. What happens behind the curtains is what is really important in this tutorial.

Before getting ahead of ourselves, we should probably be aware of the API that we'll be using. We'll be using the Markit on Demand API which is free as long as you abide by their terms. It offers various endpoints, but we're going to focus on stock quotes.

Starting with the TypeScript code, open the project's app/app.component.ts file and include the following:

import { Component, OnInit } from "@angular/core";
import { Http } from "@angular/http";
import * as Dialogs from "ui/dialogs";
import * as ApplicationSettings from "application-settings";
import "rxjs/Rx";
import { Database } from "./providers/database/database";

@Component({
    selector: "my-app",
    templateUrl: "app.component.html",
})
export class AppComponent implements OnInit {

    public stocks: Array<any>;

    public constructor(private http: Http, private database: Database) {
        this.stocks = [];
    }

    public ngOnInit() {
        let rows = this.database.getDatabase().executeQuery("stocks");
        for(let i = 0; i < rows.length; i++) {
            this.stocks.push(rows[i]);
        }
    }

    public addTicker() {
        let options = {
            title: "Ticker Symbol",
            okButtonText: "Add",
            cancelButtonText: "Cancel",
            inputType: Dialogs.inputType.text
        };
        Dialogs.prompt(options).then((result: Dialogs.PromptResult) => {
            this.getQuote(result.text);
        });
    }

    public getQuote(ticker: string) {
        this.http.get("http://dev.markitondemand.com/MODApis/Api/v2/Quote/json?symbol=" + ticker)
            .map(result => JSON.parse(result.json()))
            .do(result => console.log("RESULT: ", JSON.stringify(result)))
            .subscribe(result => {
                this.database.getDatabase().createDocument(result, result.Symbol);
                this.stocks.push(result);
            }, error => {
                console.log("ERROR: ", error);
            });
    }

}

So what is happening in the above app/app.component.ts file?

We start by importing a variety of Angular 2 and NativeScript components to be used throughout the particular page of the application.

import { Component, OnInit } from "@angular/core";
import { Http } from "@angular/http";
import * as Dialogs from "ui/dialogs";
import "rxjs/Rx";
import { Database } from "./providers/database/database";

The Http and Rx modules will allow us to use RxJS and Angular 2 to consume remote web services. The Dialogs module will allow us to present a prompt and the Database provider will allow us to use our database.

public constructor(private http: Http, private database: Database) {
    this.stocks = [];
}

In the constructor method we initialize our UI-bound array and inject any components we wish to use, including the Database provider.

It is frowned upon to load data in the constructor method. Instead we should use the ngOnInit method:

public ngOnInit() {
    let rows = this.database.getDatabase().executeQuery("stocks");
    for(let i = 0; i < rows.length; i++) {
        this.stocks.push(rows[i]);
    }
}

Inside the ngOnInit method, we query the MapReduce Couchbase view and add the results into the stocks array.

When it comes to actually using the API, we make use of RxJS and observables.

public getQuote(ticker: string) {
    this.http.get("http://dev.markitondemand.com/MODApis/Api/v2/Quote/json?symbol=" + ticker)
        .map(result => JSON.parse(result.json()))
        .do(result => console.log("RESULT: ", JSON.stringify(result)))
        .subscribe(result => {
            this.database.getDatabase().createDocument(result, result.Symbol);
            this.stocks.push(result);
        }, error => {
            console.log("ERROR: ", error);
        });
}

Since we're working with a stream of data, we can do a transformation using the map to convert the results into a JavaScript object. We can print out this object using the do operator, and finally we can subscribe to the final form of the data in the stream.

The data is saved to our database and the id value of the save will be the symbol of the stock. This will allow us to easily override the data if it already exists. The data is also added to the stock array.

When it comes to adding new data, we simply use a prompt:

public addTicker() {
    let options = {
        title: "Ticker Symbol",
        okButtonText: "Add",
        cancelButtonText: "Cancel",
        inputType: Dialogs.inputType.text
    };
    Dialogs.prompt(options).then((result: Dialogs.PromptResult) => {
        this.getQuote(result.text);
    });
}

When the prompt is dismissed, the getQuote method is called and the data is obtained from the remote service.

With the TypeScript out of the way.], we can focus on the UI. Open the project's app/app.component.html file and include the following HTML markup:

<ActionBar title="Stock Ticker">
    <ActionItem text="Add" ios.position="right" (tap)="addTicker()"></ActionItem>
</ActionBar>
<GridLayout>
    <ListView [items]="stocks" class="list-group">
        <Template let-stock="item">
            <GridLayout rows="auto, auto" columns="*, auto" class="list-group-item">
                <Label class="h2" text="{{ stock.Symbol }}" row="0" col="0"></Label>
                <Label text="{{ stock.Name }}" row="1" col="0"></Label>
                <Label text="{{ stock.LastPrice }}" row="0" col="1"></Label>
            </GridLayout>
        </Template>
    </ListView>
</GridLayout>

In the above UI, we have an action bar with one button that, when pressed, will present the prompt for collecting ticker symbols. The core of our content is in the ListView.

A few things are going on in the ListView portion of our UI. First, we are using a CSS theme provided by NativeScript to make our application look appealing without any effort. Second, we are populating the ListView with the stocks array created in our TypeScript code.

To make the presentation attractive we use a GridLayout where we can define our rows and columns of placement.

Conclusion

You just saw how to use Angular 2's Http component with RxJS to work with remote RESTful APIs in a NativeScript application. In this particular example, we saw how to use Markit on Demand's stock quote API, but it can be extended far being that. It is hard to find modern applications that don't use remote data in some fashion.

As a follow up exercise, see if you can add a pull to refresh to update the stock quotes. Just remember to abide by the terms of the free to use API.

Comments