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
template: `
<h1>Component Router</h1>
<a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a>
<nav>
<a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,
// #enddocregion template

View File

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

View File

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

View File

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

View File

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

View File

@ -4,16 +4,11 @@
// #docplaster
// #docregion v2
// #docregion hash-strategy
import {bootstrap} from 'angular2/platform/browser';
import {ROUTER_PROVIDERS} from 'angular2/router';
import {AppComponent} from './app.component';
// #enddocregion hash-strategy
import {HeroService} from './heroes/hero.service';
// #enddocregion v2
// #docregion hash-strategy
// Add these symbols to register a `LocationStrategy`
import {provide} from 'angular2/core';
import {LocationStrategy,
@ -21,27 +16,23 @@ import {LocationStrategy,
// #enddocregion hash-strategy
/* Can't use AppComponent ... but display as if we can
// #docregion v2,hash-strategy
// #docregion v2, hash-strategy
bootstrap(AppComponent, [
// #enddocregion v2,hash-strategy
// #enddocregion v2, hash-strategy
*/
// Actually use the v.2 component
import {AppComponent as ac} from './app.component.2';
bootstrap(ac, [
// #docregion v2, hash-strategy
ROUTER_PROVIDERS,
// #enddocregion v2, hash-strategy
// #docregion hash-strategy
provide(LocationStrategy,
{useClass: HashLocationStrategy}), // ~/src/#/crisis-center/
{useClass: HashLocationStrategy}) // ~/#/crisis-center/
// #enddocregion hash-strategy
// #docregion v2
HeroService,
// #docregion hash-strategy
ROUTER_PROVIDERS
// #docregion v2, hash-strategy
]);
// #enddocregion hash-strategy
// #enddocregion v2
// #enddocregion v2, hash-strategy

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -202,7 +202,7 @@ figure.image-display
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***.
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.
figure.image-display
@ -285,7 +285,7 @@ figure.image-display
We should only need this trick for the live example, not production code.
:marked
### 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.
+makeExample('router/ts/app/boot.1.ts','all', 'boot.ts')(format=".")
:marked
@ -438,7 +438,7 @@ figure.image-display
That's unrealistic and ultimately not maintainable.
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.
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.service.ts
:marked
We'll provide the `HeroService` during bootstrapping
so that is available everywhere in the app (see `boot.ts`) .
We provide the `HeroService` in the application root `AppComponent`
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
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,
we'll be changing the `AppComponent` too.
* If we followed *Heroes* lead, we'd be adding the `CrisisService` to the providers in `boot.ts`.
Now both `HeroService` and `CrisisService` would be available everywhere although
* If we followed *Heroes* lead, we'd be adding the `CrisisService` to the providers in `app.component.ts`.
Then both `HeroService` and `CrisisService` would be available everywhere although
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`.
@ -746,19 +746,28 @@ code-example(format="." language="bash").
### Child Route Configuration
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.
+makeExample('router/ts/app/crisis-center/crisis-center.component.ts', 'route-config', 'app/crisis-center/crisis-center.component.ts (routes only)' )(format=".")
:marked
There are three *Crisis Center* routes, two of them with an `id` parameter.
They refer to components we haven't talked about yet but whose purpose we
can guess by their names.
There is an *important difference in the paths*. They both begin at `/`.
Normally such paths would refer to the root of the application.
Here they refer to the **root of the child component!**.
The Component Router composes the final route by concatenating route paths beginning with the ancestor paths to this child router.
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.
We cannot tell by looking at the `CrisisCenterComponent` that it is a child component
of an application. We can't tell that its routes are child routes.
That's 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.
Such ignorance is intentional. The *Crisis Center* shouldn't know that it is the child of anything.
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.
*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`:
+makeExample('router/ts/app/app.component.ts', 'route-config', 'app/app.component.ts (routes only)' )
:marked
The second and third *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 last two *Hero* routes haven't changed.
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=".")
:marked
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.
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.
+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
is like waiting for the server asynchronously.
:marked
The dialog service returns a [promise](http://www.html5rocks.com/en/tutorials/es6/promises/).
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`).
The `DialogService` (injected in the `AppComponent` for app-wide use) does the asking.
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="routerCanDeactivate"></a>
@ -890,16 +902,17 @@ code-example(format="").
+makeExample('router/ts/app/crisis-center/crisis-detail.component.ts', 'routerCanDeactivate', 'crisis-detail.component.ts (excerpt)')
:marked
Notice that the `routerCanDeactivate` method *can* return synchronously;
it returns `true` immediately if there are no pending changes.
But it can also return a promise and the router will wait for that promise
to resolve before navigating away or staying put.
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 to resolve to truthy (navigate) or falsey (stay put).
**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
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).
<a id="final-app"></a>
.l-main-section