A 10 minute primer to the new Angular router
The new Angular router is born!
Brian Ford announced at ng-conf that the new Angular router will be available soon in Angular 1.x.
You can view Brian's talk on YouTube.
With the new router being released and not a huge amount of documentation being available yet, I decided to share my own personal notes I gathered while experimenting with the new router.
If you're currently using ngRoute
or ui-router
, it will take some mental shifting to get used to the new router and its concepts, so hopefully this article helps you avoid some of the pitfalls I encountered.
While ui-router
is a state based router, I'd like to consider the new Angular router a component router, where components are fundamental building blocks of the modern web.
If that sounds new to you, don't worry, just stay with me and by the end of this article you'll have a basic but solid understanding of the new router principles and concepts.
For complete instructions on how to install the new router, make sure to check out the official "Getting started" page.
If you want to start playing with the new router immediately, I have also created a codepen that you can use to fork and experiment without having to install anything.
Special note (April 28, 2015)
The new router API has changed a few days ago. The content of this article is still valid but the syntax in the code is slightly different now.
I am working on an updated version of this primer. If you want me to send you a note when it's ready, feel free to leave your email address below the article.
Meanwhile, I would recommend visiting the official routing guide on the Angular 2 website or the Component Router page on the AngularJS 1.x page for the latest developments and updated API documentation.
Thanks and enjoy reading!
So let's get started!
First let's have a look at a very basic application that uses the new router:
Markup:
<body ng-app="app" ng-controller="AppController">
<!-- Define a viewport and give it a name -->
<div ng-viewport="main"></div>
</body>
Script:
// Inject router in controller
angular.module('app', ['ngNewRouter'])
.controller('AppController', ['$router', AppController]);
// Controller constructor
function AppController ($router) {
// Configure router, pass array of mappings
$router.config([
{
// Define url for this route
path: '/',
// Map components to viewports for this route
components: {
// Load home component in main viewport
'main': 'home'
}
]);
}
Let's analyze together
$router
First of all we see that the router is injectable as $router
and can be configured from within a controller. This means that we can configure the router dynamically at application runtime.
We no longer need to define the routes in advance during the config phase of the application, as is the case with other routers like ngRoute
and ui-router
.
This is very powerful and allows lazy loading of components, where routes inside components are only added to the router when a component is instantiated.
Don't worry if you don't understand what that means for now, we'll get there shortly.
Viewports
Next, we see the term viewport pop up. A viewport allows you to specify where you want the new router to insert a component in the DOM, similar to how ng-view
works in ngRoute
and ui-view
in ui-router
.
To define a viewport, you use the ng-viewport
directive:
<body ng-app="myApp" ng-controller="AppController as app">
<div ng-viewport></div>
</body>
Multiple viewports are supported as well, but then you need to give them a name so you can identify them later on:
<body ng-app="myApp" ng-controller="AppController as app">
<!-- Multiple viewports require a name -->
<div ng-viewport="nav"></div>
<div ng-viewport="main"></div>
</body>
In fact, if you don't give a viewport a name, it will be given the name default
behind the scenes, so an unnamed viewport is essentially the same as:
<body ng-app="myApp" ng-controller="AppController as app">
<!-- Same as unnamed viewport -->
<div ng-viewport="default"></div>
</body>
Viewports with the same name result in the router throwing an error:
<body ng-app="myApp" ng-controller="AppController as app">
<!-- NOT allowed, throws error -->
<div ng-viewport="main"></div>
<div ng-viewport="main"></div>
</body>
Components
The term component in the context of the new Angular router stands for a combination of a template and a controller.
Do not think of a component as a route or a state or anything you may be familiar with in your current setup or application.
Do think of a component as a web component or an Angular directive with a controller and a template.
Each component has a name and when a component is instantiated, two things happen:
- the component controller is instantiated
- the component template is loaded and shown in a viewport
If we look at the router configuration again:
Script:
// Configure router, pass array of mappings
$router.config([
{
// Define url for this route
path: '/',
// Map components to viewports for this route
components: {
// Load home component in main viewport
'main': 'home'
}
]);
you notice that we only specify a component name in the mapping object.
So if a component consists of a controller and a template, how does the router know which controller and which template to use if we only specify the component name?
For optimal convenience the new router has a built-in component loader that uses a very simple convention:
A component named home is automatically instantiated as:
- a controller named
HomeController
- a template stored as
components/home/home.html
If your project requires a different strategy, you can provide the router with your own logic to resolve controllers and templates.
What's more is that the component controller is made available inside the component template as the component name. Think of it as using the controllerAs syntax behind the scenes.
So if you have a component named home
:
HomeController
is loaded as controllercomponents/home/home.html
is loaded as templateHomeController
is made available ashome
inside the template
This allows you to access controller properties and methods from within your template as:
<h1>Hi {{home.name}}</h1>
Each component also offers a series of component lifecycle hooks that allows you to hook into the routing process. As soon as one of the hooks returns false (or a promise that is rejected), routing is cancelled.
Hooks are the perfect place to perform (asynchronous) operations that you want to perform before instantiating the component such as loading data, perform authorization, verify access, etc.
The exact inner workings of the hooks and their exact execution time are documented here. Think of the hooks as places where you can define custom logic that determines whether you want to cancel routing or not. A perfect example would be authorization and access control.
By now you hopefully have a basic understanding of what viewports and components are, so it's time to have a look at what the router actually does with them.
How do we tell the router what to do?
The router is configured by passing an array of mappings to the $router.config
method where each mapping associates a path with a map of viewports and components.
So if we have the following markup:
<body ng-app="app" ng-controller="AppController">
<!-- Define a viewport and don't give a name -->
<div ng-viewport="main"></div>
</body>
we can use the following code in AppController
:
// Configure router, pass array of mappings
$router.config([
{
// Define url for this route
path: '/',
// Map components to viewports for this route
components: {
// Load home component in main viewport
'main': 'home'
}
}
]);
to tell the router that whenever the url equals /
, we want the router to load the home
component in the main
viewport.
The $router.config()
method accepts an array so you can quickly configure multiple routes at once:
$router.config([
{
path: '/',
components: {
'main': 'home'
}
},
{
path: '/admin',
components: {
'main': 'admin'
}
}
]);
There is also a shorthand notation in case you're using an unnamed viewport:
<body ng-app="app" ng-controller="AppController">
<!-- Define a viewport and don't give a name -->
<div ng-viewport></div>
</body>
where you can pass a string
instead of an object:
$router.config([
{
path: '/',
component:'home'
},
{
path: '/admin',
component: 'admin'
}
]);
You can use either acomponent
orcomponents
key in your mapping object so you can use whatever you feel is most intuitive. They behave exactly the same as you can read here. Using them both together however will throw an error. Here I use the singular syntax since there is only one viewport.
We already learned that behind the scenes an unnamed viewport is equal to a viewport with the name default
, so using an unnamed viewport is essentially the same as:
Markup:
<body ng-app="app" ng-controller="AppController">
<!-- Same as unnamed viewport -->
<div ng-viewport="default"></div>
</body>
Script:
$router.config([
{
path: '/',
// Same as component: 'home'
components: {
'default': 'home'
}
},
{
path: '/admin',
// Same as component: 'admin'
components: {
'default': 'admin'
}
}
]);
Additionally we also learned that the new router allows you to create multiple named viewports:
<body ng-app="app" ng-controller="AppController">
<!-- Define multiple named viewports -->
<div ng-viewport="nav"></div>
<div ng-viewport="main"></div>
</body>
which can be configured individually by using the viewport names as the keys in the mapping object:
$router.config([
{
path: '/',
components: {
// Load nav component in nav viewport
'nav': 'nav',
// Load home component in main viewport
'main': 'home'
}
}
]);
telling the router to instantiate:
- the
nav
component in thenav
viewport - the
home
component in themain
viewport
when the url equals /
.
By now you hopefully realize why multiple viewports require a name: we need the viewport names to tell the router where we want it to render the components in the DOM.
So far you probably think the new router resembles ngRoute
and ui-router
a lot.
However, there is more, a lot more... stay with me...
Child routers
Whenever you define a viewport, a new child router is instantiated.
The new child router can then be injected in the controller of the component that is associated with the viewport.
Let's assume the following markup:
<body ng-app="app" ng-controller="AppController">
<div ng-viewport="main"></div>
</body>
and the following scripts:
function AppController ($router) {
// Here $router is the root router
$router.config([
{
path: '/',
components: {
'main': 'home'
}
}
]);
}
function HomeController ($router) {
// Here $router is a child router
// of the root router and the controller
// can define routes embedded within the
// home component
$router.config([
// ...
]);
}
As you can see in the comments, the $router
injected in HomeController
is not the same $router
instance that is injected in AppController
. The $router
in HomeController
is a child router of the $router
in AppController
.
This is a very powerful concept and allows us to build components that have their own embedded route configurations.
I have attempted to visualize the process for easier understanding (click the image for a full size version):
A huge shift to reusable components
This powerful concept implies that we have to shift our thinking into building self-contained components that are not concerned with the inner workings of other components.
Ultimately this leads to highly reusable and loosely coupled components where even Angular itself is no longer concerned with their inner workings (e.g. you could use a web component instead of an Angular directive and everything would still work).
It also means we can no longer pass state down to components via the router, as you were probably used to doing with a state based router like ui-router
, simply because components are no longer aware of routes within other components.
I highly recommend you watch Misko Hevery's keynote from ng-conf, where he talks with tremendous enthousiasm about reusable components. An enhousiasm I share with him as it will change the web entirely.
But how do we interact with self-contained components?
If you can't pass state to components via the router, how can you interact with a component to tell it what to do, how to behave and how can you ask a component for data?
In essence, there are 4 ways:
- properties: allow you to push data into a component
- events: allow you to listen for events the component emits
- methods: allow you to control the component using its API
- shadow DOM: similar to transclusion, shadow DOM offers insertion points that allows you to push content into a component (but with more extensive features)
This is also reflected in the new Angular template syntax:
<component #x [property]="expression" (event)="action()">
Content that can be pulled in
by the component using shadow DOM
and insertion points.
</component>
<button (click)="x.method()">
Make component do something
</button>
where:
#x
: exposes the component API to call methods likex.method()
[property]
: allows expressions to be bound to DOM properties of the components (you can pass objects too, not just strings!)(event)
: allows actions to be bound to events that the component emits- content can be pushed into the component using shadow DOM.
The exact details of the new syntax are beyond the scope of this article, but I found it important to briefly touch upon them because of their large impact on how we should write self-contained components.
So instead of passing state through the router, we should now pass state to components using one or more of the ways described above.
In the context of the new Angular router, this means that if we instantiate a component, we should fetch state in the component controller:
function ComponentController ($router) {
// Fetch state here
// e.g. using $http
this.data = [1, 2, 3];
}
and pass the state down to other components inside the component template:
<other-component [data]="component.data"></other-component>
You may have noticed that this is almost similar to how Flux/Flow and ReactJS work to pass state down the hierarchy of components in an application.
This makes components self-contained and highly reusable and is essentially what the web components best practices are about, allowing you to use already existing web components without having to write them yourself.
Conclusion
The new Angular router is a component router that associates url's with mappings of viewports and components.
Building routable components requires a shift in thinking to build components that are self-contained and highly reusable.
Their synergy is amazingly powerful and will change the future of web applications entirely.
For the better!
PS: If you wish to receive a notification when follow up articles appear, just leave your email address below and I'll send you a short note when they are available.