Create A Mobile App From Your Angular Web App With NativeScript

Angular is all the rage right now. It is a significant step in the right direction from its predecessor AngularJS 1 for numerous reasons, one being its decoupling from the document object model (DOM). This separation allows for applications to be built beyond the web browser. Take for example, the mobile development framework NativeScript, which allows you to develop native Android and iOS applications. There is no DOM in this framework, but yet we can still use Angular to develop our applications.

We’re going to take a look at how to take an Angular web application and bring it to mobile using NativeScript.

The Requirements

There are only a few requirements to make this project successful.

Node.js 4+

NativeScript 2.1+

Angular CLI

The Android SDK, Xcode, or both

Node.js ships with a very important tool called the Node Package Manager (NPM). With it we can get all dependencies for both the Angular web application and NativeScript mobile application. Angular support didn’t become available in NativeScript until version 2.0. Finally, to build for Android or iOS you need the various platform SDKs.

Building the Angular Web Application

To keep things simple we’re going to build an Angular web application from the ground up. This way we can understand each component of the application making it easier to convert. The application will be a simple list based application, something you’ve probably seen many times, but it is still a great learning example.

To create the web application we’re going to make use of the Angular CLI. Assuming it has already been installed, execute the following from a Command Prompt (Windows) or Terminal (Mac and Linux):

ng new WebApp

All the command line work that comes next will be done from within the project directory that was just created. Everything has already been bootstrapped for us in that directory, we just need to add the various components and application logic.

Let’s create two routes which will represent the pages of our application. Since the Angular CLI is still in beta, it cannot yet generate routes so we must do it manually. Using the Terminal or Command Prompt, execute the following:

mkdir -p src/app/list mkdir -p src/app/create touch src/app/list/list.component.ts touch src/app/list/list.component.html touch src/app/list/list.component.css touch src/app/create/create.component.ts touch src/app/create/create.component.html touch src/app/create/create.component.css

The above commands will generate two different pages. If your operating system does not support the above mkdir and touch commands, go ahead and create the files and directories manually.

Since I’m not a design wizard, I’m going to lean on Bootstrap for all stylings in this particular web application. To include Bootstrap, execute the following:

npm install bootstrap --save

Although downloaded, it still needs to be included in the project. Open the project’s angular-cli-build.js file and add the following line to the list of vendorNpmFiles :

'bootstrap/dist/**/*'

Finally, the Bootstrap files can be referenced in the project’s src/index.html file. Open it and include the following at the bottom of the <head> tag:

<link rel="stylesheet" href="vendor/bootstrap/dist/css/bootstrap.min.css" />

Now that the basics are set up we can begin coding the application.

Creating a Page for Showing a List of Data

We’re going to focus on building up our page that will show a list of user entered data. We’re going to spend our time in two particular files, one being a UI file and one being a logic file.

Starting with the logic file, open the project’s src/app/list/list.component.ts file and include the following code:

import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Location } from '@angular/common'; @Component({ moduleId: module.id, selector: 'app-list', templateUrl: 'list.component.html', styleUrls: ['list.component.css'] }) export class ListComponent implements OnInit { public personList: Array<Object>; constructor(private router: Router, private location: Location) { this.personList = localStorage.getItem("people") != null ? JSON.parse(localStorage.getItem("people")) : []; this.location.subscribe((path) => { this.personList = localStorage.getItem("people") != null ? JSON.parse(localStorage.getItem("people")) : []; }); } ngOnInit() {} create() { this.router.navigate(["/create"]); } }

We should probably break down the above code to see what is happening.

The first thing that we’re doing is we’re importing the various dependency components for use. For example, we include the Router so that we can eventually navigate to the next page. We include Location so that we can listen for pop events in the navigation stack, also known as going backwards.

The Angular CLI took care of bootstrapping the @Component for us which essentially tells us which HTML file to pair with this logic file. We’ll design the HTML file in a bit.

By making the personList public we are allowing the list to be bound to our HTML UI. This means it will be able to be rendered in the front-end.

constructor(private router: Router, private location: Location) { this.personList = localStorage.getItem("people") != null ? JSON.parse(localStorage.getItem("people")) : []; this.location.subscribe((path) => { this.personList = localStorage.getItem("people") != null ? JSON.parse(localStorage.getItem("people")) : []; }); }

The above constructor will initialize our list. For the web application we will make use of HTML5 local storage for storing our data. Things will be slightly different for NativeScript, but not by much. Essentially we’re saying, parse the serialized local storage data if it exists or use an empty array if it does not exist. By using the subscribe method on the location object we are listening for the pop event in the navigation stack. When this event happens we can reload from local storage.

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

Finally we have our create method that will navigate to the other page or route.

Before we get into coding the next page, let’s look at the HTML file that is paired with the TypeScript code that we just wrote. Open the project’s src/app/list/list.component.html file and include the following markup:

<div class="container"> <div class="panel panel-default"> <div class="panel-heading"> <strong>Web Application - List</strong> <div class="pull-right"> <a (click)="create()"><span class="glyphicon glyphicon-plus"></span></a> </div> </div> <ul class="list-group"> <li class="list-group-item" *ngFor="let person of personList">{{ person.firstname }} {{ person.lastname }}</li> </ul> </div> </div>

Most of the code above is from the Bootstrap documentation. Take note in particular the (click) event on the glyph icon in the panel heading. This will call the create function which will navigate us to the next page. Also note the following:

<ul class="list-group"> <li class="list-group-item" *ngFor="let person of personList">{{ person.firstname }} {{ person.lastname }}</li> </ul>

We are looping through the personList array and displaying the firstname and lastname properties.

Creating a Form for Saving Data

With the list page out of the way, let’s focus on the page for creating our data.

Open the project’s src/app/create/create.component.ts file and include the following code:

import { Component, OnInit } from '@angular/core'; import { Location } from '@angular/common'; @Component({ moduleId: module.id, selector: 'app-create', templateUrl: 'create.component.html', styleUrls: ['create.component.css'] }) export class CreateComponent implements OnInit { public personList: Array<Object>; public firstname: string; public lastname: string; constructor(private location: Location) { this.personList = localStorage.getItem("people") != null ? JSON.parse(localStorage.getItem("people")) : []; this.firstname = ""; this.lastname = ""; } ngOnInit() {} save() { if(this.firstname != "" && this.lastname != "") { this.personList.push({firstname: this.firstname, lastname: this.lastname}); localStorage.setItem("people", JSON.stringify(this.personList)); this.location.back(); } } }

Let’s break down what is happening in the above code.

Just like with the list page, we are importing some Angular components and defining the HTML page that will be paired with our TypeScript code. We are defining three public variables that will be bound to the UI for this particular page.

constructor(private location: Location) { this.personList = localStorage.getItem("people") != null ? JSON.parse(localStorage.getItem("people")) : []; this.firstname = ""; this.lastname = ""; }

In the constructor method we initialize our variables and pull any existing person data from local storage.

save() { if(this.firstname != "" && this.lastname != "") { this.personList.push({firstname: this.firstname, lastname: this.lastname}); localStorage.setItem("people", JSON.stringify(this.personList)); this.location.back(); } }

In the save method we check to make sure that the firstname and lastname variables are not empty. If this condition is met, we can push a new object with them into the personList array and then re-save a serialized version of that array. When done, we pop back in the navigation stack.

Now let’s have a look at the HTML file that goes with this TypeScript code. Open the project’s src/app/create/create.component.html file and include the following markup:

<div class="container"> <div class="panel panel-default"> <div class="panel-heading"> <strong>Web Application - Create</strong> <div class="pull-right"> <a (click)="save()"><span class="glyphicon glyphicon-floppy-disk"></span></a> </div> </div> <div class="panel-body"> <form> <div class="form-group"> <label for="firstname">First Name</label> <input type="text" class="form-control" [(ngModel)]="firstname" id="firstname" placeholder="First Name"> </div> <div class="form-group"> <label for="lastname">Last Name</label> <input type="text" class="form-control" [(ngModel)]="lastname" id="lastname" placeholder="Last Name"> </div> </form> </div> </div> </div>

Like with the list page we have a (click) event that calls a function. This time it calls the save function for saving our data and navigating back in the stack. We also have the following in this particular HTML file:

<form> <div class="form-group"> <label for="firstname">First Name</label> <input type="text" class="form-control" [(ngModel)]="firstname" id="firstname" placeholder="First Name"> </div> <div class="form-group"> <label for="lastname">Last Name</label> <input type="text" class="form-control" [(ngModel)]="lastname" id="lastname" placeholder="Last Name"> </div> </form>

Above we have a form and each field has an [(ngModel)] tag. This is how we bind the firstname and lastname variables from our TypeScript file to the HTML file.

Bringing the Routes Together and Setting a Default Page Route

We have our two pages, but they are not yet connected to each other in the application. We also don’t have any concept of a default page yet.

Open the project’s src/app/app.component.ts file and include the following code:

import { Component } from '@angular/core'; import { ROUTER_DIRECTIVES } from "@angular/router"; @Component({ moduleId: module.id, selector: "app-root", directives: [ROUTER_DIRECTIVES], template: "<router-outlet></router-outlet>" }) export class AppComponent { }

By including the code above we are saying that we want this TypeScript file to act as our main routing outlet. All child page routes and components will flow through this file because of the <router-outlet> tag.

However, we don’t have the routes configured.

Open the project’s src/main.ts file and include the following code:

import { bootstrap } from '@angular/platform-browser-dynamic'; import { enableProdMode } from '@angular/core'; import { provideRouter, RouterConfig } from '@angular/router'; import { AppComponent, environment } from './app/'; import { ListComponent } from './app/list/list.component'; import { CreateComponent } from './app/create/create.component'; if (environment.production) { enableProdMode(); } export const AppRoutes: RouterConfig = [ { path: "", component: ListComponent }, { path: "create", component: CreateComponent } ] bootstrap(AppComponent, [provideRouter(AppRoutes)]);

In the above code we are importing every component we wish to link. When defining the routes, the route with the blank path is the default route. Finally we inject these routes when bootstrapping.

The Angular web application should be complete at this point. To see it in action, execute the following from your Command Prompt or Terminal:

ng serve

If you’re interested in seeing the full source code to the web application, you can download it here. Note there are a lot of files and directories that have been generated from the Angular CLI. Feel free to examine them, but they won’t apply for the NativeScript portion of this guide.

Converting to a NativeScript Mobile Application

Now we’re going to take a look at building the NativeScript equivalent of this web application. A few major differences to note between the two. First, we won’t be using the Angular CLI, but instead the NativeScript CLI. Since NativeScript develops native applications there is no concept of local storage. Instead we’ll be using the very similar application-settings storage. It can easily be switched out to use SQLite or Couchbase NoSQL.

With that said, let’s start by creating a new NativeScript project. From the Terminal (Mac and Linux) or Command Prompt (Windows), execute the following:

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

There are a few things to note about the above commands. By using the –ng tag we are using the Angular NativeScript template. It operates on TypeScript which is what we used in the web application. Also note that if you’re not using a Mac, you cannot build for the iOS platform.

We need to add the component files to our project. The NativeScript CLI won’t do this for us, but it isn’t too troublesome to do this manually. Create the directories and files below:

mkdir app/components mkdir app/components/list mkdir app/components/create touch app/components/list/list.component.ts touch app/components/list/list.component.html touch app/components/create/create.component.ts touch app/components/create/create.component.html

If you’re using Windows, you won’t have the mkdir and touch commands so you’ll have to create these manually.

Now we can start developing the application!

Creating a List Component for Displaying a List of Data

Instead of picking and pulling from the web application, we’re going to add our code and markup from scratch. However, you’ll notice much of what we do is near identical to the web application.

We’re going to start with the TypeScript file of the list component. Open the project’s app/components/list/list.component.ts file and include the following code:

import {Component} from "@angular/core"; import {Router} from "@angular/router"; import {Location} from "@angular/common"; import * as ApplicationSettings from "application-settings"; @Component({ selector: "list", templateUrl: "./components/list/list.component.html", }) export class ListComponent { public personList: Array<Object>; constructor(private router: Router, private location: Location) { this.router = router; this.personList = JSON.parse(ApplicationSettings.getString("people", "[]")); this.location.subscribe((path) => { this.personList = JSON.parse(ApplicationSettings.getString("people", "[]")); }); } create() { this.router.navigate(["/create"]); } }

Between the web version and the NativeScript version of this file, things are very similar. This NativeScript version has a lot of things stripped out and the local storage has been replaced with application-settings , but beyond that I used the same code.

The core difference is in the UI that is bound to this TypeScript file. Open the project’s app/components/list/list.html file and include the following markup:

<ActionBar title="NativeScript - List"> <ActionItem text="Add" (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>

NativeScript does not render applications in a web view like other mobile frameworks so it doesn’t understand HTML markup. Instead we must use XML markup that NativeScript understands so it can be converted to native code at compile time.

In the above XML we are creating an action bar with a button for navigating us to the next component when we (tap) it. Notice instead of (click) we are using (tap) which isn’t really a big change.

The larger change will be in how we loop through the array items. For the NativeScript ListView we don’t make use of *ngFor but something else instead. Again, in the end, it isn’t so far off from the web example.

Creating a Component for Saving User Defined Data

Let’s move onto the second page or component of our mobile application. Again, you’ll notice all the similarities between the Angular web application and the Angular NativeScript application.

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 * as ApplicationSettings from "application-settings"; @Component({ selector: "create", templateUrl: "./components/create/create.component.html", }) export class CreateComponent { private personList: Array<Object>; public firstname: string; public lastname: string; constructor(private location: Location) { this.location = location; this.personList = JSON.parse(ApplicationSettings.getString("people", "[]")); this.firstname = ""; this.lastname = ""; } save() { if(this.firstname != "" && this.lastname != "") { this.personList.push({firstname: this.firstname, lastname: this.lastname}); ApplicationSettings.setString("people", JSON.stringify(this.personList)); this.location.back(); } } }

Like with the list component we are importing the necessary Angular and NativeScript components. We are also defining what XML file to be paired with this particular TypeScript file.

The CreateComponent itself is pretty much identical to that of the web application version. The only exception being its use of application-settings rather than local storage.

Now let’s look at the XML layout to go with this TypeScript file. Open the project’s app/components/create/create.html file and include the following markup:

<ActionBar title="NativeScript - 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>

It is very different than the HTML web version, again because NativeScript doesn’t understand common HTML.

Here we have an action bar with two buttons. iOS doesn’t have a back button so we need to define it. We also create a button for triggering the save function when it is tapped.

When it comes to our form, we have two TextField items, both bound to our TypeScript file with the [(ngModel)] tag.

Configuring the Application Routes

We have our two components created, but they are not integrated with our application yet. We need to set up the routing in a similar fashion to what was done in the web application.

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"; @Component({ selector: "my-app", directives: [NS_ROUTER_DIRECTIVES], template: "<page-router-outlet></page-router-outlet>" }) export class AppComponent { }

A few things to note in this file.

We’re adding the routing outlet and the NativeScript router directives. This file will act as a driver to each page of our application.

Next we need to define the actual application routes. Instead of doing this in the app/app.component.ts file we are going to manipulate the app/main.ts file. Open it and include the following code:

import {nativeScriptBootstrap} from "nativescript-angular/application"; import {nsProvideRouter} from "nativescript-angular/router"; import {RouterConfig} from "@angular/router"; import {AppComponent} from "./app.component"; 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, [[nsProvideRouter(AppRoutes, {})]]);

In this file we are keeping the foundation bootstrap logic that already existed, but we are also including an array of application routes to be injected into the bootstrap. The routes are injected with the nsProvideRouter component.

At this point our application is complete and can be launched. If you’d like to see the full source code to this project, it can be downloaded here.

Conclusion

We just saw how to convert an Angular web application to a NativeScript Android and iOS mobile application. There are a few key points I’d like to make about what we just accomplished. In the web application we used Bootstrap for our styling because I’m not much of a designer. The Bootstrap UIs were created with HTML, something a web browser can understand. Since NativeScript uses native code and doesn’t understand HTML, we have to use XML components. This is the core difference between web and mobile. The TypeScript code was near identical between the web and NativeScript versions.

Building a web application from Angular makes things very easy for us when we try to convert the code to mobile. Since Angular doesn’t rely on the DOM, native frameworks like NativeScript can really shine.

Nic Raboy Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.