docs(router): add query params milestone

Additional fixes suggested by recent issues.
closes #637
This commit is contained in:
Ward Bell 2015-12-31 20:04:22 -08:00
parent 8e3e0834b5
commit af458737be
20 changed files with 679 additions and 167 deletions

View File

@ -6,11 +6,22 @@ import {Component} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {CrisisListComponent} from './crisis-list.component';
// #enddocregion
/*
// Apparent Milestone 2 imports
// #docregion
// #docregion hero-import
import {HeroListComponent} from './heroes/hero-list.component';
import {HeroDetailComponent} from './heroes/hero-detail.component';
import {HeroService} from './heroes/hero.service';
// #enddocregion hero-import
// #enddocregion
*/
// Actual Milestone 2 imports
import {HeroListComponent} from './heroes/hero-list.component.1';
import {HeroDetailComponent} from './heroes/hero-detail.component.1';
import {HeroService} from './heroes/hero.service';
// #docregion
@Component({
selector: 'my-app',
@ -45,6 +56,3 @@ import {HeroService} from './heroes/hero.service';
export class AppComponent { }
// #enddocregion route-config
// #enddocregion
// #docregion child-router-link
// #enddocregion child-router-link

View File

@ -1,11 +1,10 @@
// #docplaster
// #docregion
import {Component} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {CrisisCenterComponent} from './crisis-center/crisis-center.component';
import {HeroListComponent} from './heroes/hero-list.component';
import {HeroDetailComponent} from './heroes/hero-detail.component';
import {CrisisCenterComponent} from './crisis-center/crisis-center.component.1';
import {HeroListComponent} from './heroes/hero-list.component.1';
import {HeroDetailComponent} from './heroes/hero-detail.component.1';
import {DialogService} from './dialog.service';
import {HeroService} from './heroes/hero.service';
@ -36,7 +35,6 @@ import {HeroService} from './heroes/hero.service';
<a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:1}]">Princess Crisis</a>
// #enddocregion princess-anchor
*/
// #docregion
// #docregion template
template: `
<h1 class="title">Component Router</h1>

View File

@ -3,36 +3,30 @@
// Also includes digression on HashPathStrategy (not used in the final app)
// #docplaster
// #docregion v2
// #docregion
import {bootstrap} from 'angular2/platform/browser';
import {ROUTER_PROVIDERS} from 'angular2/router';
import {AppComponent} from './app.component';
// #enddocregion v2
// Add these symbols to register a `LocationStrategy`
// Add these symbols to override the `LocationStrategy`
import {provide} from 'angular2/core';
import {LocationStrategy,
HashLocationStrategy} from 'angular2/router';
// #enddocregion hash-strategy
// #enddocregion
/* Can't use AppComponent ... but display as if we can
// #docregion v2, hash-strategy
// #docregion
bootstrap(AppComponent, [
// #enddocregion v2, hash-strategy
// #enddocregion
*/
// Actually use the v.2 component
import {AppComponent as ac} from './app.component.2';
bootstrap(ac, [
// #docregion v2, hash-strategy
// #docregion
ROUTER_PROVIDERS,
// #enddocregion v2, hash-strategy
// #docregion hash-strategy
provide(LocationStrategy,
{useClass: HashLocationStrategy}) // ~/#/crisis-center/
// #enddocregion hash-strategy
// #docregion v2, hash-strategy
{useClass: HashLocationStrategy}) // .../#/crisis-center/
]);
// #enddocregion v2, hash-strategy
// #enddocregion

View File

@ -0,0 +1,28 @@
import {Component} from 'angular2/core';
import {RouteConfig, RouterOutlet} from 'angular2/router';
import {CrisisListComponent} from './crisis-list.component.1';
import {CrisisDetailComponent} from './crisis-detail.component.1';
import {CrisisService} from './crisis.service';
// #docregion minus-imports
@Component({
template: `
<h2>CRISIS CENTER</h2>
<router-outlet></router-outlet>
`,
directives: [RouterOutlet],
// #docregion providers
providers: [CrisisService]
// #enddocregion providers
})
// #docregion route-config
@RouteConfig([
// #docregion default-route
{path:'/', name: 'CrisisCenter', component: CrisisListComponent, useAsDefault: true},
// #enddocregion default-route
{path:'/:id', name: 'CrisisDetail', component: CrisisDetailComponent}
])
// #enddocregion route-config
export class CrisisCenterComponent { }
// #enddocregion minus-imports

View File

@ -6,25 +6,17 @@ import {CrisisListComponent} from './crisis-list.component';
import {CrisisDetailComponent} from './crisis-detail.component';
import {CrisisService} from './crisis.service';
// #docregion minus-imports
@Component({
template: `
<h2>CRISIS CENTER</h2>
<router-outlet></router-outlet>
`,
directives: [RouterOutlet],
// #docregion providers
providers: [CrisisService]
// #enddocregion providers
})
// #docregion route-config
@RouteConfig([
// #docregion default-route
{path:'/', name: 'CrisisCenter', component: CrisisListComponent, useAsDefault: true},
// #enddocregion default-route
{path:'/:id', name: 'CrisisDetail', component: CrisisDetailComponent}
])
// #enddocregion route-config
export class CrisisCenterComponent { }
// #enddocregion minus-imports
// #enddocregion

View File

@ -0,0 +1,92 @@
// #docplaster
// #docregion
import {Component, OnInit} from 'angular2/core';
import {Crisis, CrisisService} from './crisis.service';
import {RouteParams, Router} from 'angular2/router';
// #docregion routerCanDeactivate
import {CanDeactivate, ComponentInstruction} from 'angular2/router';
import {DialogService} from '../dialog.service';
// #enddocregion routerCanDeactivate
@Component({
// #docregion template
template: `
<div *ngIf="crisis">
<h3>"{{editName}}"</h3>
<div>
<label>Id: </label>{{crisis.id}}</div>
<div>
<label>Name: </label>
<input [(ngModel)]="editName" placeholder="name"/>
</div>
<button (click)="save()">Save</button>
<button (click)="cancel()">Cancel</button>
</div>
`,
// #enddocregion template
styles: ['input {width: 20em}']
})
// #docregion routerCanDeactivate, cancel-save
export class CrisisDetailComponent implements OnInit, CanDeactivate {
crisis: Crisis;
editName: string;
// #enddocregion routerCanDeactivate, cancel-save
constructor(
private _service: CrisisService,
private _router: Router,
private _routeParams: RouteParams,
private _dialog: DialogService
) { }
// #docregion ngOnInit
ngOnInit() {
let id = +this._routeParams.get('id');
this._service.getCrisis(id).then(crisis => {
if (crisis) {
this.editName = crisis.name;
this.crisis = crisis;
} else { // id not found
this.gotoCrises();
}
});
}
// #enddocregion ngOnInit
// #docregion routerCanDeactivate
routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) : any {
// Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged.
if (!this.crisis || this.crisis.name === this.editName) {
return true;
}
// Otherwise ask the user with the dialog service and return its
// promise which resolves to true or false when the user decides
return this._dialog.confirm('Discard changes?');
}
// #enddocregion routerCanDeactivate
// #docregion cancel-save
cancel() {
this.editName = this.crisis.name;
this.gotoCrises();
}
save() {
this.crisis.name = this.editName;
this.gotoCrises();
}
// #enddocregion cancel-save
// #docregion gotoCrises
gotoCrises() {
// Like <a [routerLink]="['CrisisCenter']">Crisis Center</a
this._router.navigate(['CrisisCenter']);
}
// #enddocregion gotoCrises
// #docregion routerCanDeactivate, cancel-save
}
// #enddocregion routerCanDeactivate, cancel-save
// #enddocregion

View File

@ -4,14 +4,10 @@
import {Component, OnInit} from 'angular2/core';
import {Crisis, CrisisService} from './crisis.service';
import {RouteParams, Router} from 'angular2/router';
// #docregion routerCanDeactivate
import {CanDeactivate, ComponentInstruction} from 'angular2/router';
import {DialogService} from '../dialog.service';
// #enddocregion routerCanDeactivate
@Component({
// #docregion template
template: `
<div *ngIf="crisis">
<h3>"{{editName}}"</h3>
@ -25,16 +21,14 @@ import {DialogService} from '../dialog.service';
<button (click)="cancel()">Cancel</button>
</div>
`,
// #enddocregion template
styles: ['input {width: 20em}']
})
// #docregion routerCanDeactivate, cancel-save
export class CrisisDetailComponent implements OnInit, CanDeactivate {
crisis: Crisis;
editName: string;
// #enddocregion routerCanDeactivate, cancel-save
constructor(
private _service: CrisisService,
private _router: Router,
@ -42,7 +36,6 @@ export class CrisisDetailComponent implements OnInit, CanDeactivate {
private _dialog: DialogService
) { }
// #docregion ngOnInit
ngOnInit() {
let id = +this._routeParams.get('id');
this._service.getCrisis(id).then(crisis => {
@ -54,9 +47,7 @@ export class CrisisDetailComponent implements OnInit, CanDeactivate {
}
});
}
// #enddocregion ngOnInit
// #docregion routerCanDeactivate
routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) : any {
// Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged.
if (!this.crisis || this.crisis.name === this.editName) {
@ -66,9 +57,7 @@ export class CrisisDetailComponent implements OnInit, CanDeactivate {
// promise which resolves to true or false when the user decides
return this._dialog.confirm('Discard changes?');
}
// #enddocregion routerCanDeactivate
// #docregion cancel-save
cancel() {
this.editName = this.crisis.name;
this.gotoCrises();
@ -78,19 +67,16 @@ export class CrisisDetailComponent implements OnInit, CanDeactivate {
this.crisis.name = this.editName;
this.gotoCrises();
}
// #enddocregion cancel-save
// #docregion gotoCrises
gotoCrises() {
let route =
// pass along the crisis id if available
// so that the CrisisList component can select that crisis
['CrisisCenter', {id: this.crisis ? this.crisis.id : null} ]
this._router.navigate(route);
let crisisId = this.crisis ? this.crisis.id : null;
// Pass along the hero id if available
// so that the CrisisListComponent can select that hero.
// Add a totally useless `foo` parameter for kicks.
// #docregion gotoCrises-navigate
this._router.navigate(['CrisisCenter', {id: crisisId, foo: 'foo'} ]);
// #enddocregion gotoCrises-navigate
}
// #enddocregion gotoCrises
// #docregion routerCanDeactivate, cancel-save
}
// #enddocregion routerCanDeactivate, cancel-save
// #enddocregion

View File

@ -0,0 +1,36 @@
// #docplaster
// #docregion
import {Component, OnInit} from 'angular2/core';
import {Crisis, CrisisService} from './crisis.service';
import {Router} from 'angular2/router';
@Component({
// #docregion template
template: `
<ul>
<li *ngFor="#crisis of crises"
(click)="onSelect(crisis)">
<span class="badge">{{crisis.id}}</span> {{crisis.name}}
</li>
</ul>
`,
// #enddocregion template
})
export class CrisisListComponent implements OnInit {
crises: Crisis[];
constructor(
private _service: CrisisService,
private _router: Router) {}
ngOnInit() {
this._service.getCrises().then(crises => this.crises = crises);
}
// #docregion select
onSelect(crisis: Crisis) {
this._router.navigate(['CrisisDetail', { id: crisis.id }] );
}
// #enddocregion select
}

View File

@ -18,7 +18,7 @@ import {Router, RouteParams} from 'angular2/router';
})
export class CrisisListComponent implements OnInit {
crises: Crisis[];
// #docregion isSelected
private _selectedId: number;
constructor(
@ -27,19 +27,14 @@ export class CrisisListComponent implements OnInit {
routeParams: RouteParams) {
this._selectedId = +routeParams.get('id');
}
// #enddocregion isSelected
isSelected(crisis: Crisis) { return crisis.id === this._selectedId; }
ngOnInit() {
this._service.getCrises().then(crises => this.crises = crises);
}
// #docregion isSelected
isSelected(crisis: Crisis) { return crisis.id === this._selectedId; }
// #enddocregion isSelected
// #docregion select
onSelect(crisis: Crisis) {
this._router.navigate( ['CrisisDetail', { id: crisis.id }] );
}
// #enddocregion select
}

View File

@ -0,0 +1,44 @@
// #docregion
import {Component, OnInit} from 'angular2/core';
import {Hero, HeroService} from './hero.service';
import {RouteParams, Router} from 'angular2/router';
@Component({
template: `
<h2>HEROES</h2>
<div *ngIf="hero">
<h3>"{{hero.name}}"</h3>
<div>
<label>Id: </label>{{hero.id}}</div>
<div>
<label>Name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
<button (click)="gotoHeroes()">Back</button>
</div>
`,
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
// #docregion ctor
constructor(
private _router:Router,
private _routeParams:RouteParams,
private _service:HeroService){}
// #enddocregion ctor
// #docregion ngOnInit
ngOnInit() {
let id = this._routeParams.get('id');
this._service.getHero(id).then(hero => this.hero = hero);
}
// #enddocregion ngOnInit
// #docregion gotoHeroes
gotoHeroes() {
// Like <a [routerLink]="['Heroes']">Heroes</a>
this._router.navigate(['Heroes']);
}
// #enddocregion gotoHeroes
}

View File

@ -37,8 +37,13 @@ export class HeroDetailComponent implements OnInit {
// #docregion gotoHeroes
gotoHeroes() {
// <a [routerLink]="['Heroes']">Heroes</a>
this._router.navigate(['Heroes']);
let heroId = this.hero ? this.hero.id : null;
// Pass along the hero id if available
// so that the HeroList component can select that hero.
// Add a totally useless `foo` parameter for kicks.
// #docregion gotoHeroes-navigate
this._router.navigate(['Heroes', {id: heroId, foo: 'foo'} ]);
// #enddocregion gotoHeroes-navigate
}
// #enddocregion gotoHeroes
}

View File

@ -0,0 +1,49 @@
// #docplaster
// #docregion
// TODO SOMEDAY: Feature Componetized like HeroCenter
import {Component, OnInit} from 'angular2/core';
import {Hero, HeroService} from './hero.service';
import {Router} from 'angular2/router';
@Component({
// #docregion template
template: `
<h2>HEROES</h2>
<ul>
<li *ngFor="#hero of heroes"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
`
// #enddocregion template
})
export class HeroListComponent implements OnInit {
heroes: Hero[];
// #docregion ctor
constructor(
private _router: Router,
private _service: HeroService) { }
// #enddocregion ctor
ngOnInit() {
this._service.getHeroes().then(heroes => this.heroes = heroes)
}
// #docregion select
onSelect(hero: Hero) {
// #docregion nav-to-detail
this._router.navigate( ['HeroDetail', { id: hero.id }] );
// #enddocregion nav-to-detail
}
// #enddocregion select
}
// #enddocregion
/* A link parameters array
// #docregion link-parameters-array
['HeroDetail', { id: hero.id }] // {id: 15}
// #enddocregion link-parameters-array
*/

View File

@ -1,10 +1,12 @@
// #docplaster
// TODO SOMEDAY: Feature Componetized like CrisisCenter
// #docregion
// TODO SOMEDAY: Feature Componetized like HeroCenter
import {Component, OnInit} from 'angular2/core';
import {Hero, HeroService} from './hero.service';
import {Router} from 'angular2/router';
// #docregion import-route-params
import {Router, RouteParams} from 'angular2/router';
// #enddocregion import-route-params
@Component({
// #docregion template
@ -12,6 +14,7 @@ import {Router} from 'angular2/router';
<h2>HEROES</h2>
<ul>
<li *ngFor="#hero of heroes"
[class.selected]="isSelected(hero)"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
@ -21,29 +24,30 @@ import {Router} from 'angular2/router';
})
export class HeroListComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
// #docregion ctor
private _selectedId: number;
constructor(
private _service: HeroService,
private _router: Router,
private _service: HeroService) { }
routeParams: RouteParams) {
this._selectedId = +routeParams.get('id');
}
// #enddocregion ctor
// #docregion isSelected
isSelected(hero: Hero) { return hero.id === this._selectedId; }
// #enddocregion isSelected
// #docregion select
onSelect(hero: Hero) {
this._router.navigate( ['HeroDetail', { id: hero.id }] );
}
// #enddocregion select
ngOnInit() {
this._service.getHeroes().then(heroes => this.heroes = heroes)
}
// #docregion select
onSelect(hero: Hero) {
// #docregion nav-to-detail
this._router.navigate( ['HeroDetail', { id: hero.id }] );
// #enddocregion nav-to-detail
}
// #enddocregion select
}
// #enddocregion
/* A link parameters array
// #docregion link-parameters-array
['HeroDetail', { id: hero.id }] // {id: 15}
// #enddocregion link-parameters-array
*/

View File

@ -1,7 +1,4 @@
<!DOCTYPE html>
<script>
var boot = 'app/boot'+'.1'; // choices: '.1', '.2', '.3', ''
</script>
<!-- #docregion -->
<html>
<head>
@ -26,19 +23,13 @@
}
}
});
System.import('app/boot')
System.import('app/boot.1') // <----- ONLY CHANGE
.then(null, console.error.bind(console));
</script>
<script>
System.config({
packages: {'app': {defaultExtension: 'js'}}
});
// window.boot is for our testing; you should just use 'app/boot'
System.import(window.boot||'app/boot').catch(console.log.bind(console));
</script>
</head>
<body>
<h1>Milestone 1</h1>
<my-app>loading...</my-app>
</body>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<!-- #docregion base-href -->
<base href="/">
<!-- #enddocregion base-href -->
<title>Router Sample</title>
<link rel="stylesheet" href="styles.css">
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<!-- #docregion router-lib -->
<script src="node_modules/angular2/bundles/router.dev.js"></script>
<!-- #enddocregion router-lib -->
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/boot.2') // <----- ONLY CHANGE
.then(null, console.error.bind(console));
</script>
</head>
<body>
<h1>Milestone 2</h1>
<my-app>loading...</my-app>
</body>
</html>
<!-- #enddocregion -->

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<!-- #docregion base-href -->
<base href="/">
<!-- #enddocregion base-href -->
<title>Router Sample</title>
<link rel="stylesheet" href="styles.css">
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<!-- #docregion router-lib -->
<script src="node_modules/angular2/bundles/router.dev.js"></script>
<!-- #enddocregion router-lib -->
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/boot.3') // <----- ONLY CHANGE
.then(null, console.error.bind(console));
</script>
</head>
<body>
<h1>Milestone 3</h1>
<my-app>loading...</my-app>
</body>
</html>
<!-- #enddocregion -->

View File

@ -20,7 +20,9 @@ a:hover {color: white; background-color: #1171a3; }
a.router-link-active {color: white; background-color: #52b9e9; }
/* #enddocregion starter */
/* #docregion selected */
.selected { background-color: #EEE; color: #369; }
/* #enddocregion selected */
.badge {
font-size: small;

View File

@ -20,14 +20,51 @@ include ../../../../_includes/_util-fns
or in response to some other stimulus from any source. And the router logs activity
in the browser's history journal so the back and forward buttons work as well.
[Live Example](/resources/live-examples/router/ts/plnkr.html).
We'll learn many router details in this chapter which covers
* [configuring a router](#route-config)
* the [link parameter arrays](#link-parameters-array) that propel router navigation
* navigating when the user clicks a data-bound [RouterLink](#router-link)
* navigating under [program control](#navigate)
* embedding critical information in the URL with [route parameters](#route-parameters)
* creating a [child router](#child-router) with its own routes
* setting a [default route](#default)
* confirming or canceling navigation with [router lifecycle hooks](#lifecycle-hooks)
* passing optional information in [query parameters](#query-parameters)
We proceed in phases marked by milestones building rom a simple two-pager with placeholder views
up to a modular, multi-view design with child routes.
Try that [live final version](/resources/live-examples/router/ts/plnkr.html).
But first, an overview of router basics.
.l-main-section
:marked
## The Basics
Let's begin with a few core concepts of the Component Router.
Then we can explore the details through a sequence of examples.
### Setup
The Component Router is an optional service that presents a particular component view for a given URL.
It is not part of the Angular 2 core. It is in its own library
within the Angular npm bundle.
We make it available by loading its script in our `index.html`, after
the Angular core script.
+makeExample('router/ts/index.html','router-lib')(format=".")
:marked
### *&lt;base href>*
Most routing applications should add a `<base>` element just after the `<head>` tag
to tell the router how to compose navigation URLs.
The **`Router`** is a service that presents a particular Component view for a given URL.
If the `app` folder is the application root, as it is for our sample application,
set the `href` value *exactly* as shown here.
+makeExample('router/ts/index.html','base-href')(format=".")
.l-sub-section
:marked
We cover other options in the [details below](#browser-url-styles).
:marked
### Configuration
When the browser's URL changes, the router looks for a corresponding **`RouteDefinition`**
from which it can determine the Component to display.
@ -51,6 +88,7 @@ include ../../../../_includes/_util-fns
will use that value to find and present the hero whose `id` is 42.
We'll learn more about route parameters later in this chapter.
:marked
### Router Outlet
Now we know how the router gets its configuration.
When the browser URL for this application becomes `/heroes`,
the router matches that URL to the `RouteDefintion` named *Heroes* and displays the `HeroListComponent`
@ -59,6 +97,7 @@ code-example(format="", language="html").
&lt;!-- Routed views go here -->
&lt;router-outlet>&lt;/router-outlet>
:marked
### Router Links
Now we have routes configured and a place to render them, but
how do we navigate? The URL could arrive directly from the browser address bar.
But most of the time we navigate as a result of some user action such as the click of
@ -77,7 +116,7 @@ code-example(format="", language="html").
'CrisisCenter' and 'Heroes' are the names of the `Routes` we configured above.
We'll learn to write more complex link expressions &mdash; and why they are arrays &mdash;
[later](#link-parameter-array) in the chapter.
[later](#link-parameters-array) in the chapter.
:marked
### Let's summarize
@ -131,38 +170,25 @@ table
td.
An Angular component with an attached router.
:marked
We'll learn many more details in this chapter which covers
* [configuring a router](#route-config)
* the [link parameter arrays](#link-parameters-array) that propel router navigation
* navigating when the user clicks a data-bound [RouterLink](#router-link)
* navigating under [program control](#navigate)
* passing information in [route parameters](#route-parameter)
* creating a [child router](#child-router) with its own routes
* setting a [default route](#default)
* pausing, confirming and/or canceling a navigation with the the `routerCanDeactivate` [router lifecycle hook](#lifecycle-hooks)
We proceed in phases marked by milestones.
Our first milestone is the ability to navigate between two placeholder views.
At our last milestone, we'll have a modular, multi-view design with child routes.
We assume that you're already comfortable with the basic Angular 2 tools and concepts
we introduced in the [QuickStart](../quickstart.html) and
the [Tour of Heroes](../tutorial/) tutorial.
.l-sub-section
:marked
While we make incremental progress on a sample application, this chapter is not a tutorial.
We discuss code and design decisions pertinent to routing and application design.
We gloss over everything in between.
The full source is available in the [live example](/resources/live-examples/router/ts/plnkr.html).
:marked
We've barely touched the surface of the router and its capabilities.
The following detail sections describe a sample routing application
as it evolves over a sequence of milestones.
We strongly recommend taking the time to read and understand this story.
.l-main-section
:marked
## The Sample Application
We have an application in mind as we move from milestone to milestone.
.l-sub-section
:marked
While we make incremental progress toward the ultimate sample application, this chapter is not a tutorial.
We discuss code and design decisions pertinent to routing and application design.
We gloss over everything in between.
The full source is available in the [live example](/resources/live-examples/router/ts/plnkr.html).
:marked
Our client is the Hero Employment Agency.
Heroes need work and The Agency finds Crises for them to solve.
@ -245,6 +271,7 @@ figure.image-display
The Component Router library is part of the Angular npm bundle.
We make it available by loading its script in our `index.html`, right after
the Angular core script.
<a id="base-href"></a>
+makeExample('router/ts/index.html','router-lib')(format=".")
:marked
### Set the *&lt;base href>*
@ -503,13 +530,13 @@ figure.image-display
Notice the `:id` token in the the path. That creates a slot in the path for a **Route Parameter**.
In this case, we're expecting the router to insert the `id` of a hero into that slot.
If we tell the router to navigate to the detail component and display "Magenta", we expect hero `id` (15) to appear in the
If we tell the router to navigate to the detail component and display "Magneta", we expect hero `id` (15) to appear in the
browser URL like this:
code-example(format="." language="bash").
localhost:3000/hero/15
:marked
If a user enters that URL into the browser address bar, the router should recognize the
pattern and go to the same "Magenta" detail view.
pattern and go to the same "Magneta" detail view.
<a id="navigate"></id>
### Navigate to the detail imperatively
@ -522,28 +549,29 @@ code-example(format="." language="bash").
We'll adjust the `HeroListComponent` to implement these tasks, beginning with its constructor
which acquires the router service and the `HeroService` by dependency injection:
+makeExample('router/ts/app/heroes/hero-list.component.ts','ctor')(format=".")
+makeExample('router/ts/app/heroes/hero-list.component.1.ts','ctor')(format=".")
:marked
We make a few changes to the template:
+makeExample('router/ts/app/heroes/hero-list.component.ts','template')(format=".")
+makeExample('router/ts/app/heroes/hero-list.component.1.ts','template')(format=".")
:marked
The template defines an `*ngFor` repeater such as [we've seen before](displaying-data.html#ngFor).
There's a `(click)` [EventBinding](template-syntax.html#event-binding) to the component's `onSelect` method
which we implement as follows:
+makeExample('router/ts/app/heroes/hero-list.component.ts','select')(format=".")
+makeExample('router/ts/app/heroes/hero-list.component.1.ts','select')(format=".")
:marked
It calls the router's **`navigate`** method with a **Link Parameters Array**.
This array is similar to the *link parameters array* we met [earlier](#shell-template) in an anchor tag while
binding to the `RouterLink` directive. This time we see it in code rather than in HTML.
<a id="route-parameter"></id>
### Setting the route parameter
<a id="route-parameters"></id>
### Setting the route parameters object
We're navigating to the `HeroDetailComponent` where we expect to see the details of the selected hero.
We'll need *two* pieces of information: the destination and the hero's `id`.
Accordingly, the *link parameters array* has *two* items: the **name** of the destination route and a **route parameters object** that specifies the
`id` of the selected hero.
+makeExample('router/ts/app/heroes/hero-list.component.ts','link-parameters-array')(format=".")
+makeExample('router/ts/app/heroes/hero-list.component.1.ts','link-parameters-array')(format=".")
:marked
The router composes the appropriate two-part destination URL from this array:
code-example(format="." language="bash").
@ -560,12 +588,12 @@ code-example(format="." language="bash").
As usual, we write a constructor that asks Angular to inject that service among the other services
that the component require and reference them as private variables.
+makeExample('router/ts/app/heroes/hero-detail.component.ts','ctor')(format=".")
+makeExample('router/ts/app/heroes/hero-detail.component.1.ts','ctor')(format=".")
:marked
Later, in the `ngOnInit` method,
we ask the `RouteParams` service for the `id` parameter by name and
tell the `HeroService` to fetch the hero with that `id`.
+makeExample('router/ts/app/heroes/hero-detail.component.ts','ngOnInit')(format=".")
+makeExample('router/ts/app/heroes/hero-detail.component.1.ts','ngOnInit')(format=".")
.l-sub-section
:marked
@ -582,7 +610,7 @@ code-example(format="." language="bash").
The router `navigate` method takes the same one-item *link parameters array*
that we wrote for the `[routerLink]` directive binding.
It holds the **name of the `HeroListComponent` route**:
+makeExample('router/ts/app/heroes/hero-detail.component.ts','gotoHeroes')(format=".")
+makeExample('router/ts/app/heroes/hero-detail.component.1.ts','gotoHeroes')(format=".")
:marked
### Heroes App Wrap-up
@ -618,13 +646,11 @@ code-example(format="." language="bash").
Here are the relevant files for this version of the sample application.
+makeTabs(
`router/ts/app/app.component.2.ts,
router/ts/app/boot.2.ts,
router/ts/app/heroes/hero-list.component.ts,
router/ts/app/heroes/hero-detail.component.ts,
router/ts/app/heroes/hero-list.component.1.ts,
router/ts/app/heroes/hero-detail.component.1.ts,
router/ts/app/heroes/hero.service.ts`,
`,v2,,,`,
null,
`app.component.ts,
boot.ts,
hero-list.component.ts,
hero-detail.component.ts,
hero.service.ts`)
@ -714,7 +740,7 @@ code-example(format="." language="bash").
<a id="child-router"></id>
### Child Routing Component
We create a new `app/crisis-center` folder and add `crisis-center-component.ts` to it with the following contents:
+makeExample('router/ts/app/crisis-center/crisis-center.component.ts', 'minus-imports', 'crisis-center/crisis-center.component.ts (minus imports)')
+makeExample('router/ts/app/crisis-center/crisis-center.component.1.ts', 'minus-imports', 'crisis-center/crisis-center.component.ts (minus imports)')
:marked
The `CrisisCenterComponent` parallels the `AppComponent`.
@ -736,7 +762,7 @@ code-example(format="." language="bash").
### Service isolation
We add the `CrisisService` to the component's providers array
instead of registering it with the `bootstrap` function in `boot.ts`.
+makeExample('router/ts/app/crisis-center/crisis-center.component.ts', 'providers')
+makeExample('router/ts/app/crisis-center/crisis-center.component.1.ts', 'providers')
:marked
This step limits the scope of that service to the *Crisis Center* component and its sub-component tree.
No component outside of the *Crisis Center* needs access to the `CrisisService`.
@ -748,7 +774,7 @@ code-example(format="." language="bash").
The `@RouteConfig` decorator that adorns the `CrisisCenterComponent` class defines routes in much the same way
that we did earlier.
+makeExample('router/ts/app/crisis-center/crisis-center.component.ts', 'route-config', 'app/crisis-center/crisis-center.component.ts (routes only)' )(format=".")
+makeExample('router/ts/app/crisis-center/crisis-center.component.1.ts', 'route-config', 'app/crisis-center/crisis-center.component.ts (routes only)' )(format=".")
:marked
There is an *important difference in the paths*. They both begin at `/`.
Normally such paths would refer to the root of the application.
@ -807,7 +833,7 @@ code-example(format="").
We've tried the sample application and it didn't fail. We must have done something right.
Scroll to the end of the child `CrisisCenterComponent`s first route.
+makeExample('router/ts/app/crisis-center/crisis-center.component.ts', 'default-route', 'app/crisis-center/crisis-center.component.ts (default route)')(format=".")
+makeExample('router/ts/app/crisis-center/crisis-center.component.1.ts', 'default-route', 'app/crisis-center/crisis-center.component.ts (default route)')(format=".")
:marked
There is `useAsDefault: true` again. That tells the router to compose the final URL using the path from the default child route.
Concatenate the base URL with `/crisis-center/` and `/`, remove extraneous slashes, and we get:
@ -872,7 +898,7 @@ code-example(format="").
We discard the changes if the user presses he *Cancel* button.
Both buttons navigate back to the crisis list after save or cancel.
+makeExample('router/ts/app/crisis-center/crisis-detail.component.ts', 'cancel-save', 'crisis-detail.component.ts (excerpt)')(format=".")
+makeExample('router/ts/app/crisis-center/crisis-detail.component.1.ts', 'cancel-save', 'crisis-detail.component.ts (excerpt)')(format=".")
:marked
What if the user tries to navigate away without saving or canceling?
The user could push the browser back button or click the heroes link.
@ -899,7 +925,7 @@ code-example(format="").
<a id="routerCanDeactivate"></a>
:marked
We execute the dialog inside the router's `routerCanDeactivate` lifecycle hook method.
+makeExample('router/ts/app/crisis-center/crisis-detail.component.ts', 'routerCanDeactivate', 'crisis-detail.component.ts (excerpt)')
+makeExample('router/ts/app/crisis-center/crisis-detail.component.1.ts', 'routerCanDeactivate', 'crisis-detail.component.ts (excerpt)')
:marked
Notice that the `routerCanDeactivate` method *can* return synchronously;
it returns `true` immediately if there is no crisis or there are no pending changes.
@ -912,7 +938,195 @@ code-example(format="").
could navigate away. That's the router's job.
We simply write this method and let the router take it from there.
The final code for the *Crisis Center* feature is [here](#crisis-center-structure-and-code).
The relevant *Crisis Center* code for this milestone is
+makeTabs(
`router/ts/app/crisis-center/crisis-center.component.ts,
router/ts/app/crisis-center/crisis-list.component.1.ts,
router/ts/app/crisis-center/crisis-detail.component.1.ts,
router/ts/app/crisis-center/crisis.service.ts
`,
null,
`crisis-center.component.ts,
crisis-list.component.ts,
crisis-detail.component.ts,
crisis.service.ts,
`)
<a id="query-parameters"></a>
.l-main-section
:marked
## Milestone #4: Query Parameters
We use [*route parameters*](#route-parameters) to specify a parameterized value *within* the route URL
as we do when navigating to the `HeroDetailComponent` in order to view-and-edit the hero with *id:15*.
code-example(format="." language="bash").
localhost:3000/hero/15
:marked
Sometimes we wish to add optional information to a route request.
For example, the `HeroListComponent` doesn't need help to display a list of heroes.
But it might be nice if the previously-viewed hero were pre-selected when returning from the `HeroDetailComponent`.
figure.image-display
img(src='/resources/images/devguide/router/selected-hero.png' alt="Selected hero" width="175")
:marked
That becomes possible if we can include hero Magneta's `id` in the URL when we
return from the `HeroDetailComponent`, a scenario we'll pursue in a moment.
Optional information takes other forms. Search criteria are often loosely structured, e.g., `name='wind*'`.
Multiple values are common &mdash; `after='12/31/2015' & before='1/1/2017'` &mdash; in no particular order &mdash;
`before='1/1/2017' & after='12/31/2015'` &mdash; in a variety of formats &mdash; `during='currentYear'` .
These kinds of parameters don't fit easily in a URL *path*. Even if we could define a suitable URL token scheme,
doing so greatly complicates the pattern matching required to translate an incoming URL to a named route.
The **URL query string** is the ideal vehicle for conveying arbitrarily complex information during navigation.
The query string isn't involved in pattern matching and affords enormous flexiblity of expression.
Almost anything serializable can appear in a query string.
The Component Router supports navigation with query strings as well as route parameters.
We define query string parameters in the *route parameters object* just like we do with route parameters.
<a id="route-parameters-object"></a>
### Route parameters object
When navigating to the `HeroDetailComponent` we specified the `id` of the hero-to-edit in the
*route parameters object* and made it the second item of the [*link parameters array*](link-parameters-array).
+makeExample('router/ts/app/heroes/hero-list.component.1.ts','link-parameters-array')(format=".")
:marked
The router embedded the `id` value in the navigation URL because we had defined it
as a route parameter with an `:id` placeholder token in the route `path`:
+makeExample('router/ts/app/app.component.2.ts','hero-detail-route')(format=".")
:marked
When the user clicks the back button, the `HeroDetailComponent` constructs another *link parameters array*
which it uses to navigate back to the `HeroListComponent`.
+makeExample('router/ts/app/heroes/hero-detail.component.1.ts','gotoHeroes')(format=".")
:marked
This array lacks a route parameters object because we had no reason to send information to the `HeroListComponent`.
Now we have a reason. We'd like to send the id of the current hero with the navigation request so that the
`HeroListComponent` can highlight that hero in its list.
We do that with a route parameters object in the same manner as before.
We also defined a junk parameter (`foo`) that the `HeroListComponent` should ignore.
Here's the revised navigation statement:
+makeExample('router/ts/app/heroes/hero-detail.component.ts','gotoHeroes-navigate')(format=".")
:marked
The application still works. Clicking "back" returns to the hero list view.
Look at the browser address bar. It should look something like this:
code-example(format="." language="bash").
localhost:3000/heroes?id=15&foo=foo
.l-sub-section
:marked
Unfortunately, the browser address bar does not change when running the live example in plunker.
You can take our word for it or
download the sample from within plunker, unzip it, and browse to `index.html`.
You may have to launch it in a local server such as
[http-server](https://www.npmjs.com/package/http-server) or [lite-server](https://github.com/johnpapa/lite-server).
:marked
The `id` value appears in the query string (`?id=15&foo=foo`), not in the URL path.
The path for the "Heroes" route doesn't have an `:id` token.
.alert.is-helpful
:marked
The router replaces route path tokens with corresponding values from the route parameters object.
**Every parameter _not_ consumed by a route path goes in the query string.**
:marked
### Query parameters in the *RouteParams* service
The list of heroes is unchanged. No hero row is highlighted.
.l-sub-section
:marked
The [live example](/resources/live-examples/router/ts/plnkr.html) *does* highlight the selected
row because it demonstrates the final state of the application which includes the steps we're *about* to cover.
At the moment we're describing the state of affairs *prior* to those steps.
:marked
The `HeroListComponent` isn't expecting any parameters at all and wouldn't know what to do with them.
Let's change that.
When navigating from the `HeroListComponent` to the `HeroDetailComponent`
the router picked up the route parameter object and made it available to the `HeroDetailComponent`
in the `RouteParams` service. We injected that service in the constructor of the `HeroDetailComponent`.
This time we'll be navigating in the opposite direction, from the `HeroDetailComponent` to the `HeroListComponent`.
This time we'll inject the `RouteParams` service in the constructor of the `HeroListComponent`.
First we extend the router import statement to include the `RouteParams` service symbol;
+makeExample('router/ts/app/heroes/hero-list.component.ts','import-route-params', 'hero-list.component.ts (import)')(format=".")
:marked
Then we extend the constructor to inject the `RouteParams` service and extract the `id` parameter as the `_selectedId`:
+makeExample('router/ts/app/heroes/hero-list.component.ts','ctor', 'hero-list.component.ts (constructor)')(format=".")
.l-sub-section
:marked
All route parameters are strings.
The (+) in front of the `routeParameters.get` expression is a JavaScript trick to convert the string to an integer.
:marked
We add an `isSelected` method that returns true when a hero's id matches the selected id.
+makeExample('router/ts/app/heroes/hero-list.component.ts','isSelected', 'hero-list.component.ts (constructor)')(format=".")
:marked
We update our template with a [Class Binding](template-syntax.html#class-binding) to that `isSelected` method.
The binding adds the `selected` CSS class when the method returns `true` and removes it when `false`.
Look for it within the repeated `<li>` tag as shown here:
+makeExample('router/ts/app/heroes/hero-list.component.ts','template', 'hero-list.component.ts (template)')(format=".")
:marked
Finally, we add the `selected` class to our CSS styles
+makeExample('router/ts/styles.css','selected', 'styles.css (selected class)')(format=".")
:marked
When the user navigates from the heroes list to the "Magneta" hero and back, "Magneta" appears selected:
figure.image-display
img(src='/resources/images/devguide/router/selected-hero.png' alt="Selected List" width="250")
:marked
The `foo` query string parameter is harmless and continues to be ignored.
### Child Routers and Query Parameters
We can define query parameters for child routers too.
The technique is precisely the same.
In fact, we made exactly the same changes to the *Crisis Center* feature.
Confirm the similarities in these *Hero* and *CrisisCenter* components,
arranged side-by-side for easy comparison:
+makeTabs(
`router/ts/app/heroes/hero-list.component.ts,
router/ts/app/crisis-center/crisis-list.component.ts,
router/ts/app/heroes/hero-detail.component.ts,
router/ts/app/crisis-center/crisis-detail.component.ts
`,
null,
`hero-list.component.ts,
crisis-list.component.ts,
hero-detail.component.ts,
crisis-detail.component.ts
`)
:marked
When we navigate back from a `CrisisDetailComponent` that is showing the *Asteroid* crisis,
we see that crisis properly selected in the list like this:
figure.image-display
img(src='/resources/images/devguide/router/selected-crisis.png' alt="Selected crisis" )
:marked
**Look at the browser address bar again**. It's *different*. It looks something like this:
code-example(format="." language="bash").
localhost:3000/crisis-center/;id=3;foo=foo
:marked
The query string parameters are no longer separated by "?" and "&".
They are **separated by semicolons (;)**
This is *matrix URL* notation &mdash; something we may not have seen before.
.l-sub-section
:marked
*Matrix URL* notation is an idea first floated
in a [1996 proposal](http://www.w3.org/DesignIssues/MatrixURIs.html) by the founder of the web, Tim Berners-Lee.
Although matrix notation never made it into the HTML standard, it is legal and
it became popular among browser routing systems as a way to isolate parameters
belonging to parent and child routes. The Angular Component Router is such a system.
The syntax may seem strange to us but users are unlikely to notice or care
as long as the URL can be emailed and pasted into a browser address bar
as this one can.
<a id="final-app"></a>
.l-main-section
@ -1016,7 +1230,7 @@ code-example(format="").
.l-main-section
<a id="link-parameter-array"></a>
<a id="link-parameters-array"></a>
:marked
## Link Parameters Array
We've mentioned the *Link Parameters Array* several times. We've used it several times.
@ -1025,7 +1239,7 @@ code-example(format="").
+makeExample('router/ts/app/app.component.3.ts', 'h-anchor')(format=".")
:marked
We've written a two element array when specifying a route parameter like this
+makeExample('router/ts/app/heroes/hero-list.component.ts', 'nav-to-detail')(format=".")
+makeExample('router/ts/app/heroes/hero-list.component.1.ts', 'nav-to-detail')(format=".")
:marked
These two examples cover our needs for an app with one level routing.
The moment we add a child router, such as the *Crisis Center*, we create new link array possibilities.
@ -1121,15 +1335,9 @@ code-example(format="").
When the router navigates to a new component view, it updates the browser's location and history
with a URL for that view.
This is a strictly local URL. The browser shouldn't send a request to the server
This is a strictly local URL. The browser shouldn't send this URL to the server
and should not reload the page.
.l-sub-section
:marked
We're talking now about the ***browser*** URL
**not** the *route* URL that we record in a `RouteDefinition`.
The browser URL is what we paste into the browser's **address bar**
and email to folks so they can deep-link into an application page.
:marked
Modern HTML 5 browsers support
[history.pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries),
a technique that changes a browser's location and history without triggering a server page request.
@ -1142,13 +1350,18 @@ code-example(format=".", language="bash").
:marked
Older browsers send page requests to the server when the location URL changes ...
unless the change occurs after a "#" (called the "hash").
Routers take advantage of this exception by composing in-application route
Routers can take advantage of this exception by composing in-application route
URLs with hashes. Here's a "hash URL" that routes to the *Crisis Center*
code-example(format=".", language="bash").
localhost:3002/src/#/crisis-center/
:marked
The Angular Component Router supports both styles.
We set our preference by providing a `LocationStrategy` during the bootstrapping process.
The Angular Component Router supports both styles with two `LocationStrategy` providers:
1. `PathLocationStrategy` - the default "HTML 5 pushState" style.
1. `HashLocationStrategy` - the "hash URL" style.
The router's `ROUTER_PROVIDERS` array sets the `LocationStrategy` to the `PathLocationStrategy`,
making it the default strategy.
We can switch to the `HashLocationStrategy` with an override during the bootstrapping process if we prefer it.
.l-sub-section
:marked
Learn about "providers" and the bootstrap process in the
@ -1176,11 +1389,10 @@ code-example(format=".", language="bash").
resort to hash routes.
### HTML 5 URLs and the *&lt;base href>*
The router use the "[HTML 5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries)"
style by default.
We don't have to provide the router's `PathLocationStrategy` because it's loaded automatically.
While the router use the "[HTML 5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries)"
style by default, we *must* configure that strategy with a **base href**
We *must* add a
The preferred way to configure the strategy is to add a
[&lt;base href> element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag
in the `<head>` of the `index.html`.
+makeExample('router/ts/index.html','base-href')(format=".")
@ -1200,14 +1412,16 @@ code-example(format=".", language="bash").
.l-sub-section
:marked
Learn about the [APP_BASE_HREF](https://angular.io/docs/ts/latest/api/router/APP_BASE_HREF-const.html)
Learn about the [APP_BASE_HREF](https://angular.io/docs/ts/latest/api/router/APP_BASE_HREF-let.html)
in the API Guide.
:marked
### *HashLocationStrategy*
We can go old-school with the `HashLocationStrategy` by
providing it as the router's `LocationStrategy` during application bootstrapping.
That means importing `provide` for Dependency Injection and the
`Location` and `HashLocationStrategy` symbols from the router,
then providing that strategy in the call to `bootstrap`:
+makeExample('router/ts/app/boot.2.ts', 'hash-strategy')
First, import the `provide` symbol for Dependency Injection and the
`Location` and `HashLocationStrategy` symbols from the router.
Then *override* the default strategy defined in `ROUTE_PROVIDERS` by
providing the `HashLocationStrategy` later in the `bootstrap` providers array argument:
+makeExample('router/ts/app/boot.2.ts','', 'boot.ts (hash URL strategy)')

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB