Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Tour Of Heroes As A NativeScript With Angular Android And iOS Application

TwitterFacebookRedditLinkedInHacker News

When it comes to learning Angular, the go-to example is the Tour of Heroes tutorial that is found in the official Angular documentation. This is a great tutorial because it covers a lot of topics, however, it is a web application tutorial. What if we wanted to build a mobile application from it, or more specifically, a native mobile application with Android and iOS support?

We’re going to see how to build a Tour of Heroes inspired iOS and Android mobile application using Angular and NativeScript.

Let’s figure out what we’re getting ourselves into here. It is important to note that this is a Tour of Heroes inspired guide, as in it won’t be exactly the same. I wanted to keep some originality here.

NativeScript Tour of Heroes

We won’t be making any HTTP requests against a RESTful API, but we’ll be simulating them with mock data. The point we’re going to prove here is that native mobile applications can be created with Angular and NativeScript using nearly the same code and logic.

The Requirements

Since we are developing a mobile application and no longer just a browser based web application, there are a few requirements that must be met to be successful with this project.

  • NativeScript 2.5+
  • Android SDK for Android
  • Xcode for iOS

The Android SDK is required if you wish to build for the Android platform. It is compatible on Linux, Mac, and Windows. Xcode is required if you wish to build for the iOS platform. Per restrictions set by Apple, iOS development can only happen on a Mac. For more information on configuring NativeScript and each of the build tools, visit the official NativeScript documentation.

Creating a New NativeScript Project with Angular Support

We’ll be creating the Tour of Heroes application based on a new project. At this point all the development requirements must be met.

From the NativeScript CLI, execute the following:

tns create tour-of-heroes --ng

In the above command take note of the --ng tag. NativeScript allows for Angular development or vanilla development, so the --ng tag indicates we want an Angular project.

After creating the project we’ll be left with a file and directory structure that is a little different than that of one that might have been created with the Angular CLI. All our development will happen in the app directory of the project, which should look familiar if you’re coming from web development.

Creating the Data Services with Angular

The official Tour of Heroes application does HTTP requests against a RESTful API to consume data. Since API development is easily a topic of its own, we’re going to use mock data instead. This mock data will reside with a series of functions inside an Angular service.

Create an app/services/data.service.ts file within your project with the following TypeScript code:

import { Injectable } from "@angular/core";

@Injectable()
export class DataService {

    private heroes: Array<any>;

    public constructor() {
        this.heroes = [
            {
                "id": 1,
                "name": "Captain America"
            },
            {
                "id": 2,
                "name": "Iron Man"
            },
            {
                "id": 3,
                "name": "Hulk"
            },
            {
                "id": 4,
                "name": "Black Widow"
            },
            {
                "id": 5,
                "name": "Thor"
            }
        ];
    }

    public getHeroes(): Array<any> {
        return this.heroes;
    }

    public getHero(id: number): any {
        for(let i = 0; i < this.heroes.length; i++) {
            if(this.heroes[i].id == id) {
                return this.heroes[i];
            }
        }
        return -1;
    }

    public delete(id: number) {
        for(let i = 0; i < this.heroes.length; i++) {
            if(this.heroes[i].id == id) {
                this.heroes.splice(i, 1);
                break;
            }
        }
    }

    public add(value: string) {
        this.heroes.push(
            {
                "id": Math.floor(Math.random() * (100 - 1)) + 1,
                "name": value
            }
        );
    }

    public edit(id: number, name: string) {
        for(let i = 0; i < this.heroes.length; i++) {
            if(this.heroes[i].id == id) {
                this.heroes[i].name = name;
                break;
            }
        }
    }

}

So what are we doing in the above code and why do we need it?

First of all, by creating an Angular service we are creating a singleton that can be shared between every component of the application.

Within the constructor method we are defining some sample data. Every method in the service will manipulate the mock data that we define in the constructor method.

The hero data will contain an id and a name. When creating a new hero, the id will be a random number. When using an API, all the methods would hit the API and the API would worry about the generation or querying.

Before the service can be used it must be injected into the @NgModule block of our application. This block can be found in the app/app.module.ts file. Open it and make it look something like the following:

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 { DataService } from "./services/data.service";

@NgModule({
    bootstrap: [
        AppComponent
    ],
    imports: [
        NativeScriptModule,
        NativeScriptFormsModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent
    ],
    providers: [DataService],
    schemas: [
        NO_ERRORS_SCHEMA
    ]
})
export class AppModule { }

We’ve imported the DataService class and added it to the providers array of the @NgModule block. This won’t be the last time we make changes to the app/app.module.ts file.

For more information on services in a NativeScript application, check out a previous tutorial I wrote called, Working with Shared Providers in a NativeScript Angular Application.

Configuring the Application Router and Defining the Possible Pages

This application will have three different pages. We’ll have a dashboard that will give us a quick look of our heroes, a page for listing and adding new heroes, and a page for editing an existing hero.

Go ahead and create the following files and directories within your project:

mkdir -p app/components/dashboard
mkdir -p app/components/heroes
mkdir -p app/components/hero
touch app/components/dashboard/dashboard.component.ts
touch app/components/dashboard/dashboard.component.html
touch app/components/heroes/heroes.component.ts
touch app/components/heroes/heroes.component.html
touch app/components/heroes/hero.component.ts
touch app/components/heroes/hero.component.html

If your operating system doesn’t allow the mkdir and touch commands, go ahead and create those files and directories manually.

While we’ve just created our pages, these are actually going to be child pages. This is because we want to use a segmented bar to be able to switch between them. The segmented bar will exist as a parent page that controls navigation to each of the child pages. For this reason, create the following:

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

At this point we can start designing each of the created pages and adding page logic.

Defining Navigation Routes for the NativeScript with Angular Application

We haven’t added anything to our components yet, but we can design our routes in anticipation. Route definitions are added to the project’s app/app.routing.ts file. Open this file and make it look like the following:

import { NgModule } from "@angular/core";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { Routes } from "@angular/router";

import { ParentComponent } from "./components/parent/parent.component";
import { DashboardComponent } from "./components/dashboard/dashboard.component";
import { HeroesComponent } from "./components/heroes/heroes.component";
import { HeroComponent } from "./components/hero/hero.component";

const routes: Routes = [
    { path: "", component: ParentComponent, children: [
        { path: "", component: DashboardComponent },
        { path: "heroes", component: HeroesComponent },
        { path: "hero/:id", component: HeroComponent },
    ]}
];

@NgModule({
    imports: [NativeScriptRouterModule.forRoot(routes)],
    exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }

Again, we haven’t created our classes yet, but this is in anticipation. What is interesting to us is the routes array.

Notice that we have one top level route with an empty path. Any route with an empty path means that it is a default route. When the application loads, it will default to loading the ParentComponent class. This route has three children, one of which is the default. One of the child routes accepts an id parameter that we can pass in.

While not necessarily related to routing, we have to add all our components to the @NgModule block. Open the project’s app/app.module.ts file and make it look like the following:

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 { ParentComponent } from "./components/parent/parent.component";
import { DashboardComponent } from "./components/dashboard/dashboard.component";
import { HeroesComponent } from "./components/heroes/heroes.component";
import { HeroComponent } from "./components/hero/hero.component";

import { DataService } from "./services/data.service";

@NgModule({
    bootstrap: [
        AppComponent
    ],
    imports: [
        NativeScriptModule,
        NativeScriptFormsModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        ParentComponent,
        DashboardComponent,
        HeroesComponent,
        HeroComponent
    ],
    providers: [DataService],
    schemas: [
        NO_ERRORS_SCHEMA
    ]
})
export class AppModule { }

In the above we’ve imported each of the anticipated components and added them to the declarations array of the @NgModule block. Every component, directive, and pipe that you wish to use within your application must be added to the declarations array. These are called declarable classes. This will be the last time we visit the app/app.module.ts file.

Designing and Developing the Application Pages

With the routes defined and the component files in place, we can start designing and developing each of our application pages. Like with the official Tour of Heroes demo application, nothing in our NativeScript application will be too complex. The core differences will reside in the UI as HTML versus XML differences.

To start things off, we should create our parent navigation page that will manage each of our three child pages.

Creating the Parent Page to Our Children

The parent page is responsible for displaying a NativeScript segmented bar and routing each child page through it. While we could make all components a top level component, it makes sense to have parents and children in this example.

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

import { Component } from "@angular/core";
import { SegmentedBarItem } from "ui/segmented-bar";
import { Router } from "@angular/router";

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

    public navItems: Array<SegmentedBarItem>;

    public constructor(private router: Router) {
        this.navItems = [];
        this.navItems.push(this.createSegmentedBarItem("Dashboard"));
        this.navItems.push(this.createSegmentedBarItem("Heroes"));
    }

    private createSegmentedBarItem(title: string): SegmentedBarItem {
        let item: SegmentedBarItem = <SegmentedBarItem> new SegmentedBarItem();
        item.title = title;
        return item;
    }

    public navigate(index: number) {
        switch(index) {
            case 0:
                this.router.navigate(["/"]);
                break;
            case 1:
                this.router.navigate(["/heroes"]);
                break;
        }
    }

}

In the above ParentComponent class we are defining our segmented bar tabs and what happens when each of them are clicked. For this example we only have two tabs, one for showing the dashboard and the other, the list of heroes.

Remember the paths we chose in the app/app.routing.ts file? We are using those when trying to navigate on segmented bar item click.

So what does the UI behind the parent component look like? Open the project’s app/components/parent/parent.component.html file and include the following HTML markup:

<ActionBar title="Tour of Heroes"></ActionBar>
<StackLayout>
    <SegmentedBar #sb [items]="navItems" selectedIndex="0" (selectedIndexChange)="navigate(sb.selectedIndex)"></SegmentedBar>
    <router-outlet></router-outlet>
</StackLayout>

The top level HTML will have an action bar followed by a segmented bar. The segmented bar will use an #sb template variable which will allow us to pass the index during a change event. Each of the items created in the TypeScript logic are added to the HTML via the [items] attribute.

Because this HTML acts as a wrapper, we have a <router-outlet> in which every child can pass through.

Creating the Hero Dashboard

The first child component we see when we launch the application is the dashboard. In our example it will just display all our heroes, but it will display them differently than the alternative component.

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

import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { DataService } from "../../services/data.service";

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

    public heroes: Array<any>;

    public constructor(private router: Router, private data: DataService) {
        this.heroes = this.data.getHeroes();
    }

    public edit(id: number) {
        this.router.navigate(["/hero", id]);
    }

}

We did import the DataService class in the project’s app/app.module.ts file, but it also needs to be imported in every component we wish to use it.

After injecting the service in the constructor method we can get all the heroes that are available. If we wish to edit any of the heroes, we can navigate to the detail page by passing the id value

So what does the HTML behind this TypeScript look like? Open the project’s app/components/dashboard/dashboard.component.html file and include the following HTML markup:

<StackLayout>
    <Label text="Top Heroes" class="h2"></Label>
    <FlexboxLayout flexWrap="wrap" flexDirection="row">
        <Label *ngFor="let hero of heroes" text="{{ hero.name }}" (tap)="edit(hero.id)" flexGrow="1" class="hero-flexgrid-item"></Label>
    </FlexboxLayout>
</StackLayout>

To create our responsive grid we are using a Flexbox inspired FlexboxLayout. If the columns don’t fit on a row they will wrap to the next row. The columns are generated by looping through the heroes that were found in the TypeScript logic. If we click on any of the columns, we will navigate to the page for editing data.

Creating the List of Heroes

Now let’s look at our other segmented item screen, the heroes list. In theory we can accomplish more on this screen than in the dashboard. However, that is up to you and how you’ve set it up.

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

import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { DataService } from "../../services/data.service";

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

    public heroes: Array<any>;

    public constructor(private router: Router, private data: DataService) {
        this.heroes = this.data.getHeroes();
    }

    public add(value: string) {
        if(value != "") {
            this.data.add(value);
        }
    }

    public remove(id: number) {
        this.data.delete(id);
    }

    public edit(id: number) {
        this.router.navigate(["/hero", id]);
    }

}

From a logic perspective, the HeroesComponent is very similar to the DashboardComponent, but with more options. These options include adding and removing heroes.

When we want to add or remove a hero, the relevant methods from the DataService service are called.

Now let’s look at the HTML behind this logic. Open the project’s app/components/heroes/heroes.component.html file and include the following:

<StackLayout>
    <GridLayout rows="auto" columns="*, auto" margin="5" marginTop="15">
        <TextField #heroName hint="Hero Name" row="0" col="0" class="text-input"></TextField>
        <Button text="Add" (tap)="add(heroName.text); heroName.text = ''" class="btn btn-primary" row="0" col="1"></Button>
    </GridLayout>
    <Label text="My Heroes" class="h2"></Label>
    <ScrollView>
        <StackLayout>
            <GridLayout *ngFor="let hero of heroes" rows="auto" columns="30, *, 10" class="hero-grid-item">
                <Label text="{{ hero.id }}" row="0" col="0"></Label>
                <Label text="{{ hero.name }}" row="0" col="1" (tap)="edit(hero.id)"></Label>
                <Label text="x" row="0" col="2" (tap)="remove(hero.id)"></Label>
            </GridLayout>
        </StackLayout>
    </ScrollView>
</StackLayout>

While we could have used a FlexboxLayout again, I’ve decided to change it up and use a standard GridLayout instead. This layout acts more like a table.

The upper part of this page has a very basic form with a text input and button. When the button is pressed, the text in the field is passed to the add method in the TypeScript. The lower part of this page is a stack of tables. Each table has three columns, two of which have click events. We could have easily used one table with multiple rows, rather than multiple tables.

Creating a Way to Edit Existing Heroes

The third and final child page gives us a means to edit any existing hero. This can also be referred to as the detail page in a master-detail design scenario.

Starting with the page logic, open the project’s app/components/hero/hero.component.ts file and include the following TypeScript code:

import { Component, OnInit } from "@angular/core";
import { Location } from "@angular/common";
import { ActivatedRoute } from "@angular/router";
import { DataService } from "../../services/data.service";

@Component({
    selector: "hero",
    templateUrl: "./components/hero/hero.component.html",
})
export class HeroComponent implements OnInit {

    public hero: any;

    public constructor(private location: Location, private route: ActivatedRoute, private data: DataService) {
        this.hero = {};
    }

    public ngOnInit() {
        this.route.params.subscribe(params => {
            this.hero = this.data.getHero(params["id"]);
        });
    }

    public cancel() {
        this.location.back();
    }

    public save(id: number, name: string) {
        if(name != "") {
            this.data.edit(id, name);
            this.location.back();
        }
    }

}

Like with the other two children, the HeroComponent class is very similar in design. Instead of working with an array of heroes, we will be working with a single hero.

The goal here is to allow canceling which will take us to the previous page or saving which will call our DataService service and return the the previous page after.

Open the project’s app/component/hero/hero.component.html file and add the following HTML markup:

<StackLayout>
    <Label text="{{ hero.name }} Details..." class="h2"></Label>
    <GridLayout rows="auto, auto" columns="100, *" margin="5">
        <Label text="ID:" row="0" col="0"></Label>
        <Label text="{{ hero.id }}" row="0" col="1"></Label>
        <Label text="Name:" row="1" col="0"></Label>
        <TextView #heroName hint="{{ hero.name }}" row="1" col="1" class="text-input" margin="0"></TextView>
    </GridLayout>
    <GridLayout rows="auto" columns="*, *">
        <Button text="Cancel" (tap)="cancel()" class="btn btn-danger" margin="5" row="0" col="0"></Button>
        <Button text="Save" (tap)="save(hero.id, heroName.text)" class="btn btn-primary" margin="5" row="0" col="1"></Button>
    </GridLayout>
</StackLayout>

Like with the hero list page, I’ve decided to use a GridLayout over a FlexboxLayout as my layout. Either will do, it just depends on your design preference.

We’re essentially just displaying the hero information on the screen and submitting the new hero name during the click event.

Global CSS for the Application Components

Just like with Angular for the web, we can choose to have local CSS to each component or global CSS that can be used by all components.

For simplicity, the class names we used throughout this guide will exist in the global CSS file. Open the project’s app/app.css and include the following:

@import 'nativescript-theme-core/css/core.light.css';

.h2 {
    margin-top: 15;
    margin-left: 5;
}

.hero-flexgrid-item {
    width: 32%;
    margin: 5;
    padding: 10;
    background-color: #EEEEEE;
}

.hero-grid-item {
    margin: 5;
    padding: 10;
    background-color: #EEEEEE;
}

.text-input {
    border-color: #CCCCCC;
    border-width: 1;
    padding: 5;
    margin-right: 5;
}

.btn {
    margin: 0;
}

.btn-danger {
    background-color: red;
    color: #FFFFFF;
}

Custom CSS wasn’t really necessary towards the success of our application, but it does help make our application look more attractive and similar to the web version.

Conclusion

You just saw how to make your own mobile compatible Tour of Heroes application with Angular. While the official tutorial was designed for web, we took it to Android and iOS as a native mobile application with NativeScript.

If you wanted to take this application a step further, you could swap out the mock data in the service with the HTTP requests in the official tutorial. Then you can host your own RESTful API and it should work since all the HTTP and RxJS operators that you’d find in Angular for the web will exist in Angular for NativeScript. After all, they are one in the same.

If you’d like to learn more about nested child components and routing between them, check out a previous tutorial I wrote called, Nested Routing in a NativeScript Angular Application for Android and iOS.

Nic Raboy

Nic Raboy

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