NOTE! You are browsing legacy documentation. For latest visit docs.nativescript.org.

NativeScript Angular

Introduction

Since the beginning of Angular (even back in the days when we were calling it Angular 2), you could use NativeScript with Angular to build mobile apps.

However, when you needed to build both a web and a native mobile app, you had to create two separate projects. However this has changed with the dawn of Schematics and ng add.

The Angular and NativeScript teams teamed up to create nativescript-schematics, a schematic collection that enables you to build both web and mobile apps from a single project.

Code-Sharing Projects

A code-sharing project is one where we keep the code for the web and mobile apps in one place. Here’s a quick diagram to show you what that looks like at a high level.

project-structure

The objective is to share as much code as possible, and split the platform-specific code into separate files.

This usually means that we can share the code for:

  • Routes for navigation,
  • Services for common business logic,
  • and Component Class definition for common behaviour of a component

While, splitting the code for:

  • UI Layer (CSS and HTML) - as you need to use different user interface components in web and NativeScript-built native apps,
  • and NgModules - so that you can import platform-specific modules, without creating conflicts (e.g. Angular Material Design - which is web only) between web and mobile.

Example: A Shopping Basket

For example in order to add a shopping basket to an online store, you would need:

  • a Basket Service,
  • a Payment Service and
  • a Basket Component

Like in this diagram:

basket-example

The BasketService should allow you to:

  • loadItems - get items placed in the basket
  • updateItemQuantity - change the quantity of a given item in the basket
  • remove - remove a given item from the basket

The Basket Service should be made of 100% shareable code.

basket.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Item } from '@src/app/item.model.ts';

@Injectable({
    providedIn: 'root'
})
export class BasketService {

  constructor(private http: HttpClient) { }

  public loadItems(): Observable<Items[]> {
    return this.http.get('get-data-request');
  }

  public updateItemQuantity(itemId: number, quantity: number) {
    // update item quantity code
  }

  public remove(itemId: number) {
    // remove item code
  }
}

payment.service.ts

The PaymentService - should allow you to:

  • processOneClickPayment - process a payment using user's default card

Again, the PaymentService should be made of 100% shareable code

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

@Injectable()
export class PaymentService {
  public processOneClickPayment() {
    // Payment code
  }
}

Next, you would need to create a component, that would inject the Basket and the Payment services, and allow users to:

  • see what is in the basket,
  • update the quantity of its contents and
  • buy the items.

One more time, the behaviour and the code of the BasketComponent should be 100% shared.

basket.component.ts

@Component()
export class BasketComponent implements onInit {
  items$: Observable<Item[]>;

  constructor(
    basketService: BasketService,
    paymentService: PaymentService
  ) { }

  public ngOnInit() {
    this.items = this.basketService.loadItems();
  }

  public increaseQuantity(item: Item) {
    this.basketService.updateItemQuantity(item.id, item.quantity + 1);
  }

  public decreaseQuantity(item: Item) {
    if (item.quantity > 1) {
      this.basketService.updateItemQuantity(item.id, item.quantity - 1);
    } else {
      this.removeItem(item);
    }
  }

  public removeItem(item: Item) {
    this.basketService.remove(item.id);
  }

  public pay() {
    this.paymentService.processOneClickPayment();
  }
}

The final bit is the implementation of the view templates. Both, templates should:

  • display a list of items - their name and quantity,
  • allow to increase or decrease the quantity of an item,
  • remove an item and
  • allow to process the payment

In this case the UI for the web and mobile apps is going to be different, and as a result the template code cannot be shared. However, everything underneath that drives its behaviour is still shared.

To create two separate templates, you just need to use a naming convention. Simply create two files:

  • basket.component.html - the web template file,
  • basket.component.tns.html - the NativeScript template file - it is the .tns that makes it a {N} file.

basket.component.html

<div>
  <h1>Basket</h1>
  <div *ngFor="let item of items$ | async">
    <h3></h3>
    <h4></h4>
    <button (click)="increaseQuantity(item)">➕</button>
    <button (click)="decreaseQuantity(item)">➖</button>
    <button (click)="removeItem(item)">🗑</button>
  <div>
  <button (click)="pay()">BUY NOW💰</button>
</div>

basket.component.tns.html

<ActionBar title="Basket"></ActionBar>
<StackLayout>
  <ListView [items]="items$ | async">
    <ng-template let-item="item">
      <GridLayout columns="*, auto, auto, auto, auto">
        <Label [text]="item.name"></Label>
        <Label [text]="item.quantity" col="1"></Label>
        <Button [text]="➕" (tap)="increaseQuantity(item)"></Button>
        <Button [text]="➖" (tap)="decreaseQuantity(item)"></Button>
        <Button [text]="🗑" (tap)="removeItem(item)"></Button>
      </GridLayout>
    </ng-template>
  </ListView>

  <Button text="BUY NOW💰" (tap)="pay()"></Button>
</StackLayout>

Even though, the contents of basket.component.html and basket.component.tns.html are not quite the same, you can see that in general they follow the same structure. Both use the async pipe to load the data from items$ and both use the component methods (increaseQuantity(item), pay()).

Remapped imports

It's strongly recommended to use remapped import statements in code-sharing projects. Don't worry if you are not familiar with the term - it was coined by the NativeScript team during the development of the NativeScript Angular code-sharing project structure. The remapped import statements are TypeScript import statements in the following form:

import { AppComponent } from '@src/app/app.component';

Notice the module specifier:

'@src/app/app.component'

In a code-sharing project, the TypeScript compiler is configured to understand the @src symbol. Depending on the platform you're building for - web or mobile, the compiler will use one of two configuration files. Let's take a look into the configuration file for web:

tsconfig.app.json

"compilerOptions": {
  "baseUrl": ".",
  "paths": {
    "@src/*": [
      "src/*.web.ts",
      "src/*.ts"
    ]
  }
}
...

This tells the compiler for any module specifier that matches the pattern "@src/*" (i.e. starts with "@src"), to look in two locations:

  1. "src/*.web.ts": meaning the web-specific file, i.e. @src/app/app.component => ./src/app/app.component.web.ts;
  2. "src/*.ts": meaning the module name unchanged, i.e. @src/app/app.component => ./src/app/app.component.ts.

The configuration for mobile is similar:

tsconfig.tns.json

"compilerOptions": {
  "baseUrl": ".",
  "paths": {
    "@src/*": [
      "src/*.tns.ts",
      "src/*.ts"
    ]
  }
}
...

Again, the platform-specific files are preferred during resolution:

  1. "src/*.tns.ts": meaning the NativeScript-specific file, i.e. @src/app/app.component => ./src/app/app.component.tns.ts;
  2. "src/*.ts": meaning the module name unchanged, i.e. @src/app/app.component => ./src/app/app.component.ts.

You can also use *.android and *.ios files to split any platform-specific logic. Their resolution is handled by webpack during build.

Lazy Loaded Modules

You need to use remapped imports when configuring lazy loaded modules as well.

Dynamic imports

Since Angular 8.0, you can use dynamic imports in your routing configuration. The import path should be a remapped import.

const routes: Routes = [
  {
    path: 'lazy',
    loadChildren: () => import('@src/app/lazy/lazy.module').then(m => m.LazyModule),
  }
];

Static strings

If you decide to stick to static strings for the paths of the lazy loaded modules, make sure they are also following the same format:

const routes: Routes = [
  {
    path: 'lazy',
    loadChildren: '@src/app/lazy/lazy.module#LazyModule',
  }
];

However, in this case, the build process won't detect automatically the module file. You have to include it manually in the TypeScript compilation. Update the necessary configuration TypeScript configuration files.

  1. If the module is mobile-only, update the mobile configuration:

    tsconfig.tns.json

    {
      ...
      "files": [
        "src/main.tns.ts",
        "src/app/lazy/lazy.module.tns.ts"
      ]
    }
    
  2. If the module is web-only, update the web configuration file:

    tsconfig.app.json

    {
      ...
      "files": [
        "src/main.ts",
        "src/polyfills.ts",
        "src/app/lazy/lazy.module.ts"
      ]
    }
    
  3. If the module is shared, update both configuration files:

    tsconfig.tns.json

    {
      ...
      "files": [
        "src/main.tns.ts",
        "src/app/lazy/lazy.module.ts"
      ]
    }
    

    tsconfig.app.json

    {
      ...
      "files": [
        "src/main.ts",
        "src/polyfills.ts",
        "src/app/lazy/lazy.module.ts"
      ]
    }
    

What’s next?

Read about Creating a New Project to get started.