Angular2 with ES2015 and ui-router
Update: Updated for ui-router 1.0.0-alpha.5 and Angular2 rc1
Angular v2 is right around the corner. If you intend to move an existing application to Angular v2, now is the time to start playing around and planning out your migration strategy.
In this post I'll walkthrough a typical migration with 2 important features:
- The code will be in ES2015, not Typescript
- ui-router will be used for deep-linking
The main reason I wanted to use ES2015 is because there are very few examples available right now. Almost every example out there is written in Typescript, and the official angular.io examples are written in ES5 with no modules.
Typescript is great - I love Typescript. But maybe you just want to prove that Angular2 is right for your project without redoing your build system. It's good to know what to use instead of annotations in that case.
As for ui-router - it's hands down the best router for Angular v1, and they've released a version for Angular v2. For those unfamiliar with ui-router, the important thing to know is that instead of "routes" you have "states", which can be nested (parent -> child relationships) and have multiple named views (eg. header vs content).
The Example App
For this post, I've created an example app (github link) intended to tell us all about numbers, with the following features:
- A list of featured numbers.
- A view with details about a specific number, including a form for entering comments, and a list of all previous comments.
- A login screen, which is required if you want to post comments.
- Using ui-router to switch views.
So some obnoxiously simple content, but enough feature complexity to make the exercise worthwhile.
Here's the structure:
- app/ # Application source code
- details/ # number-details component
- list/ # number-list component
- login/ # Login modal form
- model/ # Shared 'model' services
- config.js # SystemJS config
- index.html # Bootstrap HTML
- index.js # Bootstrap JS code
- states.js # ui-router states
- e2e/ # End-to-end tests
- server/ # Mock server
- karma.conf.js # Unit tests config
- protractor.conf.js # E2E tests config
The Migration
The ultimate goal is to have the same application running in Angular v2 with no loss of functionality. Any E2E tests should still work with only minor tweaks.
You can find the final source code on github.
I also recommend taking a look at the UI-Router for Angular v2 QuickStart Source. It's an excellent resource, and I've shamelessly copied code snippets from it.
Code preparation
One important thing you'll want to do is upgrade ui-router to the 1.0.0 alpha and make use of the "Route to component" feature which lets you declare states and views with components instead of template + controller. This will make migrating to Angular v2 trivial.
package.json - Update dependencies
First thing we need to do is add Angular v2 libraries and remove Angular v1 libraries through npm:
npm install --save @angular/common@2.0.0-rc.1 @angular/compiler@2.0.0-rc.1 @angular/core@2.0.0-rc.1 @angular/http@2.0.0-rc.1 @angular/platform-browser@2.0.0-rc.1 @angular/platform-browser-dynamic@2.0.0-rc.1 rxjs@5.0.0-beta.6 ui-router-ng2@1.0.0-alpha.5 es6-shim reflect-metadata@0.1.3 zone.js@0.6.12
npm uninstall --save angular angular-mocks angular-ui-router angular-ui-bootstrap
app/ - Configuration and Bootstrap
Starting with the SystemJS configuration (app/config.js
), there's no surprises here. Just switching Angular v1 libraries for Angular v2 libraries. If you want to learn more about SystemJS config, best place to start is the SystemJS Configuration API Docs.
The HTML (app/index.html
) has not changed significantly at all. All I've done is included zone.js and Reflect through script tags, and added some new shims - most of which I probably don't need on an evergreen browser like Chrome or Firefox. I renamed bootstrap()
to bootstrapApp()
because (as you'll see in index.js
) it would conflict with @angular/platform-browser-dynamic.bootstrap()
.
In the JS (app/index.js
) we start to see some real differences. Lets step through this:
import {Injector} from '@angular/core';
export class MyUIRouterConfig {
static get parameters() {
return [[Injector]];
}
constructor(injector) {
this.injector = injector;
}
// ...
}
What we're doing is creating a class which provides the configuration for ui-router. You could think of it as the equivalent of a angularModule.config()
function with $urlRouterProvider
and $stateProvider
in Angular v1.
The static get parameters()
method is your equivalent of $inject
from Angular v1 - and I quite like the trick of using static get property()
rather than MyClass.property = {}
.
You'll notice instead of a flat array of strings, we've got an array of arrays with Classes. The Classes make sense, but why the second level of arrays? Well this is how you add metadata to your parameters. Take a look at this Typescript example for @OptionalMetadata
:
class Car {
engine;
constructor(@Optional() engine:Engine) {
this.engine = engine;
}
}
Here's how this would translate in ES2015:
class Car {
static get parameters() {
return [[Engine, new OptionalMetadata()]];
}
}
If @OptionalMetadata()
accepted an input, we'd pass it to the constructor.
Moving on with our MyUIRouterConfig
class:
import {getStates} from './states.js';
export class MyUIRouterConfig {
// ...
configure(uiRouter) {
// Register each state
getStates().forEach(state => uiRouter.stateRegistry.register(state));
// Specifies the default state - Similar to $urlRouterProvider.otherwise()
uiRouter.urlRouterProvider.otherwise(() => uiRouter.stateService.go('numbers-list', null, null));
// Register providers for resolve function parameters
let rootState = uiRouter.stateRegistry.root();
rootState.resolve['numbersModel'] = () => this.injector.get(NumbersModel);
}
ui-router looks for this configure(uiRouter)
method during bootstrap. First it's registering the different states (which are declared elsewhere). Then it's setting the numbers-list
state as the default to use when no other states match.
That last part is a temporary workaround. The Angular v2 version of ui-router is still using the old string based DI approach from Angular v1. They fully intend to support Angular v2 friendly DI in the future. But until then, any services you want to pass to a state's resolve
functions will need a string based mapping - like the one shown for numbersModel
.
Now the last touch for MyUIRouterConfig
:
import {Class} from '@angular/core';
export class MyUIRouterConfig {
// ...
}
Class({constructor: MyUIRouterConfig});
This is another ES2015 specific detail. If you take a look at the ES5 examples from angular.io, they mention this ng.core
namespace. eg. ng.core.Component().Class()
. This is exactly the same as import * as ngCore from '@angular/core'
. So what we've done here is pull in the ng.core.Class
function and wrap it around the MyUIRouterConfig
class. This is the equivalent of the following in Typescript:
import {Injectable} from '@angular/core';
@Injectable()
export class MyUIRouterConfig {
// ...
}
Alright, almost done with index.js
:
import {trace, UIROUTER_PROVIDERS, UiView, UIRouterConfig, Category} from 'ui-router-ng2';
import {HTTP_PROVIDERS} from '@angular/http';
import {provide, enableProdMode} from '@angular/core';
import {LocationStrategy, HashLocationStrategy, PlatformLocation} from '@angular/common';
import {BrowserPlatformLocation} from '@angular/platform-browser';
import {bootstrap} from '@angular/platform-browser-dynamic';
import {NumbersModel} from './model/numbers.js';
import {LoginModel} from './model/login.js';
export function bootstrapApp(prod=false) {
if (prod) {
enableProdMode();
} else {
trace.enable(Category.TRANSITION, Category.VIEWCONFIG);
}
bootstrap(UiView, [
provide(LocationStrategy, { useClass: HashLocationStrategy }),
provide(PlatformLocation, { useClass: BrowserPlatformLocation }),
...UIROUTER_PROVIDERS,
...HTTP_PROVIDERS,
NumbersModel,
LoginModel,
provide(UIRouterConfig, { useClass: MyUIRouterConfig })
]);
}
We're exporting a function that starts the application, taking 1 parameter which specifies if we're in "Prod Mode" or not. If we are, we run @angular/core.enableProdMode()
to disable debugging features in Angular v2. If not, we're calling ui-router-ng2.trace.enable()
to enable from debugging features for ui-router. What it does is output state transition information to the console, which is really helpful during development.
Next is the stock standard @angular/core.bootstrap()
call, with a few extra details for ui-router:
-
provide(...)
Theprovide
method lets us manipulate the DI during bootstrap, specifying exactly what to return when certain Classes or Tokens are used. This is useful when you might be dealing with both Server and Browser applications and you want to swap out Classes based on which is currently being used. -
provide(LocationStrategy, { useClass: HashLocationStrategy })
Because Angular2 can be run on multiple platforms (eg. browser, server, native, etc.) it needs to be explicit about what "Location" means for that platform. In this case we're specifying that the "Location", as far as Angular is concerned, is the URL after the hash (#). If we were using the whole path as per HTML5 mode, we would instead usePathLocationStrategy
. -
provide(PlatformLocation, { useClass: BrowserPlatformLocation })
Just as we specified how to use the Location, we're now specifying where that location comes from. For a browser it would be the URL. -
provide(UIRouterConfig, { useClass: MyUIRouterConfig })
. What this does is say "Angular - whenever something asks for aUIRouterConfig
instance, instead give them aMyUIRouterConfig
instance."
app/*/state.js - Update ui-router states
To update a ui-router state from Angular v1 to Angular v2, there's only 2 things you'll need to do:
- Use component classes instead of string names.
eg.{component: 'myComponent'}
becomes{component: MyComponent}
- Update resolve functions to use
$transition$.params()
instead of$stateParams
.
eg.(myModel, $stateParams) => myModel.get($stateParams.id)
would become(myModel, $transition$) => myModel.get($transition$.params().id)
3 more things you'll want to remember:
- You still use an
$inject
property for DI on resolve functions (for now). - Resolve functions still work off promises. So if you have an Observable, use
rxjs/add/operator/toPromise
to convert it to a promise. - Remember to add string mappings for anything you want to inject into resolve functions.
That's about it. Features like nested states and named views still work as before. Results from resolve functions will map to component bindings with the same name, just like in Angular v1.
app/*/template.html - Update component templates
I'm going to skip over the Angular v2 core details here (eg. Using (click)
instead of ng-click
, etc.) because there's plenty of resources around on that. I'll just point out the ui-router specifics.
Here's what a ui-sref
call in Angular v1 looks like (from app/list/template.html
):
<li ng-repeat="number in $ctrl.numbers" ui-sref-active="active">
<a href ui-sref="numbers-list.details({numberId: number.id})">{{number.label}} ({{number.id}})</a>
</li>
Here's the same thing in Angular v2:
<li *ngFor="let number of numbers" [class.active]="number === selectedNumber">
<a href uiSref="numbers-list.details" [uiParams]="{numberId: number.id}" (uiSrefStatus)="onStateStatus($event, number)">{{number.label}} ({{number.id}})</a>
</li>
Couple of things you'll notice:
- The state name and params are split into
uiSref
and[uiParams]
. ui-sref-active
has been replaced with(uiSrefStatus)
and[class.active]
. This is just a work around for a bug withuiSrefActive
where it will throw an error if it's not on the same or a descendant element touiSref
.
app/*/component.js - Update components
Components are registered using angular2/core.Component().Class()
, like so:
import template from './template.html!text';
import {Component} from 'angular2/core';
import {UIROUTER_DIRECTIVES} from 'ui-router-ng2';
import {NumbersListController} from './controller.js';
export const NumbersListComponent = Component({
selector: 'numbers-list',
template: template,
directives: [UIROUTER_DIRECTIVES]
}).Class({constructor: NumbersListController});
app/*/controller.js - Update component controllers
ui-router wise, there's not much you need to do to controllers.
If you used the $state
service, you'll want to declare a UIRouter
parameter and use uiRouter.stateService
. A great example of this is the uiSref
directive source code.
"Wait a sec - what about the login modal?"
Alas, the ng-bootstrap project which is built for Angular2 has not yet finished their modal, but it looks promising.
Conclusion
With a few tweaks we can get ui-router states written for Angular v1 working in Angular v2. Though there are a few ugly workarounds, but we're also using a bleeding edge "alpha" release - which I think is damn good.
I did gloss over a lot of Angular v1 to Angular v2 details that aren't specific to ui-router or using ES2015. So if there's anything you don't understand or you have any questions, feel free to leave a comment. The Angular v2 site also has a great "Upgrading from 1.x" guide.
Cheers,
Jason Stone