docs(router): fixes recent issues

closes #619 & #626
This commit is contained in:
Ward Bell 2015-12-31 16:55:53 -08:00
parent 872064e12e
commit 8e3e0834b5
15 changed files with 96 additions and 83 deletions

View File

@ -13,8 +13,10 @@ import {HeroListComponent} from './hero-list.component';
// #docregion template // #docregion template
template: ` template: `
<h1>Component Router</h1> <h1>Component Router</h1>
<nav>
<a [routerLink]="['CrisisCenter']">Crisis Center</a> <a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a> <a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>
`, `,
// #enddocregion template // #enddocregion template

View File

@ -9,16 +9,20 @@ import {CrisisListComponent} from './crisis-list.component';
// #docregion hero-import // #docregion hero-import
import {HeroListComponent} from './heroes/hero-list.component'; import {HeroListComponent} from './heroes/hero-list.component';
import {HeroDetailComponent} from './heroes/hero-detail.component'; import {HeroDetailComponent} from './heroes/hero-detail.component';
import {HeroService} from './heroes/hero.service';
// #enddocregion hero-import // #enddocregion hero-import
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
template: ` template: `
<h1>Component Router</h1> <h1>Component Router</h1>
<nav>
<a [routerLink]="['CrisisCenter']">Crisis Center</a> <a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a> <a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>
`, `,
providers: [HeroService],
directives: [ROUTER_DIRECTIVES] directives: [ROUTER_DIRECTIVES]
}) })
// #enddocregion // #enddocregion

View File

@ -7,6 +7,8 @@ import {CrisisCenterComponent} from './crisis-center/crisis-center.component';
import {HeroListComponent} from './heroes/hero-list.component'; import {HeroListComponent} from './heroes/hero-list.component';
import {HeroDetailComponent} from './heroes/hero-detail.component'; import {HeroDetailComponent} from './heroes/hero-detail.component';
import {DialogService} from './dialog.service';
import {HeroService} from './heroes/hero.service';
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
@ -38,12 +40,15 @@ import {HeroDetailComponent} from './heroes/hero-detail.component';
// #docregion template // #docregion template
template: ` template: `
<h1 class="title">Component Router</h1> <h1 class="title">Component Router</h1>
<nav>
<a [routerLink]="['CrisisCenter', 'CrisisCenter']">Crisis Center</a> <a [routerLink]="['CrisisCenter', 'CrisisCenter']">Crisis Center</a>
<a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:1}]">Princess Crisis</a> <a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:1}]">Princess Crisis</a>
<a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:2}]">Dragon Crisis</a> <a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:2}]">Dragon Crisis</a>
</nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>
`, `,
// #enddocregion template // #enddocregion template
providers: [DialogService, HeroService],
directives: [ROUTER_DIRECTIVES] directives: [ROUTER_DIRECTIVES]
}) })
@RouteConfig([ @RouteConfig([

View File

@ -7,16 +7,22 @@ import {CrisisCenterComponent} from './crisis-center/crisis-center.component';
import {HeroListComponent} from './heroes/hero-list.component'; import {HeroListComponent} from './heroes/hero-list.component';
import {HeroDetailComponent} from './heroes/hero-detail.component'; import {HeroDetailComponent} from './heroes/hero-detail.component';
import {DialogService} from './dialog.service';
import {HeroService} from './heroes/hero.service';
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
// #docregion template // #docregion template
template: ` template: `
<h1 class="title">Component Router</h1> <h1 class="title">Component Router</h1>
<nav>
<a [routerLink]="['CrisisCenter']">Crisis Center</a> <a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a> <a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>
`, `,
// #enddocregion template // #enddocregion template
providers: [DialogService, HeroService],
directives: [ROUTER_DIRECTIVES] directives: [ROUTER_DIRECTIVES]
}) })
// #docregion route-config // #docregion route-config

View File

@ -18,6 +18,6 @@ bootstrap(AppComponent, [
import {AppComponent as ac} from './app.component.1'; import {AppComponent as ac} from './app.component.1';
bootstrap(ac, [ bootstrap(ac, [
// #docregion all // #docregion all
ROUTER_PROVIDERS, ROUTER_PROVIDERS
]); ]);
// #enddocregion all // #enddocregion all

View File

@ -4,16 +4,11 @@
// #docplaster // #docplaster
// #docregion v2 // #docregion v2
// #docregion hash-strategy
import {bootstrap} from 'angular2/platform/browser'; import {bootstrap} from 'angular2/platform/browser';
import {ROUTER_PROVIDERS} from 'angular2/router'; import {ROUTER_PROVIDERS} from 'angular2/router';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
// #enddocregion hash-strategy
import {HeroService} from './heroes/hero.service';
// #enddocregion v2 // #enddocregion v2
// #docregion hash-strategy
// Add these symbols to register a `LocationStrategy` // Add these symbols to register a `LocationStrategy`
import {provide} from 'angular2/core'; import {provide} from 'angular2/core';
import {LocationStrategy, import {LocationStrategy,
@ -31,17 +26,13 @@ bootstrap(AppComponent, [
import {AppComponent as ac} from './app.component.2'; import {AppComponent as ac} from './app.component.2';
bootstrap(ac, [ bootstrap(ac, [
// #docregion v2, hash-strategy
ROUTER_PROVIDERS,
// #enddocregion v2, hash-strategy
// #docregion hash-strategy // #docregion hash-strategy
provide(LocationStrategy, provide(LocationStrategy,
{useClass: HashLocationStrategy}), // ~/src/#/crisis-center/ {useClass: HashLocationStrategy}) // ~/#/crisis-center/
// #enddocregion hash-strategy // #enddocregion hash-strategy
// #docregion v2 // #docregion v2, hash-strategy
HeroService,
// #docregion hash-strategy
ROUTER_PROVIDERS
]); ]);
// #enddocregion hash-strategy // #enddocregion v2, hash-strategy
// #enddocregion v2

View File

@ -3,9 +3,5 @@ import {bootstrap} from 'angular2/platform/browser';
import {ROUTER_PROVIDERS} from 'angular2/router'; import {ROUTER_PROVIDERS} from 'angular2/router';
import {AppComponent} from './app.component.3'; import {AppComponent} from './app.component.3';
import {DialogService} from './dialog.service';
bootstrap(AppComponent, [ bootstrap(AppComponent, [ROUTER_PROVIDERS]);
ROUTER_PROVIDERS,
DialogService
]);

View File

@ -3,11 +3,5 @@ import {bootstrap} from 'angular2/platform/browser';
import {ROUTER_PROVIDERS} from 'angular2/router'; import {ROUTER_PROVIDERS} from 'angular2/router';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {DialogService} from './dialog.service';
import {HeroService} from './heroes/hero.service';
bootstrap(AppComponent, [ bootstrap(AppComponent, [ROUTER_PROVIDERS]);
ROUTER_PROVIDERS,
DialogService,
HeroService
]);

View File

@ -16,7 +16,7 @@ import {CanDeactivate, ComponentInstruction, Router} from 'angular2/router';
styles: ['input {width: 20em}'] styles: ['input {width: 20em}']
}) })
export class AddCrisisComponent implements CanDeactivate { export class AddCrisisComponent implements CanDeactivate {
public editName: string; editName: string;
constructor( constructor(
private _service: CrisisService, private _service: CrisisService,

View File

@ -22,8 +22,7 @@ import {CrisisService} from './crisis.service';
// #docregion default-route // #docregion default-route
{path:'/', name: 'CrisisCenter', component: CrisisListComponent, useAsDefault: true}, {path:'/', name: 'CrisisCenter', component: CrisisListComponent, useAsDefault: true},
// #enddocregion default-route // #enddocregion default-route
{path:'/:id', name: 'CrisisDetail', component: CrisisDetailComponent}, {path:'/:id', name: 'CrisisDetail', component: CrisisDetailComponent}
{path:'/list/:id', name: 'CrisisList', component: CrisisListComponent}
]) ])
// #enddocregion route-config // #enddocregion route-config
export class CrisisCenterComponent { } export class CrisisCenterComponent { }

View File

@ -31,8 +31,8 @@ import {DialogService} from '../dialog.service';
// #docregion routerCanDeactivate, cancel-save // #docregion routerCanDeactivate, cancel-save
export class CrisisDetailComponent implements OnInit, CanDeactivate { export class CrisisDetailComponent implements OnInit, CanDeactivate {
public crisis: Crisis; crisis: Crisis;
public editName: string; editName: string;
// #enddocregion routerCanDeactivate, cancel-save // #enddocregion routerCanDeactivate, cancel-save
constructor( constructor(
@ -56,16 +56,17 @@ export class CrisisDetailComponent implements OnInit, CanDeactivate {
} }
// #enddocregion ngOnInit // #enddocregion ngOnInit
// #docregion canDeactivate // #docregion routerCanDeactivate
routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) : any {
// Allow navigation (`true`) if no crisis or the crisis is unchanged. // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged.
// Otherwise ask the user with the dialog service and return its if (!this.crisis || this.crisis.name === this.editName) {
// promise which resolves true-or-false when the user decides return true;
return !this.crisis ||
this.crisis.name === this.editName ||
this._dialog.confirm('Discard changes?');
} }
// #enddocregion canDeactivate // 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 // #docregion cancel-save
cancel() { cancel() {
@ -82,7 +83,9 @@ export class CrisisDetailComponent implements OnInit, CanDeactivate {
// #docregion gotoCrises // #docregion gotoCrises
gotoCrises() { gotoCrises() {
let route = let route =
['CrisisList', {id: this.crisis ? this.crisis.id : null} ] // 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); this._router.navigate(route);
} }

View File

@ -17,7 +17,7 @@ import {Router, RouteParams} from 'angular2/router';
`, `,
}) })
export class CrisisListComponent implements OnInit { export class CrisisListComponent implements OnInit {
public crises: Crisis[]; crises: Crisis[];
// #docregion isSelected // #docregion isSelected
private _selectedId: number; private _selectedId: number;

View File

@ -19,7 +19,7 @@ import {RouteParams, Router} from 'angular2/router';
`, `,
}) })
export class HeroDetailComponent implements OnInit { export class HeroDetailComponent implements OnInit {
public hero: Hero; hero: Hero;
// #docregion ctor // #docregion ctor
constructor( constructor(

View File

@ -20,8 +20,8 @@ import {Router} from 'angular2/router';
// #enddocregion template // #enddocregion template
}) })
export class HeroListComponent implements OnInit { export class HeroListComponent implements OnInit {
public heroes: Hero[]; heroes: Hero[];
public selectedHero: Hero; selectedHero: Hero;
// #docregion ctor // #docregion ctor
constructor( constructor(

View File

@ -202,7 +202,7 @@ figure.image-display
Both buttons navigate back to the *Crisis Center* and its list of crises. Both buttons navigate back to the *Crisis Center* and its list of crises.
Suppose we click a crisis, make a change, but ***do not click either button***. Suppose we click a crisis, make a change, but ***do not click either button***.
Maye we click the browser back button instead. Maybe we click the "Heroes" link. Maybe we click the browser back button instead. Maybe we click the "Heroes" link.
Do either. Up pops a dialog box. Do either. Up pops a dialog box.
figure.image-display figure.image-display
@ -285,7 +285,7 @@ figure.image-display
We should only need this trick for the live example, not production code. We should only need this trick for the live example, not production code.
:marked :marked
### Booting with the router service providers ### Booting with the router service providers
Our app launches from the `boot.ts` file in the `~/app` folder so let's start there. Our app launches from the `boot.ts` file in the `/app` folder so let's start there.
It's short and all of it is relevant to routing. It's short and all of it is relevant to routing.
+makeExample('router/ts/app/boot.1.ts','all', 'boot.ts')(format=".") +makeExample('router/ts/app/boot.1.ts','all', 'boot.ts')(format=".")
:marked :marked
@ -438,7 +438,7 @@ figure.image-display
That's unrealistic and ultimately not maintainable. That's unrealistic and ultimately not maintainable.
We think it's better to put each feature area in its own folder. We think it's better to put each feature area in its own folder.
Our first step is to **create a separate `app/heroes/` folder**. Our first step is to **create a separate `app/heroes/` folder**
and add *Hero Management* feature files there. and add *Hero Management* feature files there.
We won't be creative about it. Our example is pretty much a We won't be creative about it. Our example is pretty much a
@ -467,8 +467,8 @@ figure.image-display
.file hero-list.component.ts .file hero-list.component.ts
.file hero.service.ts .file hero.service.ts
:marked :marked
We'll provide the `HeroService` during bootstrapping We provide the `HeroService` in the application root `AppComponent`
so that is available everywhere in the app (see `boot.ts`) . so that is available everywhere in the app.
Now it's time for some surgery to bring these files and the rest of the app Now it's time for some surgery to bring these files and the rest of the app
into alignment with our application router. into alignment with our application router.
@ -663,8 +663,8 @@ code-example(format="." language="bash").
`AppComponent` route configuration. If we rename a *Crisis Center* component or change a route definition, `AppComponent` route configuration. If we rename a *Crisis Center* component or change a route definition,
we'll be changing the `AppComponent` too. we'll be changing the `AppComponent` too.
* If we followed *Heroes* lead, we'd be adding the `CrisisService` to the providers in `boot.ts`. * If we followed *Heroes* lead, we'd be adding the `CrisisService` to the providers in `app.component.ts`.
Now both `HeroService` and `CrisisService` would be available everywhere although Then both `HeroService` and `CrisisService` would be available everywhere although
they're only needed in their respective feature modules. That stinks. they're only needed in their respective feature modules. That stinks.
Changes to a sub-module such as *Crisis Center* shouldn't provoke changes to the `AppComponent` or `boot.ts`. Changes to a sub-module such as *Crisis Center* shouldn't provoke changes to the `AppComponent` or `boot.ts`.
@ -746,19 +746,28 @@ code-example(format="." language="bash").
### Child Route Configuration ### Child Route Configuration
The `CrisisCenterComponent` is a *Routing Component* like the `AppComponent`. The `CrisisCenterComponent` is a *Routing Component* like the `AppComponent`.
The `@RouteConfig` decorator that adorns the `CrisisCenterComponent` class defines routes in the same way The `@RouteConfig` decorator that adorns the `CrisisCenterComponent` class defines routes in much the same way
that we did earlier. 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.ts', 'route-config', 'app/crisis-center/crisis-center.component.ts (routes only)' )(format=".")
:marked :marked
There are three *Crisis Center* routes, two of them with an `id` parameter. There is an *important difference in the paths*. They both begin at `/`.
They refer to components we haven't talked about yet but whose purpose we Normally such paths would refer to the root of the application.
can guess by their names. Here they refer to the **root of the child component!**.
We cannot tell by looking at the `CrisisCenterComponent` that it is a child component The Component Router composes the final route by concatenating route paths beginning with the ancestor paths to this child router.
of an application. We can't tell that its routes are child routes. In our example, there is one ancestor path: "crisis-center".
The final route to the `HeroDetailComponent` displaying hero 11 would be something like:
code-example(format="").
localhost:3000//crisis-center/11
:marked
We cannot know this simply by looking at the `CrisisCenterComponent` alone.
We can't tell that it is a *child* routing component.
We can't tell that its routes are child routes; they are indistinguiable from top level application routes.
That's intentional. The *Crisis Center* shouldn't know that it is the child of anything. Such ignorance is intentional. The *Crisis Center* shouldn't know that it is the child of anything.
It might be the top level component of its own application. It might be repurposed in a different application. Today it is a child component one level down.
Tomorrow it might be the top level component of its own application.
Next month it might be re-purposed in a different application.
The *Crisis Center* itself is indifferent to these possibilities. The *Crisis Center* itself is indifferent to these possibilities.
*We* make it a child component of our application by reconfiguring the routes of the top level `AppComponent`. *We* make it a child component of our application by reconfiguring the routes of the top level `AppComponent`.
@ -767,8 +776,9 @@ code-example(format="." language="bash").
Here is is the revised route configuration for the parent `AppComponent`: Here is is the revised route configuration for the parent `AppComponent`:
+makeExample('router/ts/app/app.component.ts', 'route-config', 'app/app.component.ts (routes only)' ) +makeExample('router/ts/app/app.component.ts', 'route-config', 'app/app.component.ts (routes only)' )
:marked :marked
The second and third *Hero* routes haven't changed. The last two *Hero* routes haven't changed.
The first *Crisis Center* route has changed &mdash; *signficantly* &mdash; and we've formatted it to draw attention to the differences:
The first *Crisis Center* route has changed &mdash; *significantly* &mdash; and we've formatted it to draw attention to the differences:
+makeExample('router/ts/app/app.component.ts', 'route-config-cc')(format=".") +makeExample('router/ts/app/app.component.ts', 'route-config-cc')(format=".")
:marked :marked
Notice that the **path ends with a slash and three trailing periods (`/...`)**. Notice that the **path ends with a slash and three trailing periods (`/...`)**.
@ -794,7 +804,7 @@ code-example(format="." language="bash").
It could be any of the three. In the absence of additional information, the router can't decide and must throw an error. It could be any of the three. In the absence of additional information, the router can't decide and must throw an error.
We've tried the sample application and it didn't fail. We must have done something. 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. 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.ts', 'default-route', 'app/crisis-center/crisis-center.component.ts (default route)')(format=".")
@ -879,9 +889,11 @@ code-example(format="").
by waiting for the user's answer asynchronously. Waiting for the user asynchronously by waiting for the user's answer asynchronously. Waiting for the user asynchronously
is like waiting for the server asynchronously. is like waiting for the server asynchronously.
:marked :marked
The dialog service returns a [promise](http://www.html5rocks.com/en/tutorials/es6/promises/). The `DialogService` (injected in the `AppComponent` for app-wide use) does the asking.
The promise *resolves* when the user eventually decides
to discard changes and navigate away (`true`) or keep the pending changes and stay in the crisis editor (`false`). It returns a [promise](http://www.html5rocks.com/en/tutorials/es6/promises/) that
*resolves* when the user eventually decides what to do: either
to discard changes and navigate away (`true`) or to preserve the pending changes and stay in the crisis editor (`false`).
<a id="CanDeactivate"></a> <a id="CanDeactivate"></a>
<a id="routerCanDeactivate"></a> <a id="routerCanDeactivate"></a>
@ -890,17 +902,18 @@ code-example(format="").
+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.ts', 'routerCanDeactivate', 'crisis-detail.component.ts (excerpt)')
:marked :marked
Notice that the `routerCanDeactivate` method *can* return synchronously; Notice that the `routerCanDeactivate` method *can* return synchronously;
it returns `true` immediately if there are no pending changes. it returns `true` immediately if there is no crisis or there are no pending changes.
But it can also return a promise and the router will wait for that promise But it can also return a promise and the router will wait for that promise to resolve to truthy (navigate) or falsey (stay put).
to resolve before navigating away or staying put.
**Two critical points** **Two critical points**
1. The router hook is optional. We don't inherit from a base class. We simply implement the method or not. 1. The router hook is optional. We don't inherit from a base class. We simply implement the interface method or not.
1. We rely on the router to call the hook. We don't worry about all the ways that the user 1. We rely on the router to call the hook. We don't worry about all the ways that the user
could navigate away. That's the router's job. could navigate away. That's the router's job.
We simply write this method and let the router take it from there. 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).
<a id="final-app"></a> <a id="final-app"></a>
.l-main-section .l-main-section
:marked :marked