Angular Router Guide
Angular Router Guide
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
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.
1. Applying redirects.
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.
[
{ 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
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
}
}
]
}
]
@Injectable()
class ConversationsResolver implements Resolve<any> {
constructor(private repo: ConversationsRepo, private currentUser: User
@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 {
}
@Component({
template: `
...
<router-outlet></router-outlet>
...
`
})
class ConversationCmp {
}
@Component({
template: `
...
<router-outlet></router-outlet>
...
`
})
class ConversationCmp {
}
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
@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);
}
}
Let's look at all the operations of the Angular router one more time:
The router link directive will take the array and will set the
This is how the router will encode the information about this URL:
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
[
{path: 'inbox', params: {a: 'v1'}},
{path: '33', params: {b1: 'v1', b2: 'v2'}}
]
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
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)
/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
[
{ 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'
}
]
Is is important that the second concern, the action, does not affect the
The router goes through the provided array of routes, one by one, checkin
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
[
{
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:
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 default the router checks if the URL starts with the path property of
[
{ 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
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [
{ path: 'messages', component: MessagesCmp },
{ path: 'messages/:id', component: MessageCmp }
]
}
]
}
[
{
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
[
{
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
}
]
Note that a redirectTo value can contain variable segments captured by..
One redirect at a time
[
{
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
[
{
path: 'legacy/messages/:id',
redirectTo: 'messages/:id'
},
{
path: 'messages/:id',
redirectTo:...
Using redirects to normalize URLs
[
{ path: '', pathMatch: 'full', redirectTo: '/inbox' },
{
path: ':folder',
children: [
...
]
}
]
[
{
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[];
}
[
{
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
@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
@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
[
{ 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
Router.navigate
Let's see what we can do with
router.navigate
.
router.navigate('/inbox/33/messages/44')
is sugar for
router.navigate(['/inbox/33/messages/44'])
router.navigate([
'/inbox', 33, {details: true}, 'messages', 44, {mode: 'preview'}
])
navigates to
Summary
At some point, however, our application will be big enough, that even wi
We are going to continue using the mail app example, but this time we wi
main.ts:
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'
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
contacts.ts:
import {NgModule, Component} from '@angular/core';
import {RouterModule} from '@angular/router';
const ROUTES = [
{ path: '', component: ContactsComponent },
{ path: ':id', component: ContactComponent }
];
@NgModule({
imports: [RouterModule.forChild(ROUTES)]
})
class ContactsModule {}
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';
const ROUTES = [
{ path: '', component: ContactsComponent },
{ path: ':id', component: ContactComponent, loadChildren: 'details.bun
];
@NgModule({
imports: [RouterModule.forChild(ROUTES)]
})
class ContactsModule {}
details.ts:
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
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
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);
{
path: 'contacts',
loadChildren: () => System.import('somePath'),
}
Preloading modules
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
First, we load the initial bundle, which contains only the components we
The router uses guards to make sure that navigation is permitted, which
const ROUTES = [
{
path: ':folder',
children: [
{
path: '',
component: ConversationsCmp
},
{
path: ':id',
component: ConversationCmp,
children: [...]
}
]
},
{
path: 'contacts',
canLoad: [CanLoadContacts],
loadChildren: 'contacts.bundle.js'
}
];
@Injectable()
class CanLoadContacts implements CanLoad {
constructor(private permissions: Permissions,
private currentUser: UserToken) {}
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) {}
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
[
{
path: 'compose',
component: ComposeCmp,
canDeactivate: [SaveChangesGuard]
outlet: 'popup'
}
]
Where
SaveChangesGuard
is defined as follows:
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
@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
1. Let's start with defining a few helpers used for identifying the sta
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)).
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
onSubmit() {
const routerStateRoot = this.route.snapshot.root;
const conversationRoute = routerStateRoot.firstChild;
const conversationId = +conversationRoute.params['id'];
this.actions.next({
type: 'reply',
conversationId: conversationId,
payload: payload
});
}
}
@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.
Finally, we can always write an integration test that will exercise the
beforeEach(async(() => {
TestBed.configureTestingModule({
// MailModule is an NgModule that contains all application
// components and the router configuration
providers: [
{ provide: 'initialData', useValue: initialData}
]
});
TestBed.compileComponents();
}));
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
The enableTracing option makes the router log all its internal event
The useHash option enables the location strategy that uses the URL f
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
...
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
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:
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
.