Building Angular 2 Demos With System.js And TypeScript

If you've followed my blog for any time now, you've probably come to understand that I love ES5. Sometimes, I think maybe I'm one of the few people who actually thinks JavaScript is an awesome language. And, for months now, I've tried my hardest to write all of my Angular 2 demos using a single ES5 [and HTML] file. But, I must admit that some of the syntax provided for in ES6 and TypeScript is nice. And, it's the wave of the future. So, I'm going to try and get my new Angular 2 demos done with System.js and TypeScript.

Run this demo in my JavaScript Demos project on GitHub.

I know that not everyone enjoyed my "one page" approach to demos. But, for me, a single page made everything easier to think about. I could read it from top-to-bottom and each module depended on one of the modules below it. No jumping around from file to file, switching mental contexts, trying to follow the flow of logic. It was simple, easy, and I could CMD-D to seamlessly jump from one reference to another (using SublimeText).

I tried really hard to keep the "one page" approach while also leveraging TypeScript. But, no matter what I tried, it seemed to be the worst of both worlds. I neither got the benefits of ES5 hoisting nor the benefits of ES6 module exports. And, it just looked ugly. So, I have finally given up and am breaking the demos up into individual files.

That said, I am still refusing to add a build step. It's very important to me that my demos be runnable with as little overhead as possible. At least, whenever they can. As such, I'm using the System.js TyepScript plugin to perform on-the-fly transpiling to ES5 in the browser.

Now, I don't really know much about System.js or TypeScript. So, I'd like to give a huge shout-out to Meligy who helped me get this working. Also, special thanks to Martin Hochel, Guy Bedford, and a few others for letting me hassle them with questions. You guys all rock!

Because I keep all of my JavaScript demos in a single repository, I didn't want each demo to have it's own copy of all the NPM modules. But, because the demos are hosted on GitHub pages, I also don't have the luxury of running "npm install" for each demo. As such, all of the demos need to share a common set of NPM modules that are committed to the repository. Luckily, with System.js path and map aliasing, I can have all the commonly loaded modules be pulled from my vendor scripts.

In the common files, I now have:

/vendor/angularjs-2-beta/rc1/ node_modules

/vendor/angularjs-2-beta/rc1/package.json

/vendor/angularjs-2-beta/rc1/tsconfig.json

Notice that the package.json file is part of the common files. Any "npm install" that I do will be in the common files and the result of said install will be available to all of my System.js-based Angular 2 demos.

In the actual demo, I then configure System.js to point to "/vendor/angularjs-2-beta/rc1/" for all of the common dependencies:

(function() { // Alias the path to the common rc1 vendor scripts. var paths = { "rc1/*": "../../vendor/angularjs-2-beta/rc1/*" }; // Tell Angular how normalize path and package aliases. var map = { "@angular": "rc1/node_modules/@angular", "angular2-in-memory-web-api": "rc1/node_modules/angular2-in-memory-web-api", "rxjs": "rc1/node_modules/rxjs", "ts": "rc1/node_modules/plugin-typescript/lib/plugin.js", "tsconfig.json": "rc1/tsconfig.json", "typescript": "rc1/node_modules/typescript/lib/typescript.js" }; // Setup meta data for individual areas of the application. var packages = { "app": { main: "main.ts", defaultExtension: "ts" }, "rc1/node_modules": { defaultExtension: "js" } }; var ngPackageNames = [ "common", "compiler", "core", "http", "platform-browser", "platform-browser-dynamic", "router", "router-deprecated", "upgrade" ]; ngPackageNames.forEach( function iterator( packageName ) { packages[ "@angular/" + packageName ] = { main: ( packageName + ".umd.js" ) // , // defaultExtension: "js" }; } ); System.config({ paths: paths, map: map, packages: packages, transpiler: "ts", typescriptOptions: { tsconfig: true }, meta: { typescript: { exports: "ts" } } }); // Load "./app/main.ts" (gets full path from package configuration above). System .import( "app" ) .then( function handleResolve() { console.info( "System.js successfully bootstrapped app." ); }, function handleReject( error ) { console.warn( "System.js could not bootstrap the app." ); console.error( error ); } ) ; })();

As you can see, I'm mapping all "rc1/*" paths to the vendor directory. And, I'm mapping all common dependencies onto "rc1/". Most of the general logic in this file was taken from the Plunkr "Getting Started" guide for Angular 2.

My main demo page then loads several hard dependencies from the common node modules followed by the above configuration file:

<!doctype html> <html> <head> <meta charset="utf-8" /> <title> Building Angular 2 Demos With System.js And TypeScript </title> <link rel="stylesheet" type="text/css" href="./demo.css"></link> <!-- Load libraries (including polyfill(s) for older browsers. --> <script type="text/javascript" src="../../vendor/angularjs-2-beta/rc1/node_modules/core-js/client/shim.min.js"></script> <script type="text/javascript" src="../../vendor/angularjs-2-beta/rc1/node_modules/zone.js/dist/zone.js"></script> <script type="text/javascript" src="../../vendor/angularjs-2-beta/rc1/node_modules/reflect-metadata/Reflect.js"></script> <script type="text/javascript" src="../../vendor/angularjs-2-beta/rc1/node_modules/systemjs/dist/system.src.js"></script> <!-- Configure SystemJS loader. --> <script type="text/javascript" src="./system.config.js"></script> </head> <body> <h1> Building Angular 2 Demos With System.js And TypeScript </h1> <my-app> Loading... </my-app> </body> </html>

To test this all out, I created a very simple demo that loads a service that makes an API request to load an external JSON file (of friends). Here's my root component:

// Import the core angular services. import { Component } from "@angular/core"; import { HTTP_PROVIDERS } from "@angular/http"; import { NgInit } from "@angular/core"; // Import the application components and services. import { FriendService } from "./friend.service"; // I provide the root component of the application. @Component({ selector: "my-app", providers: [ HTTP_PROVIDERS, FriendService ], template: ` <div *ngIf="isLoading" class="loading"> Loading friends... </div> <div *ngIf="isDoneLoading"> <p> You Have {{ friends.length }} friends! </p> <ul> <li *ngFor="let friend of friends" [class.is-bff]="friend.isBFF"> <span>{{ friend.name }}</span> </li> </ul> </div> ` }) export class AppComponent implements NgInit { // I hold the collection of friends to display. public friends: any[]; // I provide access to the friend repository. public friendService: FriendService; // I determine if the data has been fully loaded. public isDoneLoading: boolean; public isLoading: boolean; // I initialize the component. constructor( friendService: FriendService ) { this.friends = []; this.friendService = friendService; this.isDoneLoading = false; this.isLoading = true; } // --- // PUBLIC METHODS. // --- // I get called once after the component has been instantiated and the input // properties have been bound. public ngOnInit(): void { this.friendService .getFriends() .subscribe( handleResolve.bind( this ) ) ; function handleResolve( newFriends: any[] ) : void { this.friends = newFriends; // Flag the data as fully loaded. this.isLoading = false; this.isDoneLoading = true; } } }

As you can see, it's all written in TypeScript. This will be transpiled on-the-fly to an ES5 target when System.js goes to load it (via the application bootstrapping, which I have not shown).

The root component provides the Http-related services and depends on the FriendService:

// Import the core angular services. import { Http } from "@angular/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { Response } from "@angular/http"; // Enable RxJS operators. import "rxjs/add/operator/map"; // I provide a service for accessing the Friend repository. @Injectable() export class FriendService { // I hold the URL prefix for the API call. private baseUrl: string; // I provide an HTTP client implementation. private http: Http; // I initialize the service. constructor( http: Http ) { this.baseUrl = "./app/"; this.http = http; } // --- // PUBLIC METHODS. // --- // I return the entire collection of friends as an Observable. public getFriends() : Observable { var stream = this.http .get( this.baseUrl + "friends.json" ) .map( this.unwrapResolve ) ; return( stream ); } // --- // PRIVATE METHODS. // --- // I unwrap the raw HTTP response, returning the deserialized data. private unwrapResolve( response: Response ) : string { return( response.json() ); } }

As you can see, this is also written in TypeScript.

Now, while the on-the-fly, in-browser TypeScript transpiler properly converts everything over to ES5, it doesn't appear to actually take into account type-checking. Meaning, if I have a function that's defined as returning an array but it actually returns a string, the transpiler doesn't throw an error. As such, if I include an incorrect type annotation, I probably won't notice it as it won't actually stop the demo from working. Oh well, you can't win every battle.

Part of me is sad that it has come this point. But, part of me is also excited for the future. I think that, in some ways, I stunt my learning by holding on too tightly to the existing technology. Now that I've taken the plunge on my demos, hopefully it will accelerate my learning of technologies like ES6, ES7, and TypeScript.

Tweet This Fascinating post by @BenNadel - Building Angular 2 Demos With System.js And TypeScript Woot woot — you rock the party that rocks the body!







