A lot has been written about Angular2 over the past year. Much of the information has quickly become outdated as the API continues to evolve and mature. This fast-paced change has created gaps in documentation, making it difficult for busy developers to stay up-to-date with the latest version of the framework.
I’ve been wanting to test out a number of new technologies; ASP.NET 5 (now ASP.NET Core 1.0), Angular2, TypeScript, and JSPM, to name a few.
Recently, I started a new project and had the opportunity to test out the Angular2 framework. There is always risk associated with using new technologies on a project – lack of up-to-date documentation, project configuration problems, and unforeseen bugs.
I encountered all of the above.
In this Angular2 tutorial, I’ll reveal the problems faced and lessons learned along the way so you are better positioned to have success with this exciting new technology.
ASP.NET Core 1.0
I wanted to try the new ASP.NET, which combines MVC and Web API.
Also, I develop on a MacBook Pro in Parallels and because ASP.NET Core is cross platform, the idea of native development in OS X without having to spin up a Windows VM was enticing.
Angular2/TypeScript
Upon starting the project, I knew I wanted to use Angular2, but wasn’t so sure about TypeScript. I’d heard a lot about the tool, but didn’t really see any practical reason to use it over regular plain old JavaScript. However, after finding examples all over the web while searching for the best way to get started with Angular2, and since Angular2 is built in TypeScript, I decided to give it a shot.
From using annotations (@Injectable()) to adding metadata to classes or class members, to using lambdas ((a) => a.foo === b;) instead of anonymous functions, there are so many great features and syntax to take advantage of.
JSPM
Historically, the defacto standard for JavaScript dependency management has been NPM for dev/build dependencies and Bower for browser dependencies. But, there has been a lot of innovation out there recently. More and more developers are starting to use npm3 for browser dependencies. The Angular2 team has decided they will only distribute via npm, not bower.
There are ways to make it work with bower. But, I decided to branch out and explore the other options.
From the official Angular2 site, there are various examples using SystemJS to load scripts. SystemJS is built and maintained by Guy Bedford. Guy has also been working on JSPM, a package manager for frontend dependencies that uses SystemJS. One of the benefits of using JSPM over Bower is that it handles resolving and loading dependencies for the user. Instead of having to rely on a complicated grunt/bower task to find, order, concatenate, minify and inject bower dependencies, JSPM maps the dependencies and relies on SystemJS to load it all, simplifying the frontend build process. More on this later.
Visual Studio Code
I started out using Visual Studio 2015 for the development environment, but eventually found it was too slow with this particular configuration and eventually switched to Visual Studio Code. Code has come a long way recently. In fact, I’d recommend it over Visual Studio 2015. It wasn’t long after using Visual Studio Code that I switched the entire dev environment completely to OS X and powered it off a VM. This set up provides a lot more flexibility.
Enough Talk, Show Me The Code!
Enough intro. Let’s dive into some code! (See the completed walkthrough on Github.)
ASP.NET 5/Core 1 Setup
(Feel free to skip this section if ASP.NET is not your thing.)
Let’s get the backend up and running first. I’ll assume you already have ASP.NET 5/Core 1 and NPM installed and updated. If you don’t, see the ASP.NET Getting Started docs and the NPM docs.
First, install is the ASP.NET yeoman generator (and yeoman if you don’t already have it):
> npm install -g yo generator-aspnet
Next generate a new project by following the prompts:
> yo aspnet _-----_ | | .--------------------------. |--(o)--| | Welcome to the | `---------´ | marvellous ASP.NET 5 | ( _´U`_ ) | generator! | /___A___\ '--------------------------' | ~ | __'.___.'__ ´ ` |° ´ Y ` ? What type of application do you want to create? Web API Application ? What's the name of your ASP.NET application? AspnetCore1Angular2Jspm create AspnetCore1Angular2Jspm/.gitignore create AspnetCore1Angular2Jspm/appsettings.json create AspnetCore1Angular2Jspm/Dockerfile create AspnetCore1Angular2Jspm/Startup.cs create AspnetCore1Angular2Jspm/project.json create AspnetCore1Angular2Jspm/Properties/launchSettings.json create AspnetCore1Angular2Jspm/Controllers/ValuesController.cs create AspnetCore1Angular2Jspm/wwwroot/README.md create AspnetCore1Angular2Jspm/wwwroot/web.config Your project is now created, you can use the following commands to get going cd "AspnetCore1Angular2Jspm" dnu restore dnu build (optional, build will also happen when it's run) dnx web
Next, cd into the generated project and restore it’s dependencies:
> cd AspnetCore1Angular2Jspm > dnu restore
Open up Visual Studio Code:
> code .
Note: If code . doesn’t work for you, see Launching from the Command Line.
Add a basic index.html to to the wwwroot folder. Something like this:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>ASP.NET Core 1/Angular2/JSPM Sample</title> </head> <body> <p>Hello world!</p> </body> </html>
You need to tell ASP.NET to serve the file. Open up the Startup.cs and add the following line in the Configure method, anywhere before app.UseStaticFiles():
app.UseDefaultFiles();
Run the following and you should see a little “Hello world!” page when you navigate to http://localhost:5000.
> dnx web
Note: To stop the dnx web command at any time, press ^C (Ctrl + c).
JSPM/TypeScript/Angular2 Setup
The folder structure should look similar to this:
. ├── Controllers │ └── ValuesController.cs ├── Dockerfile ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.json ├── project.json ├── project.lock.json └── wwwroot ├── README.md ├── index.html └── web.config
Create a package.json by running the following and answering the prompts:
> npm init
Install JSPM. You’ll install it globally first, then install it locally as a dev dependency:
> npm install -g jspm > npm install --save-dev jspm
Configure JSPM with run jspm init for the following responses:
> jspm init Would you like jspm to prefix the jspm package.json properties under jspm? [yes]: Enter server baseURL (public folder path) [./]:./wwwroot Enter jspm packages folder [wwwroot/jspm_packages]: Enter config file path [wwwroot/config.js]: Configuration file wwwroot/config.js doesn't exist, create it? [yes]: Enter client baseURL (public folder URL) [/]: Do you wish to use a transpiler? [yes]: Which ES6 transpiler would you like to use, Babel, TypeScript or Traceur? [babel]:typescript ok Verified package.json at package.json Verified config file at wwwroot/config.js Looking up loader files... system.js system.src.js system.js.map system-csp-production.js system-polyfills.js system-csp-production.js.map system-csp-production.src.js system-polyfills.src.js system-polyfills.js.map Using loader versions: systemjs@0.19.17 Looking up npm:typescript Updating registry cache... ok Installed typescript as npm:typescript@^1.6.2 (1.7.5) ok Loader files downloaded successfully
This will install the TypeScript compiler and anything else it needs in wwwroot/jspm_packages/ and map everything up in the wwwroot/config.js. I’ll explain how that works in a little bit.
Note: You may want to add jspm_packages/ to your .gitignore.
For convenience, lets add the following to the scripts property of the packages.json . This will run jspm install anytime you run npm install :
"postinstall": "jspm install"
Add a tsconfig.json file to wwwroot/:
{ "compilerOptions": { "target": "es5", "module": "commonjs", "emitDecoratorMetadata": true, "experimentalDecorators": true }, "exclude": [ "jspm_packages" ] }
You will need to install a plugin to load the TypeScript files called plugin-typescript, built by Frank Wallis:
> jspm install ts
Add the typescriptOptions and packages properties to configuration in wwwroot/config.js, so that it looks like this:
System.config({ baseURL: "/", defaultJSExtensions: true, transpiler: "typescript", typescriptOptions: { "tsconfig": true, "module": "system" }, packages: { "app": { "defaultExtension": "ts", "meta": { "*.ts": { "loader": "ts" } } } }, paths: { "npm:*": "jspm_packages/npm/*" }, map: { ... } });
Now, we’re finally ready to install Angular2:
> jspm install angular2
You may notice at this point the map property in wwwroot/config.js has blown up with hundreds of entries. The config.js is a file SystemJS uses to load your app’s dependencies as well as their own dependencies. When you install something with JSPM, it places it inside the jspm_packages folder and adds the path to the map property (along with any dependencies) so SystemJS knows where to look when the app requires an external module.
Pretty cool, huh?
There is one thing left to do in the initial setup. Install the Angular2 dependencies as dependencies of your own. This gets a little tricky because you have to make sure the versions stay in sync with Angular2, otherwise two different copies will be downloaded and imported into the app.
Install reflect-metadata, rxjs, and zone.js. As of this writing, angular2 resolves to version 2.0.0-beta1 . reflect-metadata@0.1.2, rxjs@5.0.0-beta.0 and zone.js@0.5.10 were all installed along with it as dependencies. If you’re using angular2@2.0.0-beta, run the following command:
> jspm install reflect-metadata@0.1.2 rxjs@5.0.0-beta.0 zone.js@0.5.10
Otherwise you may have to look up the exact versions in the config.js and adjust them before running the above command.
Building the App
With setup complete, we’re ready to start building. Go ahead and create an app folder inside wwwroot. This is where all of the Angular components, directives and services will live.
Inside wwwroot/app/, create an app.component.ts It will house the AppComponent or entry point component for the Angular2 app. Fill it with this:
import { Component } from 'angular2/core'; @Component({ selector: 'my-app', template: '{{welcome}}' }) export class AppComponent { welcome: string = 'Hello from Angular2!' }
Create a boot.ts inside of wwwroot/app/ :
import 'reflect-metadata'; // workaround for https://github.com/angular/angular/issues/6007 import Zone from 'zone.js'; window.Zone = Zone; import { bootstrap } from 'angular2/platform/browser'; import { AppComponent } from './app.component'; bootstrap(AppComponent);
This is where you’ll bootstrap the app. Notice the imports at the top for relect-metadata and zone.js. These are required by Angular2. Take note of the workaround that adds zone.js to the global namespace. Keep it there or bad things will happen 🙂
Once the Angular2 code is complete, load it into the index.html. Replace the innards of the body tag with the following:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>ASP.NET Core 1/Angular2/JSPM Sample</title> </head> <body> <my-app></my-app> <script src="jspm_packages/system.js"></script> <script src="config.js"></script> <script>System.import('app/boot')</script> </body> </html>
Reload the page and you should see “Hello from Angular2!”!
Optimize
You may notice that when the page is loaded the browser made something like 258 requests and downloaded 3.1 mb…
What the heck!” you might be saying, “I only have two files, app.component.ts and boot.ts!”
Take a look at the requests and you’ll see typescript.js – the typescript compiler – which accounts for 1.8 mb of the download. The rest of the files are angular. The browser is transpiling the typescript on the fly. Pretty cool, but definitely not something you want to do in production or really want to download and transpile each individual Angular2 file.
Here’s where the SystemJS builder comes in. It creates bundles you can load instead of lazy loading individual files. For development, create a bundle with arithmetic to include any external dependencies the app imports, but exclude any files within the app:
> jspm bundle app/**/* - [app/**/*] wwwroot/bundle.js --inject
The –inject will inject the list of files into the config.js and requests for those particular files will result in the request being intercepted, and the bundle will be served.
In other words, instead of 258 requests, the browser will only make 21 requests!
For a production build, remove the arithmetic and let it bundle everything. It’s important to understand that transpiling happens as part of the bundling step and no longer in the browser. In fact you can even pass a –minify flag into the cli and it will mangle the bundle for you.
> jspm bundle app/**/* wwwroot/bundle.js --minify --inject
It is also possible to use the JSPM bundler/builder with gulp/grunt if you’d like more flexibility. See the SystemJS builder documentation for more information.
Testing
There are a few important things to keep in mind when it comes to testing with JSPM and Angular2. The Angular2 team has been putting together a testing guide, which is a great reference, and still suggest using Karma and Jasmine for testing.
Go ahead and pull Karma and Jasmine down, along with the JSPM plugin for Karma and the PhantomJS2 launcher. I’m also going to throw in the Karma spec reporter:
> npm install --save-dev karma jasmine karma-jasmine karma-jspm karma-phantomjs2-launcher karma-spec-reporter
Create a karma.conf.js in the root of the project and make it look like this:
/* global module */ module.exports = function (config) { 'use strict'; config.set({ basePath: './wwwroot', singleRun: true, frameworks: ['jspm', 'jasmine'], jspm: { loadFiles: [ 'app/**/*.spec.ts' ], serveFiles: [ 'app/**/*!(*.spec).ts', 'tsconfig.json' ] }, proxies: { '/app': '/base/app', '/jspm_packages': '/base/jspm_packages', '/tsconfig.json': '/base/tsconfig.json' }, reporters: ['spec'], browsers: ['PhantomJS2'], }); };
Replace the value of test in the scripts property of the package.json with the following:
node ./node_modules/karma/bin/karma start
Finally, add a test. Create an app.component.spec.ts in the wwwroot/app/ folder. Fill it with the following:
import { AppComponent } from './app.component'; describe('AppComponent', () =>{ let appComponent: AppComponent; beforeEach(() => { appComponent = new AppComponent(); }); it('has the correct welcome message', () => { expect(appComponent.welcome).toEqual('Hello from Angular2!'); }); });
If you were to run npm test right now, we would get an error along the lines of, “Potentially unhandled rejection [3] reflect-metadata shim is required when using class decorators.” This is because you just imported the required relect-metadata shim in the boot.ts, not the app.component.ts.
But, you don’t want to import it in every test file. Luckily, you can leverage SystemJS again.
Add a meta property to wwwroot/config.js:
meta: { "angular2/core": { "deps": [ "es6-shim", "reflect-metadata", "rxjs" ] } },
This tells SystemJS that any time angular2/core is requested, load the dependencies as well.
Update (8 March 2016)
I found out that this wasn’t working as expected and went another route to pull those dependencies in automatically.
Create a test.ts file in wwwroot/app/ and fill it with the following:
import 'reflect-metadata'; import 'es6-shim'; import 'zone.js';
The include it in the loadFiles array before your tests in the karma.conf.js :
/* global module */ module.exports = function (config) { 'use strict'; config.set({ ... jspm: { loadFiles: [ 'app/test.ts', 'app/**/*.spec.ts' ], serveFiles: [ 'app/**/*!(*.spec).ts', 'tsconfig.json' ] }, ... }); };
We already installed reflect-metadata and zone.js earlier. Install es6-shim as well, but be sure to match the version es6-shim up with the version that shows in wwwroot/config.js under npm:angular2@2.0.0-beta.* . For angular2@2.0.0-beta.8 , es6-shim@0.33.13 is the current version:
> jspm install es6-shim@0.33.13
Now Angular2’s dependencies will be loaded before your tests and you should be able to run them without issue.
Run npm test .
You should get a passed test:
AppComponent ✓ has the correct welcome message PhantomJS 2.0.0 (Mac OS X 0.0.0): Executed 1 of 1 SUCCESS (0.002 secs / 0.001 secs) TOTAL: 1 SUCCESS
Conclusion
Using newer technologies such as ASP.NET Core, Angular2, and JSPM, definitely makes it feel like you’re on the fringe. I’m confident the development experience and productivity will be improved, especially when the bugs are all worked out. Staying on top of all the amazing new technologies being released can be a challenge, but it’s great to be part of a community that solves problems, documents and shares what they’ve learned for the common good.
Update (7 March 2016)
Thanks to the valuable feedback provided, here are a few more edits required to ensure app runs successfully.
Add the following line to the wwwroot/tsconfig.json so that systemjs won’t try to load sourcemaps:
{ "compilerOptions": { "target": "es5", "module": "commonjs", "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": false }, "exclude": [ "jspm_packages" ] }
Also, modify the wwwroot/config.js by adding the prefix npm: to the angular/core in the meta property so that it looks like so:
meta: { "npm:angular2/core": { "deps": [ "es6-shim", "reflect-metadata", "rxjs" ] } },
So as to avoid something like the following error from cropping up:
Error: XHR error (404 Not Found) loading http://localhost:5000/jspm_packages/npm/angular2@2.0.0-beta.1/core Error loading http://localhost:5000/jspm_packages/npm/angular2@2.0.0-beta.1/core as "../core" from http://localhost:5000/jspm_packages/npm/angular2@2.0.0-beta.1/platform/browser.js
Big thank you to Maulik Patel!
Updating Angular2
So this blog post is a few weeks old now and Angular2, as expected, has bumped it’s version a few times since. I thought I might as well update the app too.
This can be done easily through JSPM, like so:
> jspm install angular2
Don’t forget to update the dependencies we’ve installed, remember that we’ve gotta keep em’ in sync. At the time of this writing (7 March 2016) angular2@2.0.0-beta.8 relies on the following versions to be installed:
> jspm install rxjs@5.0.0-beta.2 zone.js@0.5.15
And that’s it. Not too bad. Be sure to checkout the Angular2 changelog for change details and especially any breaking changes so you can adjust your app’s code accordingly.