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:
// angularimport {provide} from 'angular2/core';import {bootstrap} from 'angular2/platform/browser';
// appimport {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.
// nativescriptimport {nativeScriptBootstrap} from 'nativescript-angular/application';
// configimport {CoreConfigService} from './services/core-config.service';CoreConfigService.PLATFORM_TARGET = CoreConfigService.PLATFORMS.MOBILE_NATIVE;
// appimport {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:
- Create {N} XML template for each HTML template.
- 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.