Menu

Code Reuse in Angular 2 Native Mobile Apps with NativeScript

March 30th, 2016

Please welcome Nathan Walker as a guest-author on the Angular blog.  Read on to find out more about building cross-platform apps with Angular 2 and NativeScript.
The zen of multiple platforms.  Chrome, Android and iPhone all running the same code.
You may have felt the tremors reverberating from the bowels of Twitter or elsewhere about writing pure Native mobile apps in JavaScript. This is not hybrid, Cordova or webviews. These are truly native, 60fps mobile applications written with JavaScript. But that leaves one wondering: If there is no webview, how can I re-use my application code between the web and my mobile app? The answer is you need a JavaScript framework smart enough to do it, and a mobile runtime powerful enough to support it. Angular 2 is that framework, and NativeScript is that runtime. In this article, I'm going to show you how to create a single application with Angular 2 that can be rendered on the web, or rendered in a native mobile application with NativeScript. Here's what you can expect to learn:
  • How to build a Native mobile app from your existing web app codebase.
  • How NativeScript can fit perfectly in the mix with your Angular 2 web app.
  • How to utilize all of our existing web codebase with minimal to zero disruption.
  • How to configure Angular's Component to use the right view template on the right platform.
  • About a powerful feature in Angular 2: Decorators.
The strategy presented is used in the angular2-seed-advanced project (exemplified in image above). It exists for you to learn from, use directly for one of your projects as well as gather community feedback on potential integration improvements. In addition to NativeScript, it also supports ngrx/store for RxJS powered state management (Redux inspired), ng2-translate for i18n, lodash for reduction of boilerplate and more coming soon. Disclaimer: As always, there are multiple ways to achieve the same goal. Alternative strategies are welcome  and/or improvements to the one presented.

What is NativeScript? Brief Background

NativeScript, {N} for short, is an open source JavaScript framework that lets you build native mobile apps from a single code base. NativeScript works by leveraging JavaScript virtual machines—e.g. JavaScriptCore and V8—to create a bridge between the JavaScript code you write and the native APIs used to drive the native application. One notable and very exciting benefit is you will gain the ability to create truly Native components that are highly performant and feel natural on both Android or iOS. Angular 2's powerful and extensible architecture makes this possible and the ability to integrate the two technologies is achieved via nativescript-angular, a custom renderer for Angular 2 which makes it possible for Angular 2 templates to render native iOS and Android controls. More background and an excellent overview of this integration is presented here by TJ Vantoll. If you're just learning about Angular 2 and curious about the new Rendering Architecture, you can learn more about that here.

Angular 2 + NativeScript FAQ

Let me help by highlighting a few common questions/concerns you might have.

Q: Does NativeScript render native mobile apps from HTML?

A: No. NativeScript uses XML, but both HTML and XML are just markup languages. Perfect for constructing UI. It's all just angle brackets.

Q: Do I need to create a separate NativeScript XML template for each HTML template?

A: Yes.

Q: Isn't that pretty disruptive though?

A: Actually No. With Angular 2's Component Decorator, this becomes pretty...well...non-disruptive. Decorators add the ability to augment a class and its members as the class is defined, through a declarative syntax. To help illustrate, let's look at a Decorator you get for free by the Angular 2 framework: import {Component} from 'angular2/core'; @Component({   selector: 'sample'   templateUrl: './components/sample.component.html' }) export class SampleComponent {} @Component is a Decorator that declaratively augments the SampleComponent class by defining specific metadata about the class; in this case: a selector for it's use in the browser's DOM and a templateUrl to define a view. Because Angular 2 was built with extensibility in mind, you are not constrained by only the Decorators the framework provides, but rather you can extend these Decorators to create your own. In keeping with our goal to be as non-disruptive as possible with our NativeScript integration into our existing web app codebase, we are going to create our own custom Decorator, leveraging the power and elegance of Angular 2's Component Decorator by extending to augment with new capabilities allowing our NativeScript integration to be seamless. This will allow us to share code between our web app that runs in the browser, and your mobile app which will run on Android and iOS. To do so, we are going to create a useful utility which will make creating our custom Decorator easier. This utility can be found in the angular2-seed-advanced project in addition to more elaborate use cases for it. Let's start by looking at how this utility works so you can use it in your own apps.

1. Create a Decorator Utility

Decorators have been proposed to become an offical part of ES7. In the meantime, we will need the reflect-metadata shim loaded in our web app or included with your build setup (webpack, gulp, etc.). This shim provides the api to interact with our Decorator's metadata. In particular, it will load a global Reflect object we will use and define as const _reflect. import {Component} from 'angular2/core'; const _reflect: any = Reflect; export class DecoratorUtils {   public static getMetadata(metadata: any = {}, customDecoratorMetadata?: any) {     return metadata;   }      public static annotateComponent(cls: any, metadata: any = {}, customDecoratorMetadata?: any) {     let annotations = _reflect.getMetadata('annotations', cls) || [];     annotations.push(new Component(DecoratorUtils.getMetadata(metadata, customDecoratorMetadata)));     _reflect.defineMetadata('annotations', annotations, cls);     return cls;   } } We define 2 static methods:
  • getMetadata
  • annotateComponent
You may be wondering why getMetadata is there since it doesn't look like it does anything and you are correct. It doesn't...yet. The details of annotating our Component via Reflect's api is tucked away in annotateComponent. For those wanting to learn more about Reflect, I recommended reading this. Additionally, the incredibly illustrious and well-versed Pascal Precht at thoughtram has written a fantastic in-depth article here which will help provide more background information on Decorators.

2. Create a Custom Component Decorator using our Utility

Here we are exporting a function named BaseComponent which will become the name of our custom Decorator. It accepts our Component's metadata and passes that into our Utility's annotateComponent method. import {DecoratorUtils} from './utils'; export function BaseComponent(metadata: any={}) {   return function(cls: any) {     return DecoratorUtils.annotateComponent(cls, metadata);   }; }

3. Finally, create a Component using our Decorator:

Now, instead of using Angular 2's Component Decorator, we can use our custom BaseComponent Decorator. import {BaseComponent} from './decorators/base.component'; @BaseComponent({   selector: 'sample',   templateUrl: './components/sample.component.html' }) export class SampleComponent  {   public statement: string = 'Angular 2 is amazing. Even more so with {N}.'; } At this point, our BaseComponent Decorator is not helping provide anything unique. Not yet anyway. You may also be wondering, why go to the trouble of creating a custom Decorator at all? Do you really want to have conditional logic in every single one of your components that would swap your HTML templates with {N} XML templates? ... crickets ... I didn't think so. In the example above, our Web templateUrl: './components/sample.component.html': <h1>{{statement}}</h1> Remember earlier when we answered Yes to creating a separate NativeScript XML template for each HTML template? The possibilities are endless with the rich variety of native UI Components provided by NativeScript, but here's one way we might create that HTML view in NativeScript XML using nativescript-angular: <Label [text]="statement"></Label> This will render a Label UI control native to the host platform, be it Android or iOS. You might recognize the [text]="statement" binding. Yep, that's just standard Angular 2 bindings.
Our *native* `Label` UI on both Android and iOS.
Note: Please take note to not use self-closing elements like <Label [text]="statement" />. This is related to the Parse5DomAdapter noted here. Ok, now let's make our custom BaseComponent Decorator do something interesting.

Use the NativeScript XML view when running the mobile app

Queue in our Decorator Utility. The goal is to teach our Component's to know which {N} templateUrl to use without disrupting our web development flow. A new service will be introduced, ViewBrokerService, which we will create momentarily. Since we will be using our handy utility for all of our custom decorators, let's make our adjustment there: import {Component} from 'angular2/core'; import {ViewBrokerService} from '../services/view-broker.service';  const _reflect: any = Reflect; export class DecoratorUtils {   public static getMetadata(metadata: any = {}, customDecoratorMetadata?: any) {          if (metadata.templateUrl) {       // correct template for platform target       metadata.templateUrl = ViewBrokerService.TEMPLATE_URL(metadata.templateUrl);     }          return metadata;   }      public static annotateComponent(cls: any, metadata: any = {}, customDecoratorMetadata?: any) {     let annotations = _reflect.getMetadata('annotations', cls) || [];     annotations.push(new Component(DecoratorUtils.getMetadata(metadata, customDecoratorMetadata)));     _reflect.defineMetadata('annotations', annotations, cls);     return cls;   } } What is happening here?
  • If our component defines a templateUrl, we are going to shuttle it through ViewBrokerService.TEMPLATE_URL which will handle "brokering" the right view template for the right platform.
  • getMetadata now serves a purpose to keep our augmented capabilities tidy and isolated away from the details of the Reflect api. This allows us to focus on the special sauce that will make our custom Decorator tick with less distraction.

Create ViewBrokerService

This slim service provides a single static method, TEMPLATE_URL, which will be used to provide the proper path to a template based on a static runtime configuration setting, handled by CoreConfigService; created in a moment. import {CoreConfigService} from './services/core-config.service'; export class ViewBrokerService {      public static TEMPLATE_URL(path: string): string {       if (CoreConfigService.IS_MOBILE_NATIVE()) {       path = path.slice(1); // remove leading '.'       return `./frameworks/nativescript.framework${path}`; // this can be any path to your {N} views     } else {       return path;     }    } } The path returned for your {N} templates can be any location in your codebase. Here's a condensed view of the sample directory structure used throughout: In this example, all NativeScript templates are contained in a nativescript.framework folder nested under a frameworks folder. The path expands underneath the nativescript.framework folder to match the exact same path our Component's templateUrl defined, which was: templateUrl: './components/sample.component.html' With help from our Decorator, the ViewBrokerService will expand our Component's templateUrl to become: templateUrl: './frameworks/nativescript.framework/components/sample.component.html' That is, of course, only if your runtime configuration says so.

Create CoreConfigService

A pragmatic way to provide some static configuration options that can be set at runtime. interface IPlatforms {   WEB: string;   MOBILE_NATIVE: string; } export class CoreConfigService {      // supported platforms   public static PLATFORMS: IPlatforms = {     WEB: 'web',     MOBILE_NATIVE: 'mobile_native'   };      // current target (default to web)   public static PLATFORM_TARGET: string = CoreConfigService.PLATFORMS.WEB;       // convenient platform checks   public static IS_WEB(): boolean {     return CoreConfigService.PLATFORM_TARGET === CoreConfigService.PLATFORMS.WEB;   }      public static IS_MOBILE_NATIVE(): boolean {     return CoreConfigService.PLATFORM_TARGET === CoreConfigService.PLATFORMS.MOBILE_NATIVE;   } }

The Final Stretch

With all the details in place, we are now ready to run (bootstrap) both of our applications. We will need 2 separate bootstrap files, one for the web and the other for our native mobile app.

Web Bootstrap

Our web bootstrap may look something like this: // angular import {provide} from 'angular2/core'; import {bootstrap} from 'angular2/platform/browser'; // app import {SampleComponent} from './components/sample.component'; bootstrap(SampleComponent, [])   .catch(err => console.error(err));

NativeScript Bootstrap

We will use a different bootstrap file for our NativeScript app using nativeScriptBootstrap provided by nativescript-angular. // nativescript import {nativeScriptBootstrap} from 'nativescript-angular/application'; // config import {CoreConfigService} from './services/core-config.service'; CoreConfigService.PLATFORM_TARGET = CoreConfigService.PLATFORMS.MOBILE_NATIVE; // app import {SampleComponent} from './components/sample.component'; nativeScriptBootstrap(SampleComponent, []); Now when our NativeScript app runs, the templateUrl will be swapped out with the {N} view and Voila! You are now using all the code from your web application in your native mobile app! A truly amazing feat. The 2 unique considerations are:
  1. Create {N} XML template for each HTML template.
  2. Create a separate bootstrap file for your NativeScript app which sets a static configuration option used by your ViewBrokerService.

There's always one other thing

Say you want to utilize the same Component method for both your web view and {N} view. Depending on the specific control in use, you may need some conditional logic to determine what the user clicked on in the web vs. native mobile. Here's an example using our Component: import {BaseComponent} from './decorators/base.component'; import {CoreConfigService} from './services/core-config.service'; @BaseComponent({   selector: 'sample',   templateUrl: './components/sample.component.html' }) export class SampleComponent  {   public dogs: Array<any> = [     { title: 'Greyhound' },     { title: 'Frenchie' },     { title: 'Brussels Griffon' }   ];      public selectDog(e: any, dog?: any) {     if (CoreConfigService.IS_MOBILE_NATIVE()) {       if (e) {         // newIndex is a property of the SegmentedBar control event         dog = this.dogs[e.newIndex];       }     }           console.log('Selected dog:', dog);   } } Web template: './components/sample.component.html' <ul>   <li *ngFor="#dog of dogs" (click)="selectDog($event, dog)">{{dog.title}}</li> </ul> NativeScript template: './frameworks/nativescript.framework/component/sample.component.html' <SegmentedBar [items]="dogs" (selectedIndexChanged)="selectDog($event)"></SegmentedBar> The result. Web template using some CSS styling. NativeScript SegmentedBar as is.

To {N}finity and Beyond!

The advanced Angular 2 seed expands on the ideas presented here to provide a NativeScript option to the popular angular2-seed by Minko Gechev, the author of Switching to Angular 2. You will notice that with the advanced seed, the NativeScript app is in a separate directory nativescript aside from the main web src directory. The web src is actually copied into the nativescript directory when the NativeScript app is run with these instructions. This is done for several reasons, but to list the most important:
  • Removes the need to process {N} specific modules in the main web build which uses gulp.
Keep an eye on the advanced seed for improvements to potentially move the nativescript directory inside the src directory alongside the main web source (to remove the necessity of copying the src). The build will be modified soon so only the specific code relevant for either platform web or native mobile would be built upon command.