Scanning Barcodes with NativeScript and Angular 2

Not too long ago I wrote about scanning for barcodes within a NativeScript application. However, this was a very basic example and was created before Angular 2 was supported.

After having had received a lot of positive feedback on the article, I thought it would be best to take it to the next level. What if you were building a product scanning application where, if you scanned a barcode, you could see information about the particular product?

NativeScript Barcode Scanner Example

In this article, we're going to see how to store fictional product data in a NoSQL database and look it up by scanning a barcode.

You can download the source code for the project as a zip.

The Requirements

There are a few requirements to make this application successful.

  • NativeScript 2+
  • An iOS or Android Device with Camera
  • The Android SDK for Android or Xcode for iOS

Because this project uses Angular 2, NativeScript 2 is the minimum compatible version. The application will make use of the platform camera which tends to be incompatible with simulators or emulators. A device will be required instead.

To build for iOS you'll need Xcode installed and to build for Android you'll need the Android SDK installed.

Creating a New NativeScript Project

To make this guide easy to understand, we're going to create a fresh project to work with. From the Command Prompt (Windows) or Terminal (Mac and Linux), execute the following:

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

The above commands will create a new Angular 2 project that uses TypeScript as well as add the iOS and Android build platforms. Remember, you need the Android SDK to build for Android and you need Xcode to build for iOS.

Installing the Project Dependencies

This particular project will use two different NativeScript plugins.

For data storage we'll be using Couchbase, an open source NoSQL database. When it comes to JavaScript applications, NoSQL makes a lot of sense because of how data is stored and accessed. To install Couchbase, execute the following commands from your Terminal or Command Prompt:

tns plugin add nativescript-couchbase

More information on what the Couchbase plugin can accomplish can be found on GitHub.

When it comes to scanning barcodes, we'll be using a plugin by Eddy Verbruggen. It will allow us to scan for several different types of barcodes. To install this plugin, execute the following:

tns plugin add nativescript-barcodescanner

More information on what the barcode scanning plugin can accomplish can be found on the GitHub.

Because we're developing with TypeScript rather than vanilla JavaScript, we need to reference the type definitions for each of these plugins.

Open the project's references.d.ts file and include the following two lines:

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

With those lines added, the true power of TypeScript can be leveraged.

Designing the Database Provider

When working with a data layer in an Angular 2 application it is best to create a provider for it. In this scenario, the provider will be like a singleton instance of the database.

Create an app/providers/couchbase directory in your project and add a couchbase.ts file to it. This file should contain the following code:

import {Couchbase} from 'nativescript-couchbase';

export class CouchbaseProvider {

    private database: any;
    private isInstantiated: boolean;

    public constructor() {
        if(!this.isInstantiated) {
            this.database = new Couchbase("product-database");
            this.isInstantiated = true;
        }
    }

    public getDatabase() {
        return this.database;
    }

}

So what is happening in the above provider?

First, of course, we're importing the Couchbase plugin that was installed. Within the CouchbaseProvider class there are two private variables. The database variable will represent the open database instance to be used throughout the application. The isInstantiated variable will indicate whether or not the database is already open.

In the constructor method, if the database is not already instantiated, create and open a database called "product-database". This database can be further accessed by calling the getDatabase method.

More information on how Couchbase works with NativeScript can be found in a previous article I wrote.

Creating a Component for Scanning and Viewing Data

The first screen we want to worry about is the screen for presenting information about the scanned barcode. We will also be able to initialize the camera from this screen.

Create the following files and directories to represent the information component:

mkdir -p app/components/information
touch app/components/information.component.ts
touch app/components/information.component.html

If you're using an operating system that doesn't support the mkdir or touch commands, go ahead and create those files and directories manually.

Starting with the TypeScript logic file, open your project's app/components/information.component.ts file and add the following code:

import {Component} from "@angular/core";
import {Router} from "@angular/router";
import * as BarcodeScanner from "nativescript-barcodescanner";
import {CouchbaseProvider} from "../../providers/couchbase/couchbase";

@Component({
    selector: "information",
    templateUrl: "./components/information/information.component.html",
})
export class InformationComponent {

    private database: any;
    public product: any;

    public constructor(private router: Router, private couchbase: CouchbaseProvider) {
        this.database = this.couchbase.getDatabase();
        this.product = {
            name: "",
            price: "",
            inventory: "",
            sku: ""
        }
    }

    public scan() {
        BarcodeScanner.scan({
            cancelLabel: "Stop scanning",
            message: "Go scan something",
            preferFrontCamera: false,
            showFlipCameraButton: true
        }).then((result) => {
            this.product = this.database.getDocument(result.text);
        });
    }

    public create() {
        this.router.navigate(["/create-product"]);
    }

}

Let's break down the above TypeScript file.

First, we're importing the necessary Angular 2 dependencies as well as the barcode plugin and the database provider that we previously created.

In the @Component section we declare the HTML UI that will be paired with the particular logic file.

What's really important here is what's in the InformationComponent class. The database variable will hold our open database instance and the product variable will hold all information loaded from the database after a barcode has been scanned.

In the constructor method, the database instance is obtained and the product variable is initialized. The scan method does the most for our application. Calling it will launch the barcode scanner and after one of the compatible barcodes has been scanned, the barcode value will be used as a lookup key for matching data in the database. The data returned will be a product object.

Finally there is a create method that will navigate the user to the next component.

Now we're going to take a look at the UI code. Open the project's app/components/information/information.component.html and add the following markup:

<ActionBar title="Product Details">
    <ActionItem text="Add" (tap)="create()" ios.position="right"></ActionItem>
</ActionBar>
<StackLayout>
    <Label text="Product" class="fieldTitle"></Label>
    <Label [text]="product.name"></Label>
    <Label text="Price" class="fieldTitle"></Label>
    <Label [text]="product.price"></Label>
    <Label text="Inventory" class="fieldTitle"></Label>
    <Label [text]="product.inventory"></Label>
    <Label text="Sku" class="fieldTitle"></Label>
    <Label [text]="product.sku"></Label>
    <Button text="Scan UPC" (tap)="scan()"></Button>
</StackLayout>

The UI for this particular screen has a navigation bar and a series of text elements. These text elements are populated from the public variable in the TypeScript file. The button in the navigation bar will call the create method and the button within the content will call the save method.

Notice the classes on a few of the <Label> elements. These stylings can be found in the project's app/app.css file:

.fieldTitle {
    font-weight: bold;
    margin-top: 10;
}

That is all there is to the first page of this application. Now that we have scanning and lookup, it makes sense to offer an outlet for adding new product data to the database.

Creating a Component for Adding New Lookup Data

The second screen we create will allow us to add new product data to the database. The most important part of the data we add will be the product sku, since it will be the same code used in the barcodes. Before getting too far ahead of ourselves, let's create the necessary files and directories for this component.

mkdir -p app/components/create-product
touch app/components/create-product/create-product.component.ts
touch app/components/create-product/create-product.component.html

Again, if you don't have the mkdir and touch commands in your command line, create the above files and directories manually.

Like with the previous component we're going to start by developing the TypeScript logic. Open the project's app/components/create-product/create-product.component.ts file and include the following code:

import {Component} from "@angular/core";
import {Location} from "@angular/common";
import {CouchbaseProvider} from "../../providers/couchbase/couchbase";

@Component({
    selector: "create-product",
    templateUrl: "./components/create-product/create-product.component.html",
})
export class CreateProductComponent {

    private database: any;
    public product: any;

    public constructor(private location: Location, private couchbase: CouchbaseProvider) {
        this.database = this.couchbase.getDatabase();
        this.product = {
            name: "",
            price: "",
            inventory: "",
            sku: ""
        }
    }

    public save() {
        if(this.product.name && this.product.price && this.product.inventory && this.product.sku) {
            this.database.createDocument(this.product, this.product.sku);
            this.location.back();
        }
    }

}

Just like with the previous component, we are importing the necessary Angular 2 components as well as our Couchbase database component. What is most important is in the CreateProductComponent class.

The database variable will hold our open Couchbase instance. The product variable will hold properties that are bound to text inputs in the UI. We initialize both in the constructor method.

When it comes to saving with the save method, the first thing that we do is check to make sure each of the properties is not undefined. If true, create a Couchbase NoSQL document with the JavaScript object and set the key of the document to that of the product sku. When finished, we will go backwards in the navigation stack.

How about the UI that goes with this particular TypeScript file?

Open the project's app/components/create-product.component.html file and include the following markup:

<ActionBar title="Create Product">
    <ActionItem text="Save" (tap)="save()" ios.position="right"></ActionItem>
</ActionBar>
<StackLayout>
    <Label text="Product" class="fieldTitle"></Label>
    <TextField [(ngModel)]="product.name"></TextField>
    <Label text="Price" class="fieldTitle"></Label>
    <TextField [(ngModel)]="product.price"></TextField>
    <Label text="Inventory" class="fieldTitle"></Label>
    <TextField [(ngModel)]="product.inventory"></TextField>
    <Label text="Sku" class="fieldTitle"></Label>
    <TextField [(ngModel)]="product.sku"></TextField>
</StackLayout>

As you probably noticed, the UI for this component is very similar to the previous. However, in this component, we have inputs instead of static text properties. Each of the inputs is bound to the public TypeScript variable via the [(ngModel)] tags.

With the two components created, we now need to bring them together within the application.

Bringing the Angular 2 Providers and Components Together

We have a few components, but they are not connected in any fashion to the application. Now we need to bring them together. This involves creating routes for the Angular 2 Router. However, before we do that there is something you should note about the barcode scanner plugin.

In the latest versions of iOS and Android, certain permissions are required to use the device camera. You cannot use this feature without asking for permission. We're going to do that in the project's app/app.component.ts file:

import {Component, OnInit} from "@angular/core";
import {NS_ROUTER_DIRECTIVES} from "nativescript-angular/router";
import * as BarcodeScanner from "nativescript-barcodescanner";

@Component({
    selector: "my-app",
    template: "<page-router-outlet></page-router-outlet>",
    directives: [NS_ROUTER_DIRECTIVES]
})
export class AppComponent implements OnInit {

    public constructor() {}

    public ngOnInit() {
        BarcodeScanner.available().then((available) => {
            if(available) {
                BarcodeScanner.hasCameraPermission().then((granted) => {
                    if(!granted) {
                        BarcodeScanner.requestCameraPermission();
                    }
                });
            }
        });
    }

}

Notice the use of the ngOnInit method. It is frowned upon to do any heavy lifting in the Angular 2 constructor method. Instead you should do that in the ngOnInit. This method is called before any other method in our other components. This means we are asking for permission when the application launches the first time.

Now let's take a look at creating those routes.

Open the project's app/main.ts file and add the following code:

import {nativeScriptBootstrap} from "nativescript-angular/application";
import {nsProvideRouter} from "nativescript-angular/router";
import {RouterConfig} from "@angular/router";
import {CouchbaseProvider} from "./providers/couchbase/couchbase";
import {AppComponent} from "./app.component";

import {InformationComponent} from "./components/information/information.component";
import {CreateProductComponent} from "./components/create-product/create-product.component";

export const AppRoutes: RouterConfig = [
    { path: "", component: InformationComponent },
    { path: "create-product", component: CreateProductComponent }
]

nativeScriptBootstrap(AppComponent, [CouchbaseProvider, nsProvideRouter(AppRoutes, {})]);

Notice that both components are being imported here. After importing each of the components, we add them to the AppRoutes array. Any route with an empty path will represent the default page of the application. In this file, we are also importing the Couchbase provider. We are doing this because we want to inject the instance throughout the application in the nativeScriptBootstrap method. We are injecting it along with the AppRoutes array.

At this point the application can be tested on a device. Notice I didn't say simulator? The camera needs to work, and, in simulators, that is not always the case.

Conclusion

You just saw how to create a NativeScript Angular 2 application that stores product data in a local NoSQL database. This product data can be retrieved by scanning a barcode for its sku. If you're looking for a similar, non-Angular, version of the barcode scanner guide, check out the previous article I wrote on the subject.

Header image courtesy of Christiaan Colen

Comments

  • leocrawf stewart

    I downloaded and tried building the project but got this verbose error message but i will limit it to this: failed to find module :”timers”, relative to :”app/tns_modules/”. Did this run on your tests?

    • It is possible there is an incompatibility between the latest NativeScript CLI and the package versions that were used in the ZIP project. You might try creating a fresh project and copying the TypeScript and XML files into the new project, or changing the package versions in the package.json file.

      • leocrawf stewart

        I was able to create a new project, create a app.module.ts file and make appropriate changes and get it work.

  • Simon Walker

    I also downloaded this project and tried to build and get the same errors: failed to find module :”timers”, relative to :”app/tns_modules/” on an android build. Failed completely on iOS

    • The following will fix the problem:

      “`
      “reflect-metadata”: “0.1.8”,
      “zone.js”: “0.6.17”
      “`

      Notice I’ve added zone.js and increased the version of reflect-metadata. You should note that many of the packages used in this example are out of date. While the general concepts remain accurate, some of the Angular 2 stuff has changed.

      • leocrawf stewart

        I tried this and got it to work. Its not great however to be using rc versions when more stable version are available. Is there any upgrade method that can be used to simplify the process, something like tns upgrade?

        • Unfortunately the problem resides in Angular 2, not really NativeScript. The beta and release candidates are very different than the stable release making it rather impossible to upgrade via a script.

          The logic in this tutorial still applies, the Angular 2 specifics are different.

          • leocrawf stewart

            Is it possible you could update the tutorial using angular final?

          • Jitendra Kumar Behera

            its getting error when build

          • Jitendra Kumar Behera

            plz send final code jitu.kdp27@gmail.com

  • Ghândrį Şêmah XaVį

    couchbase.d.ts’ is not a module !! what does mean !

    • Going to need a little more information than this. Where the the error happen? Compile time or runtime? Etc?