0% found this document useful (0 votes)
115 views94 pages

Angular Router Guide

This document provides an overview of how the Angular router works when navigating to a new URL. It explains that the router takes a URL and applies any redirects, recognizes the router state, runs guards and resolves data, and activates the necessary components. It also discusses some key router concepts like router states, navigation, URL formats, redirects, guards, resolving data, and activating components in outlets.

Uploaded by

ramon siles
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
115 views94 pages

Angular Router Guide

This document provides an overview of how the Angular router works when navigating to a new URL. It explains that the router takes a URL and applies any redirects, recognizes the router state, runs guards and resolves data, and activates the necessary components. It also discusses some key router concepts like router states, navigation, URL formats, redirects, guards, resolving data, and activating components in outlets.

Uploaded by

ramon siles
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Contents

1: What Do Routers Do?


b'Chapter 1: What Do Routers Do?'
b'Router configuration'
b'Router state'
b'Navigation'
b'Summary'
b'Isn't it all about the URL?'
2: Overview
b'Chapter 2: Overview'
b'URL format'
b'Applying redirects'
b'Recognizing states'
b'Running guards'
b'Resolving data'
b'Activating components'
b'Navigation'
b'Summary'
3: URLs
b'Chapter 3: URLs'
b'Simple URL'
b'Params'
b'Query params'
b'Secondary segments'
4: URL Matching
b'Chapter 4: URL Matching'
b'Backtracking'
b'Depth-first'
b'Wildcards'
b'Empty-path routes'
b'Matching strategies'
b'Componentless routes'
b'Composing componentless and empty-path routes'
b'Summary'
5: Redirects
b'Chapter 5: Redirects'
b'Local and absolute redirects'
b'One redirect at a time'
b'Using redirects to normalize URLs'
b'Using redirects to enable refactoring'
6: Router State
b'Chapter 6: Router State'
b'What is RouterStateSnapshot?'
b'Accessing snapshots'
b'ActivatedRoute'
b'Query params and fragment'
7: Links and Navigation
b'Chapter 7: Links and Navigation'
b'Imperative navigation'
b'Summary'
8: Lazy Loading
b'Chapter 8: Lazy Loading'
b'Example'
b'Lazy loading'
b'Deep linking'
b'Sync link generation'
b'Navigation is URL-based'
b'Customizing module loader'
b'Preloading modules'
9: Guards
b'Chapter 9: Guards'
b'CanLoad'
b'CanActivate'
b'CanActivateChild'
b'CanDeactivate'
10: Events
b'Chapter 10: Events'
b'Enable tracing'
b'Listening to events'
b'Grouping by navigation ID'
b'Showing spinner'
11: Testing Router
b'Chapter 11: Testing Router'
b'Isolated tests'
b'Shallow testing'
b'Integration testing'
b'Summary'
12: Configuration
b'Chapter 12: Configuration'
b'Importing RouterModule'
b'Configuring router service'
b'Disable initial navigation'
b'Custom error handler'
appA: Appendix A: Fin
b'Chapter Appendix A: Fin'
b'Bug reports'
Chapter 1. What Do Routers Do?
Before we jump into the specifics of the Angular router, let's talk about what
routers do in general.

As you know, an Angular application is a tree of components. Some of these


components are reusable UI components (for example, list and table), and
some are application components, which represent screens or some logical
parts of the application. The router cares about application components, or, to
be more specific, about their arrangements. Let's call such component
arrangements router states. So a router state defines what is visible on the
screen.

Note
A router state is an arrangement of application components that
defines what is visible on the screen.
Router configuration

The router configuration defines all the potential router states of the
application. Let's look at an example:
[
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
]
},
{
path: 'compose',
component: ComposeCmp,
outlet: 'popup'
},
{
path: 'message/:id',
component: PopupMessageCmp,
outlet: 'popup'
}
]

Don't worry about understanding all the details. I will cover them in later
chapters. For now, let's depict the configuration as follows:
As you can see the router configuration is a tree, with every node representing
a route. Some nodes have components associated with them, some do not. We
also use color to designate different outlets, where an outlet is a location in the
component tree where a component is...
Router state

A router state is a subtree of the configuration tree. For instance, the example
below has ConversationsCmp activated. We say activated instead of
instantiated as a component can be instantiated only once but activated
multiple times (any time its route's parameters change):

Not all subtrees of the configuration tree are valid router states. If a node has
multiple children of the same color, i.e., of the same outlet name, only one of
them can be active at a time. For instance, ComposeCmp and PopupMessageCmp
cannot be displayed together, but ConversationsCmp and PopupMessageCmp
can. Stands to reason, an outlet is nothing but a location in the DOM where a
component is placed. So we cannot place more than one component into the
same location at the same time.
Navigation

The router's primary job is to manage navigation between states, which


includes updating the component tree.

Note
Navigation is the act of transitioning from one router state to
another.

To see how it works, let's look at the following example. Say we perform a
navigation from the state above to this one:
Because ConversationsCmp is no longer active, the router will remove it.
Then, it will instantiate ConversationCmp with MessagesCmp in it, with
ComposeCmp displayed as a popup.
Summary

That's it. The router simply allows us to express all the potential states which
our application can be in, and provides a mechanism for navigating from one
state to another. The devil, of course, is in the implementation details, but
understanding this mental model is crucial for understanding the
implementation.
Isn't it all about the URL?

The URL bar provides a huge advantage for web applications over native
ones. It allows us to reference states, bookmark them, and share them with our
friends. In a well-behaved web application, any application state transition
results in a URL change, and any URL change results in a state transition. In
other words, a URL is nothing but a serialized router state. The Angular router
takes care of managing the URL to make sure that it is always in-sync with the
router state.
Chapter 2. Overview
Now that we have learned what routers do in general, it is time to talk about
the Angular router.

The Angular router takes a URL, then does the following:

1. Applying redirects.

2. Recognizing router states.

3. Running guards and resolving data.

4. Activating all the needed components.

5. Managing navigation.
Most of it happens behind the scenes, and, usually, we do not need to worry
about it. But remember, the purpose of this book is to teach you how to
configure the router to handle any crazy requirement your application might
have. So let's get on it!
URL format

Since I will use a lot of URLs in the following examples, let's quickly look at
the URL formats:

/inbox/33(popup:compose)

/inbox/33;open=true/messages/44

As you can see, the router uses parentheses to serialize secondary segments
(for example, popup:compose), the colon syntax to specify the outlet, and the
;parameter=value syntax (for example, open=true) to specify route specific
parameters.

In the following examples we assume that we have given the following


configuration to the router, and we are navigating to
/inbox/33/messages/44:

[
{ path: '', pathMatch: 'full', redirectTo: '/inbox' },
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
]
},
{
path: 'compose',
component: ComposeCmp,
outlet: 'popup'
},
{
path: 'message/:id',
component: PopupMessageCmp,
outlet: 'popup'
...
Applying redirects

The router gets a URL from the user, either when she clicks on a link or

What is a redirect?

Note
A redirect is a substitution of a URL segment. Redirects can either

The provided configuration has only one redirect rule: { path: '', pathM

Since we are navigating to /inbox/33/messages/44 and not /, the router w


Recognizing states

Next, the router will derive a router state from the URL. To understand

The router goes through the array of routes, one by one, checking if the

If the taken path through the configuration does not "consume" the whole
Running guards

At this stage we have a future router state. Next, the router will check
Resolving data

After the router has run the guards, it will resolve the data. To see ho

[
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp,
resolve: {
conversations: ConversationsResolver
}
}
]
}
]

Where ConversationsResolver is defined as follows:

@Injectable()
class ConversationsResolver implements Resolve<any> {
constructor(private repo: ConversationsRepo, private currentUser: User

resolve(route: ActivatedRouteSnapshot, state: RouteStateSnapshot):


Promise<Conversation[]> {
return this.repo.fetchAll(route.params['folder'], this.currentUser);
}
}

Finally, we need to register ConversationsResolver when bootstrapping ou

@NgModule({
//...
providers: [ConversationsResolver],
bootstrap: [MailAppCmp]
})
class MailModule {
}

platformBrowserDynamic().bootstrapModule(MailModule);

Now when navigating to /inbox, the router will create a router state, wi
Activating components

At this point, we have a router state. The router can now activate this

To understand how it works, let's take a look at how we use router outle

The root component of the application has two outlets: primary and popup

@Component({
template: `
...
<router-outlet></router-outlet>

...
<router-outlet name="popup"></router-outlet>
`
})
class MailAppCmp {
}

Other components, such as ConversationCmp, have only one:

@Component({
template: `
...
<router-outlet></router-outlet>
...
`
})
class ConversationCmp {
}

Other components, such as ConversationCmp, have only one:

@Component({
template: `
...
<router-outlet></router-outlet>
...
`
})
class ConversationCmp {
}

Now imagine we are navigating to /inbox/33/messages/44(popup:compose)

That's what the router will do. First, it will instantiate ConversationCm
Navigation

So at this point the router has created a router state and instantiated

Imperative navigation

To navigate imperatively, inject the Router service and call navigate

@Component({...})
class MessageCmp {
public id: string;
constructor(private route: ActivatedRoute, private router: Router) {
route.params.subscribe(_ => this.id = _.id);
}

openPopup(e) {
this.router.navigate([{outlets: {popup: ['message',
this.id]}}]).then(_ => {
// navigation is done
});
}
}
RouterLink
Another way to navigate around is by using the RouterLink directive:

@Component({
template: `
<a [routerLink]="['/', {outlets: {popup: ['message',
this.id]}}]">Edit</a>
`
})
class MessageCmp {
public id: string;
constructor(private route: ActivatedRoute) {
route.params.subscribe(_ => this.id = _.id);
}
}

This directive will also update the


Summary

Let's look at all the operations of the Angular router one more time:

When the browser is loading /inbox/33/messages/44(popup:compose)


Next, the router will instantiate the conversation and message components
Now, let's say the message component has the following link in its templ

<a [routerLink]="[{outlets: {popup: ['message', this.id]}}]">Edit</a>

The router link directive will take the array and will set the

/inbox/33/messages/44(popup:message/44). Now, the user triggers a naviga

That was intense-a lot of information!...


Chapter 3. URLs
When using the Angular router, a URL is just a serialized router state.
Simple URL

Let's start with this simple URL /inbox/33.

This is how the router will encode the information about this URL:

const url: UrlSegment[] = [


{path: 'inbox', params: {}},
{path: '33', params: {}}
];

Where UrlSegment is defined as follows:

interface UrlSegment {
path: string;
params: {[name:string]:string};
}

We can use the ActivatedRoute object to get the URL segments consumed by

class MessageCmp {
constructor(r: ActivatedRoute) {
r.url.forEach((u: UrlSegment[]) => {
//...
});
}
}
Params

Let's soup it up a little by adding matrix or route-specific parameters,

[
{path: 'inbox', params: {a: 'v1'}},
{path: '33', params: {b1: 'v1', b2: 'v2'}}
]

Matrix parameters are scoped to a particular URL segment. Because of thi


Query params

Sometimes, however, you want to share some parameters across many activa

class ConversationCmp {
constructor(r: ActivateRoute) {
r.queryParams.forEach((p) => {
const token = p['token']
});
}
}

Since query parameters are not scoped, they should not be used to store

The fragment (for example, /inbox/33#fragment) is similar to query param

class ConversationCmp {
constructor(r: ActivatedRoute) {
r.fragment.forEach((f:string) => {

});
}
}
Secondary segments

Since a router state is a tree, and the URL is nothing but a serialized

/inbox/33(popup:message/44)

Here the root has two children inbox and message:

The router encodes multiple secondary children using a //.

/inbox/33(popup:message/44//help:overview)

If some other segment, not the root, has multiple children, the router w

/inbox/33/(messages/44//side:help)
Chapter 4. URL Matching
At the core of the Angular router lies a powerful URL matching engine, w

Once again, let's use this configuration:

[
{ path: '', pathMatch: 'full', redirectTo: '/inbox' },
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
]
},
{
path: 'compose',
component: ComposeCmp,
outlet: 'popup'
},
{
path: 'message/:id',
component: PopupMessageCmp,
outlet: 'popup'
}
]

First, note that every route is defined by two key parts:

How it matches the URL


What it does once the URL is matched

Is is important that the second concern, the action, does not affect the

And let's say we are navigating to /inbox/33/messages/44.

This is how matching works:

The router goes through the provided array of routes, one by one, checkin

Here it checks that /inbox/33/messages/44 starts with :folder. It does. S

The router will check that


33/messages/44
starts with '', and it does, since we interpret every string to begin wi
path: ':id'
.

This one will work. The id parameter will be set to 33, and finally the
messages/:id
route will be matched, and the second id parameter will be set to
Backtracking

Let's illustrate backtracking one more time. If the taken path through t

Say we have this configuration:

[
{
path: 'a',
children: [
{
path: 'b',
component: ComponentB
}
]
},
{
path: ':folder',
children: [
{
path: 'c',
component: ComponentC
}
]
}
]

When navigating to
/a/c
, the router will start with the first route. The /a/c URL starts with
path: 'a'
, so the router will try to match
/c
with
b
. Because it is unable to do that, it will backtrack and will match
a
with :folder, and then
c
with
c
.
Depth-first

The router doesn't try to find the best match, that is, it does not have

[
{
path: ':folder',
children: [
{
path: 'b',
component: ComponentB1
}
]
},
{
path: 'a',
children: [
{
path: 'b',
component: ComponentB2
}
]
}
]

When navigating to
/a/b
, the first route will be matched even though the second one appears to
Wildcards

We have seen that path expressions can contain two types of segments:

constant segments (for example,


path: 'messages'
)

variable segments (for example,


path: ':folder'
)

Using just these two we can handle most use cases. Sometimes, however, w
{ path: '**', redirectTo: '/notfound' }
that will match any URL that we were not able to match otherwise and wi
NotFoundCmp
.

[
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
]
}
{ path: '**', component: NotFoundCmp }
]

The wildcard route will "consume" all the URL segments, so NotFoundCmp
Empty-path routes

If you look at our configuration once again, you will see that some rout

[
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
}
]
}
]

By setting path to an empty string, we create a route that instantiates


/inbox
, the router will do the following:

First, it will check that


/inbox
starts with
:folder, which it does. So it will take what is left of the URL, which i
Empty path routes can have children, and, in general, behave like normal
/inbox;expand=true
will result in the router state where two activated routes have the exp
Matching strategies

By default the router checks if the URL starts with the path property of

// identical to {path: 'a', component: ComponentA}

{path : 'a' , pathMatch:'prefix',component: ComponentA}

The router supports a second matching strategy-full, which checks that t

[
{ path: '', redirectTo: '/inbox' },
{
path: ':folder',
children: [
...
]
}
]

Because the default matching strategy is prefix, and any URL starts with
/inbox, the router will apply the first redirect. Our intent, however, i
. Now, if we change the matching strategy to full, the router will apply
Componentless routes

Most of the routes in the configuration have either the redirectTo


path: ':folder'
route in the configuration below:

{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
]
}

We called such routes componentless routes. Their main purpose is to con

The parameters captured by a componentless route will be merged into the

This particular example could have been easily rewritten as follows:

[
{
path: ':folder',
component: ConversationsCmp
},
{
path: ':folder/:id',
...
Composing componentless and empty-path routes

What is really exciting about all these features is that they compose ve

Let me give you an example. We have learned that we can use empty-path r

[
{
path: '',
canActivate: [CanActivateMessagesAndContacts],
resolve: {
token: TokenNeededForBothMessagsAndContacts
},

children: [
{
path: 'messages',
component: MesssagesCmp
},
{
path: 'contacts',
component: ContactsCmp
}
]
}
]

Here we have defined a route that neither consumes any URL segments nor
Summary

We've learned a lot! First, we talked about how the router does matching
Chapter 5. Redirects

Using redirects we can transform the URL before the router creates a rou
Local and absolute redirects

Redirects can be local and absolute. Local redirects replace a single UR


If the redirectTo value starts with a /, then it is an absolute redirec

[
{
path: ':folder/:id',
component: ConversationCmp,
children: [
{
path: 'contacts/:name',
redirectTo: '/contacts/:name'
},
{
path: 'legacy/messages/:id',
redirectTo: 'messages/:id'
},
{
path: 'messages/:id',
component: MessageCmp
}
]
},
{
path: 'contacts/:name',
component: ContactCmp
}
]

When navigating to /inbox/33/legacy/messages/44, the router will apply t


/contacts/jim.

Note that a redirectTo value can contain variable segments captured by..
One redirect at a time

You can set up redirects at different levels of your router configuratio

[
{
path: 'legacy/:folder/:id',
redirectTo: ':folder/:id'
},
{
path: ':folder/:id',
component: ConversationCmp,
children: [
{
path: 'legacy/messages/:id',
redirectTo: 'messages/:id'
},
{
path: 'messages/:id',
component: MessageCmp
}
]
}
]

When navigating to
/legacy/inbox/33/legacy/messages/44, the router will first apply the out
/inbox/33/legacy/messages/44. After that the router will start processin

One constraint the router imposes is at any level of the configuration t

For instance, say we have this configuration:

[
{
path: 'legacy/messages/:id',
redirectTo: 'messages/:id'
},
{
path: 'messages/:id',
redirectTo:...
Using redirects to normalize URLs

We often use redirects for URL normalization. Say we want both


mail-app.vsavkin.com
and
mail-app.vsavkin.com/inbox
render the same UI. We can use a redirect to achieve that:

[
{ path: '', pathMatch: 'full', redirectTo: '/inbox' },
{
path: ':folder',
children: [
...
]
}
]

We can also use redirects to implement a not-found page:

[
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
{ path: '**', redirectTo: '/notfound/conversation' }
]
}
{ path: 'notfound/:objectType', component: NotFoundCmp }
]
Using redirects to enable refactoring

Another big use case for using redirects is to enable large scale refact
Chapter 6. Router State

During a navigation, after redirects have been applied, the router creat
RouterStateSnapshot
.
What is RouterStateSnapshot?

interface RouterStateSnapshot {
root: ActivatedRouteSnapshot;
}

interface ActivatedRouteSnapshot {
url: UrlSegment[];
params: {[name:string]:string};
data: {[name:string]:any};

queryParams: {[name:string]:string};
fragment: string;

root: ActivatedRouteSnapshot;
parent: ActivatedRouteSnapshot;
firstchild: ActivatedRouteSnapshot;
children: ActivatedRouteSnapshot[];
}

As you can see


RouterStateSnapshot
is a tree of activated route snapshots. Every node in this tree knows a

[
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{
path: 'messages',
component: MessagesCmp
},
{
path: 'messages/:id',
component: MessageCmp,
resolve: {
message: MessageResolver
}
...
Accessing snapshots

The router exposes parameters and data as observables, which is convenie

@Component({...})
class MessageCmp {
constructor(r: ActivatedRoute) {
r.url.subscribe(() => {
r.snapshot; // any time url changes, this callback is fired
});
}
}
ActivatedRoute

The ActivatedRoute
interface provides access to the url, params, data, queryParams, and fr

URL changes are the source of any changes in a route. And it has to be t

Any time the URL changes, the router derives a new set of parameters fro

Next, the router invokes the route's data resolvers and combines the res
Query params and fragment

In opposite to other observables, that are scoped to a particular route,

@Component({...})
class MessageCmp {
debug: Observable <string>;
fragment: Observable <string>;

constructor(route: ActivatedRoute) {
this.debug = route.queryParams.map(p => p.debug);
this.fragment = route.fragment;
}
}
Chapter 7. Links and Navigation

The primary job of the router is to manage navigation between different

As before, let's assume the following configuration:

[
{ path: '', pathMatch: 'full', redirectTo: '/inbox' },
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
]
},
{
path: 'compose',
component: ComposeCmp,
outlet: 'popup'
},
{
path: 'message/:id',
component: PopupMessageCmp,
outlet: 'popup'
}
]
Imperative navigation

To navigate imperatively, inject the Router service and call


navigate or navigateByUrl on it. Why two methods and not one?

Using router.navigateByUrl is similar to changing the location bar direc

To see the difference clearly, imagine that the current URL is


/inbox/11/messages/22(popup:compose).

With this URL, calling


router.navigateByUrl('/inbox/33/messages/44') will result in
/inbox/33/messages/44, and calling
router.navigate('/inbox/33/messages/44') will result in /inbox/33/messag

Router.navigate
Let's see what we can do with
router.navigate
.

Passing an array or a string

Passing a string is sugar for passing an array with a single element.

router.navigate('/inbox/33/messages/44')

is sugar for

router.navigate(['/inbox/33/messages/44'])

which itself is sugar for

router.navigate(['/inbox', 33, 'messages', 44])

Passing matrix params

router.navigate([
'/inbox', 33, {details: true}, 'messages', 44, {mode: 'preview'}
])

navigates to
Summary

That's a lot of information! So let's recap.

First, we established that the primary job of the router is to manage na


Chapter 8. Lazy Loading
Angular is built with the focus on mobile. That's why we put a lot of ef

At some point, however, our application will be big enough, that even wi

Lazy loading speeds up our application load time by splitting it into mu


Example

We are going to continue using the mail app example, but this time we wi

Let's start by sketching out our application:

main.ts:
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'

@Component({...}) class MailAppCmp {}


@Component({...}) class ConversationsCmp {}
@Component({...}) class ConversationCmp {}

@Component({...}) class ContactsCmp {}


@Component({...}) class ContactCmp {}

const ROUTES = [
{
path: 'contacts',
children: [
{ path: '', component: ContactsCmp },
{ path: ':id', component: ContactCmp }
]
},
{
path: ':folder',
children: [
{ path: '', component: ConversationsCmp },
{ path: ':id', component: ConversationCmp, children: [...]}
]
}
];

@NgModule({
//...
bootstrap: [MailAppCmp],
imports: [RouterModule.forRoot(ROUTES)]
})
class MailModule...
Lazy loading

We start with extracting all the contacts-related components and routes

contacts.ts:
import {NgModule, Component} from '@angular/core';
import {RouterModule} from '@angular/router';

@Component({...}) class ContactsComponent {}


@Component({...}) class ContactComponent {}

const ROUTES = [
{ path: '', component: ContactsComponent },
{ path: ':id', component: ContactComponent }
];

@NgModule({
imports: [RouterModule.forChild(ROUTES)]
})
class ContactsModule {}

In Angular an ng module is part of an application that can be bundled an

Referring to lazily-loaded module


Now, after extracting the contacts module, we need to update the main mo

const ROUTES = [
{
path: 'contacts',
loadChildren: 'contacts.bundle.js',
},
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [...]
}
]
...
Deep linking

But it gets better! The router also supports deep linking into lazily-lo

To see what I mean imagine that the contacts module lazy loads another o

contacts.ts:
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';

@Component({...}) class AllContactsComponent {}


@Component({...}) class ContactComponent {}

const ROUTES = [
{ path: '', component: ContactsComponent },
{ path: ':id', component: ContactComponent, loadChildren: 'details.bun
];

@NgModule({
imports: [RouterModule.forChild(ROUTES)]
})
class ContactsModule {}

details.ts:

@Component({...}) class BriefComponent {}


@Component({...}) class DetailComponent {}

const ROUTES = [
{ path: '', component: BriefDetailComponent },
{ path: 'detail', component: DetailComponent },
];

@NgModule({
imports: [RouterModule.forChild(ROUTES)]
})
class DetailModule {}
Imagine we have the following link in the main section or our application

<a [routerLink]="['/contacts', id, 'detail', {full: true}]">


Show Contact Detail
</a>

When clicking on the link, the router will...


Sync link generation

The RouterLink directive does more than handle clicks. It also sets the
<a>
tag's href attribute, so the user can right-click and "Open link in a n

For instance, the directive above will set the anchor's href attribute t
Navigation is URL-based

Deep linking into lazily-loaded modules and synchronous link generation


['/contacts', id, 'detail', {full: true}]
) is just an array of URL segments. In other words, link generation is p

This is an important design decision we have made early on because we kn


Customizing module loader

The built-in application module loader uses SystemJS. But we can provide

@NgModule({
//...
bootstrap: [MailAppCmp],
imports: [RouterModule.forRoot(ROUTES)],
providers: [{provide: NgModuleFactoryLoader, useClass: MyCustomLoader}
})
class MailModule {}

platformBrowserDynamic().bootstrapModule(MailModule);

You can look at


SystemJsNgModuleLoader
to see an example of a module loader.
Finally, you don't have to use the loader at all. Instead, you can prov

{
path: 'contacts',
loadChildren: () => System.import('somePath'),
}
Preloading modules

Lazy loading speeds up our application load time by splitting it into mu

The issue with lazy loading, of course, is that when the user navigates

To fix this problem we have added support for preloading. Now the router

This is how it works.

First, we load the initial bundle, which contains only the components we

Then, we bootstrap the application using this small bundle.


At this point the application is running, so the user can start interacti

Finally, when she clicks on a link going to a lazy-loadable module, the n


We got the best of both worlds: the...
Chapter 9. Guards

The router uses guards to make sure that navigation is permitted, which

There are four types of guards:


canLoad
,
canActivate
,
canActivateChild
, and
canDeactivate
. In this chapter we will look at each of them in detail.
CanLoad

Sometimes, for security reasons, we do not want the user to be able to e


canLoad
guard is for. If a canLoad
guard returns false, the router will not load the bundle.
Let's take this configuration from the previous chapter and set up a
canLoad
guard to restrict access to contacts:

const ROUTES = [
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [...]
}
]
},
{
path: 'contacts',
canLoad: [CanLoadContacts],
loadChildren: 'contacts.bundle.js'
}
];

Where the CanLoadContacts class is defined like this:

@Injectable()
class CanLoadContacts implements CanLoad {
constructor(private permissions: Permissions,
private currentUser: UserToken) {}

canLoad(route: Route): boolean {


if...
CanActivate

The
canActivate
guard is the default mechanism of adding permissions to the application

const ROUTES = [
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [...]
}
]
},
{
path: 'contacts',
canActivate: [CanActivateContacts],
children: [
{ path: '', component: ContactsCmp },
{ path: ':id', component: ContactCmp }
]
}
];

Where
CanActivateContacts
is defined as shown in the following code:

@Injectable()
class CanActivateContacts implements CanActivate {
constructor(private permissions: Permissions,
private currentUser: UserToken) {}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot


boolean {
if (route.routerConfig.path ===...
CanActivateChild

The canActivateChild guard is similar to canActivate, except that it is

Imagine a function that takes a URL and decides if the current user shou
canActivateChild
:

{
path: '',
canActivateChild: [AllowUrl],
children: [
{
path: ':folder',
children: [
{ path: '', component: ConversationsCmp },
{ path: ':id', component: ConversationCmp, children: [...]}
]
},
{
path: 'contacts',
children: [
{ path: '', component: ContactsCmp },
{ path: ':id', component: ContactCmp }
]
}
]
}

Where
AllowUrl
is defined like this:

@Injectable()
class AllowUrl implements CanActivateChild {
constructor(private permissions: Permissions,
private currentUser: UserToken) {}

...
CanDeactivate

The canDeactivate guard is different from the rest. Its main purpose is

To illustrate this let's change the application to ask for confirmation

[
{
path: 'compose',
component: ComposeCmp,
canDeactivate: [SaveChangesGuard]
outlet: 'popup'
}
]

Where
SaveChangesGuard
is defined as follows:

class SaveChangesGuard implements CanDeactivate<ComposeCmp> {


constructor(private dialogs: Dialogs) {}

canDeactivate(component: ComposeCmp, route: ActivatedRouteSnapshot,


state: RouterStateSnapshot): Promise<boolean> {
if (component.unsavedChanges) {
return this.dialogs.unsavedChangesConfirmationDialog();
} else {
return Promise.resolve(true);
}
}
}

The SaveChangesGuard class asks the user to confirm the navigation becau
Chapter 10. Events
The router provides an observable of navigation events. Any time the use
Enable tracing

The very first thing we can do during development to start troubleshooti

@NgModule({
import: [RouterModule.forRoot(routes, {enableTracing: true})]
})
class MailModule {
}
platformBrowserDynamic().bootstrapModule(MailModule);
Listening to events

To listen to events, inject the router service and subscribe to the even

class MailAppCmp {
constructor(r: Router) {
r.events.subscribe(e => {
console.log("event", e);
});
}
}

For instance, let's say we want to update the title any time the user su

class MailAppCmp {
constructor(r: Router, titleService: TitleService) {
r.events.filter(e => e instanceof NavigationEnd).subscribe(e => {
titleService.updateTitleForUrl(e.url);
});
}
}
Grouping by navigation ID

The router assigns a unique ID to every navigation, which we can use to

1. Let's start with defining a few helpers used for identifying the sta

function isStart(e: Event): boolean {


return e instanceof NavigationStart;
}

function isEnd(e: Event): boolean {


return e instanceof NavigationEnd ||
e instanceof NavigationCancel ||
e instanceof NavigationError;
}

2. Next, let's define a combinator that will take an observable of all

function collectAllEventsForNavigation(obs: Observable):


Observable<Event[]>{
let observer: Observer<Event[]>;
const events = [];
const sub = obs.subscribe(e => {
events.push(e);
if (isEnd(e)) {
observer.next(events);
observer.complete();
}
});
return new Observable<Event[]>(o => observer = o);
}

3. Now equipped with these helpers, we can implement the desired functi

class MailAppCmp {
constructor(r: Router) {
r.events.

//...
Showing spinner

In the last example let's use the events observable to show the spinner

class MailAppCmp {
constructor(r: Router, spinner: SpinnerService) {
r.events.
// Fitlers only starts and ends.
filter(e => isStart(e) || isEnd(e)).

// Returns Observable<boolean>.
map(e => isStart(e)).

// Skips duplicates, so two 'true' values are never emitted in a r


distinctUntilChanged().

subscribe(showSpinner => {
if (showSpinner) {
spinner.show();
} else {
spinner.hide();
}
});
}
}
Chapter 11. Testing Router
Everything in Angular is testable, and the router isn't an exception. In
Isolated tests

It is often useful to test complex components without rendering them. To

@Component({moduleId: module.id, templateUrl: 'compose.html'})


class ComposeCmp {
form = new FormGroup({
title: new FormControl('', Validators.required),
body: new FormControl('')
});

constructor(private route: ActivatedRoute,


private currentTime: CurrentTime,
private actions: Actions) {}

onSubmit() {
const routerStateRoot = this.route.snapshot.root;
const conversationRoute = routerStateRoot.firstChild;
const conversationId = +conversationRoute.params['id'];

const payload = Object.assign({},


this.form.value,
{createdAt: this.currentTime()});

this.actions.next({
type: 'reply',
conversationId: conversationId,
payload: payload
});
}
}

Here's the compose.html file:

<form [formGroup]="form" (ngSubmit)="onSubmit()">


<div>
Title: <md-input formControlName="title" required></md-input>
<span *ngIf="form.get('title').touched &&
...
Shallow testing

Testing component classes without rendering their templates works in cer


Let's see this approach in action in the following code:

@Component(
{moduleId: module.id, templateUrl: 'conversations.html'})
export class ConversationsCmp {
folder: Observable<string>;
conversations: Observable<Conversation[]>;

constructor(route: ActivatedRoute) {
this.folder = route.params.pluck<string>('folder');
this.conversations = route.data.pluck<Conversation[]>('conversations
}
}

This constructor, although short, may look a bit funky if you are not fa
folder
out of the params object, which is equivalent to
route.params.map(p => p['folder'])
. Second, we pluck out conversations.

In the template we use the async pipe to bind the two...


Integration testing

Finally, we can always write an integration test that will exercise the

describe('integration specs', () => {


const initialData = {
conversations: [
{id: 1, title: 'The Myth of Sisyphus'},
{id: 2, title: 'The Nicomachean Ethics'}
],
messages: [
{id: 1, conversationId: 1, text: 'The Path of the Absurd Man'}
]
};

beforeEach(async(() => {
TestBed.configureTestingModule({
// MailModule is an NgModule that contains all application
// components and the router configuration

// RouterTestingModule overrides the router and location provider


// to make them test-friendly.
imports: [MailModule, RouterTestingModule],

providers: [
{ provide: 'initialData', useValue: initialData}
]
});
TestBed.compileComponents();
}));

it('should navigate to a conversation', fakeAsync(() => {


// get the router from the testing NgModule
...
Summary

In this chapter we looked at three ways to test Angular components: isol


Chapter 12. Configuration
In this last chapter we will look at configuring the router.
Importing RouterModule

We configure the router by importing RouterModule, and there are two way
RouterModule.forRoot
and
RouterModule.forChild
.

RouterModule.forRoot
creates a module that contains all the router directives, the given rou
RouterModule.forChild
creates a module that contains all the directives and the given routes,

The router library provides two ways to configure the module because it
forChild
to configure every lazy-loaded child module, and
forRoot
at the root of the application. forChild
can be called multiple times, whereas
forRoot
can be called only once.

@NgModule({
imports: [RouterModule.forRoot(ROUTES)]
})
class MailModule {}

@NgModule({
imports: [RouterModule.forChild(ROUTES)]
})
class ContactsModule {}
Configuring router service

We can configure the router service by passing the following options to


RouterModule.forRoot
:

The enableTracing option makes the router log all its internal event

The useHash option enables the location strategy that uses the URL f

The initialNavigation option disables the initial navigation

The errorHandler option provides a custom error handler

Let's look at each of them in detail.

Enable tracing
Setting
enableTracing
to true is a great way to learn how the router works as shown in the fo

@NgModule({
imports: [RouterModule.forRoot(ROUTES, {enableTracing: true})]
})
class MailModule {}

With this option set, the router will log every internal event to the yo

Router Event: NavigationStart


NavigationStart(id: 1, url: '/inbox')

Router Event: RoutesRecognized


RoutesRecognized(id: 1, url: '/inbox', urlAfterRedirects: '/inbox', sta
Route(url:'', path:'') {
Route(url:'inbox', path:':folder') {
Route(url:'', path:'')
}
}
)

...
Disable initial navigation

By default,
RouterModule.forRoot
will trigger the initial navigation: the router will read the current U

@NgModule({
imports: [RouterModule.forRoot(ROUTES, {initialNavigation: false})],
})
class MailModule {
constructor(router: Router) {
router.navigateByUrl("/fixedUrl");
}
}
Custom error handler

Every navigation will either succeed, will be cancelled, or will error.

The router.events observable will emit:

NavigationStart
when navigation stars

NavigationEnd
when navigation succeeds

NavigationCancel
when navigation is canceled

NavigationError
when navigation fails

All of them contain the id property we can use to group the events assoc

If we call
router.navigate
or
router.navigateByUrl
directly, we will get a promise that:

will be resolved with true if the navigation succeeds

will be resolved with false if the navigation gets cancelled

will be rejected if the navigation fails

Navigation fails when the router cannot match the URL or an exception is

function treatCertainErrorsAsCancelations(error) {
if (error isntanceof CancelException) {
return false; //cancelation
} else {
throw...
Appendix A. Fin
This is the end of this short book on the Angular Router. We have learne
Bug reports

If you find any typos, or have suggestions on how to improve the book, p

Example app
Throughout the book I used the same application in all the examples. You
MailApp
.

You might also like