diff --git a/public/docs/_examples/router/ts/.gitignore b/public/docs/_examples/router/ts/.gitignore new file mode 100644 index 0000000000..2cb7d2a2e9 --- /dev/null +++ b/public/docs/_examples/router/ts/.gitignore @@ -0,0 +1 @@ +**/*.js diff --git a/public/docs/_examples/router/ts/app/app.component.1.ts b/public/docs/_examples/router/ts/app/app.component.1.ts new file mode 100644 index 0000000000..62f397bb07 --- /dev/null +++ b/public/docs/_examples/router/ts/app/app.component.1.ts @@ -0,0 +1,39 @@ +/* First version */ +// #docplaster + +// #docregion +import {Component} from 'angular2/core'; +import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router'; + +import {CrisisListComponent} from './crisis-list.component'; +import {HeroListComponent} from './hero-list.component'; + +@Component({ + selector: 'my-app', +// #docregion template + template: ` +

Component Router

+ Crisis Center + Heroes + + `, +// #enddocregion template + directives: [ROUTER_DIRECTIVES] +}) +// #enddocregion +/* +// #docregion route-config +@Component({ ... }) +// #enddocregion route-config +*/ +// #docregion +// #docregion route-config +@RouteConfig([ +// #docregion route-defs + {path:'/crisis-center', name: 'CrisisCenter', component: CrisisListComponent}, + {path:'/heroes', name: 'Heroes', component: HeroListComponent} +// #enddocregion route-defs +]) +export class AppComponent { } +// #enddocregion route-config +// #enddocregion diff --git a/public/docs/_examples/router/ts/app/app.component.2.ts b/public/docs/_examples/router/ts/app/app.component.2.ts new file mode 100644 index 0000000000..f6be5fe186 --- /dev/null +++ b/public/docs/_examples/router/ts/app/app.component.2.ts @@ -0,0 +1,43 @@ +/* Second Heroes version */ +// #docplaster + +// #docregion +import {Component} from 'angular2/core'; +import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router'; + +import {CrisisListComponent} from './crisis-list.component'; +// #docregion hero-import +import {HeroListComponent} from './heroes/hero-list.component'; +import {HeroDetailComponent} from './heroes/hero-detail.component'; +// #enddocregion hero-import + +@Component({ + selector: 'my-app', + template: ` +

Component Router

+ Crisis Center + Heroes + + `, + directives: [ROUTER_DIRECTIVES] +}) +// #enddocregion +/* +// #docregion route-config +@Component({ ... }) +// #enddocregion route-config +*/ +// #docregion +// #docregion route-config +@RouteConfig([ +// #docregion route-defs + {path:'/crisis-center', name: 'CrisisCenter', component: CrisisListComponent}, + {path:'/heroes', name: 'Heroes', component: HeroListComponent}, + // #docregion hero-detail-route + {path:'/hero/:id', name: 'HeroDetail', component: HeroDetailComponent} + // #enddocregion hero-detail-route +// #enddocregion route-defs +]) +export class AppComponent { } +// #enddocregion route-config +// #enddocregion diff --git a/public/docs/_examples/router/ts/app/app.component.ts b/public/docs/_examples/router/ts/app/app.component.ts new file mode 100644 index 0000000000..4d6761d162 --- /dev/null +++ b/public/docs/_examples/router/ts/app/app.component.ts @@ -0,0 +1,39 @@ +// #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'; + +@Component({ + selector: 'my-app', +// #docregion template + template: ` +

Component Router

+ Crisis Center + Heroes + + `, +// #enddocregion template + directives: [ROUTER_DIRECTIVES] +}) +// #docregion route-config +@RouteConfig([ + + // #docregion route-config-cc + { // Crisis Center child route + path: '/crisis-center/...', + name: 'CrisisCenter', + component: CrisisCenterComponent, + useAsDefault: true + }, + // #enddocregion route-config-cc + + {path: '/heroes', name: 'Heroes', component: HeroListComponent}, + {path: '/hero/:id', name: 'HeroDetail', component: HeroDetailComponent}, + {path: '/*other', redirectTo: ['Heroes']}, +]) +// #enddocregion route-config +export class AppComponent { } diff --git a/public/docs/_examples/router/ts/app/boot.1.ts b/public/docs/_examples/router/ts/app/boot.1.ts new file mode 100644 index 0000000000..6b8d17bd4d --- /dev/null +++ b/public/docs/_examples/router/ts/app/boot.1.ts @@ -0,0 +1,23 @@ +/* First version */ +// #docplaster + +// #docregion all +import {AppComponent} from './app.component'; +import {bootstrap} from 'angular2/platform/browser'; +import {ROUTER_PROVIDERS} from 'angular2/router'; + +// #enddocregion all + +/* Can't use AppComponent ... but display as if we can +// #docregion all +bootstrap(AppComponent, [ +// #enddocregion all +*/ + +// Actually use the v.1 component +import {AppComponent as ac} from './app.component.1'; +bootstrap(ac, [ +// #docregion all + ROUTER_PROVIDERS, +]); +// #enddocregion all \ No newline at end of file diff --git a/public/docs/_examples/router/ts/app/boot.2.ts b/public/docs/_examples/router/ts/app/boot.2.ts new file mode 100644 index 0000000000..e020baa014 --- /dev/null +++ b/public/docs/_examples/router/ts/app/boot.2.ts @@ -0,0 +1,47 @@ +/* Second version */ +// For Milestone #2 +// Also includes digression on HashPathStrategy (not used in the final app) +// #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, + HashLocationStrategy} from 'angular2/router'; +// #enddocregion hash-strategy + +/* Can't use AppComponent ... but display as if we can +// #docregion v2,hash-strategy + +bootstrap(AppComponent, [ +// #enddocregion v2,hash-strategy +*/ + +// Actually use the v.2 component +import {AppComponent as ac} from './app.component.2'; + +bootstrap(ac, [ +// #docregion hash-strategy + + provide(LocationStrategy, + {useClass: HashLocationStrategy}), // ~/src/#/crisis-center/ + +// #enddocregion hash-strategy +// #docregion v2 + HeroService, +// #docregion hash-strategy + ROUTER_PROVIDERS +]); +// #enddocregion hash-strategy +// #enddocregion v2 + diff --git a/public/docs/_examples/router/ts/app/boot.ts b/public/docs/_examples/router/ts/app/boot.ts new file mode 100644 index 0000000000..05a9cfec23 --- /dev/null +++ b/public/docs/_examples/router/ts/app/boot.ts @@ -0,0 +1,13 @@ +// #docregion +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 +]); diff --git a/public/docs/_examples/router/ts/app/crisis-center/add-crisis.component.ts b/public/docs/_examples/router/ts/app/crisis-center/add-crisis.component.ts new file mode 100644 index 0000000000..48eae1be18 --- /dev/null +++ b/public/docs/_examples/router/ts/app/crisis-center/add-crisis.component.ts @@ -0,0 +1,41 @@ +import {Component} from 'angular2/core'; +import {Crisis, CrisisService} from './crisis.service'; +import {DialogService} from '../dialog.service'; +import {CanDeactivate, ComponentInstruction, Router} from 'angular2/router'; + +@Component({ + template: ` +

"{{editName}}"

+
+ + +
+ + + `, + styles: ['input {width: 20em}'] +}) +export class AddCrisisComponent implements CanDeactivate { + public editName: string; + + constructor( + private _service: CrisisService, + private _router: Router, + private _dialog: DialogService) { } + + routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + return !!this.editName.trim() || + this._dialog.confirm('Discard changes?'); + } + + cancel() { this.gotoCrises(); } + + save() { + this._service.addCrisis(this.editName); + this.gotoCrises(); + } + + gotoCrises() { + this._router.navigate(['CrisisCenter']); + } +} \ No newline at end of file diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.component.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.component.ts new file mode 100644 index 0000000000..a30840d310 --- /dev/null +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.component.ts @@ -0,0 +1,29 @@ +// #docregion +import {Component} from 'angular2/core'; +import {RouteConfig, RouterOutlet} from 'angular2/router'; + +import {CrisisListComponent} from './crisis-list.component'; +import {CrisisDetailComponent} from './crisis-detail.component'; +import {CrisisService} from './crisis.service'; + +// #docregion minus-imports +@Component({ + template: ` +

CRISIS CENTER

+ + `, + directives: [RouterOutlet], +// #docregion providers + providers: [CrisisService] +// #enddocregion providers +}) +// #docregion route-config +@RouteConfig([ + {path:'/', name: 'CrisisCenter', component: CrisisListComponent, useAsDefault: true}, + {path:'/list/:id', name: 'CrisisList', component: CrisisListComponent}, + {path:'/:id', name: 'CrisisDetail', component: CrisisDetailComponent} +]) +// #enddocregion route-config +export class CrisisCenterComponent { } +// #enddocregion minus-imports +// #enddocregion diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-detail.component.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-detail.component.ts new file mode 100644 index 0000000000..632b8c3a3f --- /dev/null +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-detail.component.ts @@ -0,0 +1,90 @@ +// #docplaster + +// #docregion +import {Component, OnInit} from 'angular2/core'; +import {Crisis, CrisisService} from './crisis.service'; +import {RouteParams, Router} from 'angular2/router'; +// #docregion ngCanDeactivate +import {CanDeactivate, ComponentInstruction} from 'angular2/router'; +import {DialogService} from '../dialog.service'; + +// #enddocregion ngCanDeactivate + +@Component({ + // #docregion template + template: ` +
+

"{{editName}}"

+
+ {{crisis.id}}
+
+ + +
+ + +
+ `, + // #enddocregion template + styles: ['input {width: 20em}'] +}) +// #docregion ngCanDeactivate, cancel-save +export class CrisisDetailComponent implements OnInit, CanDeactivate { + + public crisis: Crisis; + public editName: string; + +// #enddocregion ngCanDeactivate, 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 canDeactivate + routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + return !this.crisis || + this.crisis.name === this.editName || + this._dialog.confirm('Discard changes?'); + } + // #enddocregion canDeactivate + + // #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() { + let route = + ['CrisisList', {id: this.crisis ? this.crisis.id : null} ] + + this._router.navigate(route); + } + // #enddocregion gotoCrises +// #docregion ngCanDeactivate, cancel-save +} +// #enddocregion ngCanDeactivate, cancel-save +// #enddocregion diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-list.component.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-list.component.ts new file mode 100644 index 0000000000..9916027546 --- /dev/null +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-list.component.ts @@ -0,0 +1,45 @@ +// #docplaster + +// #docregion +import {Component, OnInit} from 'angular2/core'; +import {Crisis, CrisisService} from './crisis.service'; +import {Router, RouteParams} from 'angular2/router'; + +@Component({ + template: ` + + `, +}) +export class CrisisListComponent implements OnInit { + public crises: Crisis[]; + // #docregion isSelected + private _selectedId: number; + + constructor( + private _service: CrisisService, + private _router: Router, + routeParams: RouteParams) { + this._selectedId = +routeParams.get('id'); + } + // #enddocregion isSelected + + 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 +} diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis.service.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis.service.ts new file mode 100644 index 0000000000..d6fb07ddf9 --- /dev/null +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis.service.ts @@ -0,0 +1,41 @@ +// #docplaster + +// #docregion +import {Injectable} from 'angular2/core'; + +export class Crisis { + constructor(public id: number, public name: string) { } +} + +@Injectable() +export class CrisisService { + getCrises() { return crisesPromise; } + + getCrisis(id: number | string) { + return crisesPromise + .then(crises => crises.filter(c => c.id === +id)[0]); + } + +// #enddocregion + + static nextCrisisId = 100; + + addCrisis(name:string) { + name = name.trim(); + if (name){ + let crisis = new Crisis(CrisisService.nextCrisisId++, name); + crisesPromise.then(crises => crises.push(crisis)); + } + } +// #docregion +} + +var crises = [ + new Crisis(1, 'Princess Held Captive'), + new Crisis(2, 'Dragon Burning Cities'), + new Crisis(3, 'Giant Asteroid Heading For Earth'), + new Crisis(4, 'Release Deadline Looms') +]; + +var crisesPromise = Promise.resolve(crises); +// #enddocregion diff --git a/public/docs/_examples/router/ts/app/crisis-list.component.ts b/public/docs/_examples/router/ts/app/crisis-list.component.ts new file mode 100644 index 0000000000..b712137d05 --- /dev/null +++ b/public/docs/_examples/router/ts/app/crisis-list.component.ts @@ -0,0 +1,10 @@ +// Initial empty version +// #docregion +import {Component} from 'angular2/core'; + +@Component({ + template: ` +

CRISIS CENTER

+

Get your crisis here

` +}) +export class CrisisListComponent { } diff --git a/public/docs/_examples/router/ts/app/dialog.service.ts b/public/docs/_examples/router/ts/app/dialog.service.ts new file mode 100644 index 0000000000..a25f9c68d2 --- /dev/null +++ b/public/docs/_examples/router/ts/app/dialog.service.ts @@ -0,0 +1,18 @@ +// #docregion +import {Injectable} from 'angular2/core'; +/** + * Async modal dialog service + * DialogService makes this app easier to test by faking this service. + * TODO: better modal implemenation that doesn't use window.confirm + */ +@Injectable() +export class DialogService { + /** + * Ask user to confirm an action. `message` explains the action and choices. + * Returns promise resolving to `true`=confirm or `false`=cancel + */ + confirm(message?:string) { + return new Promise((resolve, reject) => + resolve(window.confirm(message || 'Is it OK?'))); + }; +} diff --git a/public/docs/_examples/router/ts/app/hero-list.component.ts b/public/docs/_examples/router/ts/app/hero-list.component.ts new file mode 100644 index 0000000000..d1abf925de --- /dev/null +++ b/public/docs/_examples/router/ts/app/hero-list.component.ts @@ -0,0 +1,10 @@ +/// Initial empty version +// #docregion +import {Component} from 'angular2/core'; + +@Component({ + template: ` +

HEROES

+

Get your heroes here

` +}) +export class HeroListComponent { } diff --git a/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts b/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts new file mode 100644 index 0000000000..4b4170d3d2 --- /dev/null +++ b/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts @@ -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: ` +

HEROES

+
+

"{{hero.name}}"

+
+ {{hero.id}}
+
+ + +
+ +
+ `, +}) +export class HeroDetailComponent implements OnInit { + public 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() { + // Heroes + this._router.navigate(['Heroes']); + } + // #enddocregion gotoHeroes +} diff --git a/public/docs/_examples/router/ts/app/heroes/hero-list.component.ts b/public/docs/_examples/router/ts/app/heroes/hero-list.component.ts new file mode 100644 index 0000000000..2ea53b6f68 --- /dev/null +++ b/public/docs/_examples/router/ts/app/heroes/hero-list.component.ts @@ -0,0 +1,47 @@ +// #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: ` +

HEROES

+ + ` + // #enddocregion template +}) +export class HeroListComponent implements OnInit { + public heroes: Hero[]; + public selectedHero: 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) { + this._router.navigate( ['HeroDetail', { id: hero.id }] ); + } + // #enddocregion select +} +// #enddocregion + +/* A link parameters array +// #docregion link-parameters-array +['HeroDetail', { id: hero.id }] // {id: 15} +// #enddocregion link-parameters-array +*/ \ No newline at end of file diff --git a/public/docs/_examples/router/ts/app/heroes/hero.service.ts b/public/docs/_examples/router/ts/app/heroes/hero.service.ts new file mode 100644 index 0000000000..f85d353d2d --- /dev/null +++ b/public/docs/_examples/router/ts/app/heroes/hero.service.ts @@ -0,0 +1,27 @@ +// #docregion +import {Injectable} from 'angular2/core'; + +export class Hero { + constructor(public id: number, public name: string) { } +} + +@Injectable() +export class HeroService { + getHeroes() { return heroesPromise; } + + getHero(id: number | string) { + return heroesPromise + .then(heroes => heroes.filter(h => h.id === +id)[0]); + } +} + +var HEROES = [ + new Hero(11, 'Mr. Nice'), + new Hero(12, 'Narco'), + new Hero(13, 'Bombasto'), + new Hero(14, 'Celeritas'), + new Hero(15, 'Magneta'), + new Hero(16, 'RubberMan') +]; + +var heroesPromise = Promise.resolve(HEROES); diff --git a/public/docs/_examples/router/ts/example-config.json b/public/docs/_examples/router/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/router/ts/index.1.html b/public/docs/_examples/router/ts/index.1.html new file mode 100644 index 0000000000..c8b08122be --- /dev/null +++ b/public/docs/_examples/router/ts/index.1.html @@ -0,0 +1,33 @@ + + + + + + + + + Router Sample + + + + + + + + + + + + loading... + + + + diff --git a/public/docs/_examples/router/ts/index.html b/public/docs/_examples/router/ts/index.html new file mode 100644 index 0000000000..309ee443f4 --- /dev/null +++ b/public/docs/_examples/router/ts/index.html @@ -0,0 +1,29 @@ + + + + + + + + Router Sample + + + + + + + + + + + + loading... + + + + diff --git a/public/docs/_examples/router/ts/plnkr.json b/public/docs/_examples/router/ts/plnkr.json new file mode 100644 index 0000000000..741f87caa6 --- /dev/null +++ b/public/docs/_examples/router/ts/plnkr.json @@ -0,0 +1,12 @@ +{ + "description": "Router", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2,3].*", + "!app/crisis-list.component.ts", + "!app/hero-list.component.ts", + "!app/crisis-center/add-crisis.component.ts" + ], + "tags": ["router"] +} \ No newline at end of file diff --git a/public/docs/_examples/router/ts/styles.css b/public/docs/_examples/router/ts/styles.css new file mode 100644 index 0000000000..bb2b2c9cd9 --- /dev/null +++ b/public/docs/_examples/router/ts/styles.css @@ -0,0 +1,36 @@ +/* #docregion */ +/* #docregion heroes */ +/* #docregion starter */ +h1 {color: #369; font-family: Arial, Helvetica, sans-serif; font-size: 250%;} +h2 { color: #369; font-family: Arial, Helvetica, sans-serif; } +h3 { color: #444; font-weight: lighter; } +body { margin: 2em; } +body, input[text], button { color: #888; font-family: Cambria, Georgia; } +button {padding: 0.2em; font-size: 14px} + +ul {list-style-type: none; margin-left: 1em; padding: 0; width: 20em;} + +li { cursor: pointer; position: relative; left: 0; transition: all 0.2s ease; } +li:hover {color: #369; background-color: #EEE; left: .2em;} + +/* route-link anchor tags */ +a {padding: 5px; text-decoration: none; font-family: Arial, Helvetica, sans-serif; } +a:visited, a:link {color: #444;} +a:hover {color: white; background-color: #1171a3; } +a.router-link-active {color: white; background-color: #52b9e9; } + +/* #enddocregion starter */ +.selected { background-color: #EEE; color: #369; } + +.badge { + font-size: small; + color: white; + padding: 0.1em 0.7em; + background-color: #369; + line-height: 1em; + position: relative; + left: -1px; + top: -1px; +} +/* #enddocregion heroes */ +/* #enddocregion */ \ No newline at end of file diff --git a/public/docs/_examples/template-syntax/ts/app/app.component.ts b/public/docs/_examples/template-syntax/ts/app/app.component.ts index 31ed7ad6f8..27436547f2 100644 --- a/public/docs/_examples/template-syntax/ts/app/app.component.ts +++ b/public/docs/_examples/template-syntax/ts/app/app.component.ts @@ -122,7 +122,7 @@ export class AppComponent { // #docregion setStyles2 setStyles2() { return { - // camelCase style properties works too + // camelCase style properties work too fontStyle: this.canSave ? 'italic' : 'normal', // italic fontWeight: !this.isUnchanged ? 'bold' : 'normal', // normal fontSize: this.isSpecial ? 'x-large': 'smaller', // larger diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index 1a8f3c296e..adfa83188e 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -43,6 +43,11 @@ "intro": "Pipes transform displayed values within a template" }, + "router": { + "title": "Routing & Navigation", + "intro": "Discover the basics of screen navigation with the Angular 2 router" + }, + "attribute-directives": { "title": "Attribute Directives", "intro": "Attribute directives attach behavior to elements." diff --git a/public/docs/ts/latest/guide/glossary.jade b/public/docs/ts/latest/guide/glossary.jade index c7b5cfb0fb..57b284b973 100644 --- a/public/docs/ts/latest/guide/glossary.jade +++ b/public/docs/ts/latest/guide/glossary.jade @@ -399,9 +399,21 @@ and taking other similar actions that cause the application to replace one view with another. - The [Angular router](./router.html) is a richly featured mechanism for configuring - and managing the entire navigation process including the creation and destruction + The Angular [Component Router](./router.html) is a richly featured mechanism for configuring + and managing the entire view navigation process including the creation and destruction of views. +:marked + ## Routing Component +.l-sub-section + :marked + A [Component](#component) with an attached router. + + In most cases, the component became attached to a [router](#router) by means + of a `@RouterConfig` decorator that defined routes to views controlled by this component. + + The component's template has a `RouterOutlet` element where it can display views produced by the router. + + It likely has anchor tags or buttons with `RouterLink` directives that users can click to navigate. .l-main-section diff --git a/public/docs/ts/latest/guide/router-aux.jade b/public/docs/ts/latest/guide/router-aux.jade new file mode 100644 index 0000000000..e2074c45ce --- /dev/null +++ b/public/docs/ts/latest/guide/router-aux.jade @@ -0,0 +1,183 @@ +// + TODO: REVIVE AUX ROUTE MATERIAL WHEN THAT FEATURE WORKS AS EXPECTED + + + .l-main-section + :marked + ## Milestone #4: Auxiliary Routes + Auxiliary routes are routes that can be activated independently of the current + route. They are entirely optional, depending on your app needs. + + For example, your application may have a modal that appears and this could + be an auxiliary route. The modal may have its own navigation needs, such as a slideshow + and that auxiliary route is able to manage the navigation stack independently of the + primary routes. + + In our sample application, we also want to have a chat feature that allows people + the ability to have a live agent assist them. The chat window will have first an + initial route that contains a prompt to ask the visitor if they'd like to chat with + an agent. Once they initiate a chat, they go to a new route for the chat experience. + + .alert.is-critical Make diagram of chat routes + + :marked + In this auxiliary chat experience, it overlays the current screen and persists. + If you navigate from the Heros to Crisis Center, the chat auxiliary route remains + active and in view. + + Therefore the auxiliary routing is truly independent of the other + routing. In most respects, an auxiliary route behaves the same outside of it is rendered + in its own outlet and modifies the url differently. + + We'll look at how to setup an auxiliary route and considerations for when to use them. + + ### Auxiliary Route Outlet + In order to get an auxiliary route, it needs a place to be rendered. So far the app has + a single `RouterOutet` that the rest of our routes are rendered into. Auxiliary routes need to + have their own `RouterOutlet`, and that is done by giving it a name attribute. Open the + `app.component.ts` file and let's add the new outlet to the template. + .alert.is-critical Should remove app.component.4.ts based example (next) when we know what's what + +_makeExample('router/ts/app/app.component.4.ts', 'chat-outlet', 'app/app.component.ts') + .alert.is-critical Should be able to project from app.component.ts like this + +_makeExample('router/ts/app/app.component.ts', 'template', 'app/app.component.ts (excerpt)') + :marked + The name of the outlet must be unique to the component. You could reuse the name across + different components, so you don't have to worry about collisions. + + Here we give it a name of "chat", which will be used by the router when we setup our + route configs. The app component needs to know about this Auxiliary route, so we + import the `ChatComponent`, add a new ROUTE_NAME (`chat`), + and add a new 'Chat' route to the `ROUTES` in `app.routes.ts` (just below the redirect) . + +_makeExample('router/ts/app/routes.ts', null, 'app/routes.ts') + :marked + Look again at the 'Chat' route + +_makeExample('router/ts/app/routes.ts','chat-route') + :marked + You can see the route definition is nearly the same, except instead of `path` there is + an `aux`. The `aux` property makes this an Auxiliary route. + + @TODO Explain how a named outlet is paired with an aux route. + + The chat component defines its own routes just like the other components, even though + it is an Auxiliary route. + + +_makeExample('router/ts/app/chat/routes.ts', null, 'app/chat/routes.ts') + :marked + Even though this is an Auxiliary route, you notice there is no difference in how we've + configured the route config for the primary chat component. The chat component also has + the `RouterOutlet` Directive in the template so the child components render inside of + the chat window. + + In the chat components, we can use `RouterLink` to reference routes just the same as + a normal route. Since this is inside of an Auxiliary route, these relative links will + resolve within the chat component and not change the primary route (the Crisis Center or + Heros pages). + + +_makeExample('router/ts/app/chat/chat-init.component.ts', 'chat-links') + + :marked + When the chat component opens, it first initializes this template to ask the user if + they'd like to chat or not. If they agree, it takes them to the chat window where they + begin to send messages to the 'live' agent. + + The behavior of the chat components may be interesting, but have no additional insights + for routing, except for the ability to deactivate an active Auxiliary route. + + ### Exiting an Auxiliary Route + + @TODO Figure out how to close/deactivate an aux route + + ### Auxiliary Route URLs + + Auxiliary Routes do modify the url using parens, like so. + code-example(format=".", language="bash"). + localhost:3002/crisis-center(chat)/2(details) + :marked + This would be the url on a page where the user was viewing an item in the Crisis Center, + in this case the "Dragon Burning Cities" crisis, and the `(chat)` Auxiliary Route would + active and on the details child route. + + ### Multiple Auxiliary Routes + + There is no limit to how many Auxiliary Routes you have defined or active. There is probably + a practical limit where too much appears on the screen for a user, but you can have as many + Auxiliary Routes as you have named `RouteOutlet`s. + + :marked + ### Auxiliary Route Summary + + * Auxiliary routes are normal routes that are rendered outside of the primary `RouterOutlet` + * They must use a named `RouterOutlet` to render. + * Can be activated as long as the parent component is active. + * Links inside of child components are resolved against the aux parent component. + * Auxiliary routes are deactivated by @TODO? + * Routes are indicated in the url using parens. + * Multiple aux routes can be active at once. + + ### Chat + The "Chat" feature area within the `chat` folder looks like this: + ``` + app/ + chat/ + ├── chat-detail.component.ts + ├── chat-init.component.ts + ├── chat.component.ts + ├── chat.service.ts + └── routes.ts + ``` + +_makeTabs( + `router/ts/app/chat/chat.component.ts, + router/ts/app/chat/routes.ts, + router/ts/app/chat/chat-init.component.ts, + router/ts/app/chat/chat-detail.component.ts, + router/ts/app/chat/chat.service.ts + `, + null, + `chat.component.ts, + chat/routes.ts, + chat-init.component.ts, + chat-detail.component.ts, + chat.service.ts, + `) + + + The following are styles extracted from `styles.css` + that only belong if/when we add chat back + ``` + /* chat styles */ + .chat { + position: fixed; + bottom: 0; + right: 20px; + border: 1px solid #1171a3; + width: 400px; + height: 300px; + } + .chat h2 { + background: #1171a3; + color: #fff; + margin: 0; + padding: 8px; + } + .chat .close { + float: right; + display: block; + padding: 0 10px; + cursor: pointer; + } + .chat .chat-content { + padding: 10px; + } + .chat .chat-messages { + height: 190px; + overflow-y: scroll; + } + .chat .chat-input { + border-top: 1px solid #ccc; + padding-top: 10px; + } + .chat .chat-input input { + width: 370px; + padding: 3px; + } + ``` diff --git a/public/docs/ts/latest/guide/router-old-doc.jade b/public/docs/ts/latest/guide/router-old-doc.jade new file mode 100644 index 0000000000..67e9028757 --- /dev/null +++ b/public/docs/ts/latest/guide/router-old-doc.jade @@ -0,0 +1,724 @@ +.l-main-section + + h2#section-route-use Using the Component Router + p There are three steps to setting up routing with Angular's Component Router + ol + li Install the Component Router + li Map paths to components + li Link to routes + + +.l-main-section + + h2#section-install-router Import the Component Router + + p. + Create two files, index.html and app.ts, both at the root of the project: + + pre.prettyprint.lang-bash + code. + touch index.html app.ts + + p Your app directory should look something like: + pre.prettyprint.lang-bash + code. + app.ts + index.html + package.json + node_modules/ + └── ... + + p. + Because the component is an addition to the core, you must install Angular's Component Router into your app. + When using the angular2.dev.js bundle you have include the additional router.dev.js bundle. + + p. + Add Angular and Component Router into your app by adding the relevant <script> tags into your + index.html: + + //ANGULAR 1 + pre.prettyprint.lang-html.is-angular1.is-hidden + code. + <!doctype html> + <html lang="en"> + <head> + <meta charset="utf-8"> + <base href="/"> + <title>My app</title> + </head> + <body ng-app="myApp" ng-controller="AppController as app"> + <div ng-outlet></div> + <script src="/node_modules/angular/angular.js"></script> + <script src="/dist/router.es5.js"></script> + <script src="/app/app.js"></script> + </body> + </html> + + pre.prettyprint.lang-html.is-angular2 + code. + <!doctype html> + <html lang="en"> + <head> + <meta charset="utf-8"> + <base href="/"> + <title>My app</title> + </head> + <body ng-app="myApp" ng-controller="AppController as app"> + <script src="https://jspm.io/system@0.16.js"></script> + <script src="https://code.angularjs.org/2.0.0-alpha.21/angular2.dev.js"></script> + <script src="https://code.angularjs.org/2.0.0-alpha.21/router.dev.js"></script> + <script> + System.import('main'); + </script> + </body> + </html> + + + p.is-angular2. + Then you can add the router into your app by importing the Router module in your app.ts file: + + .code-box.is-angular2 + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + import {Component, View, bootstrap} from 'angular2/angular2'; + import {Router, RouterOutlet, RouterLink} from 'angular2/router'; + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + // self-executing bundle deploys angular APIs on the window object. + window.angular; + // the router APIs are part of the router sub-object. + window.angular.router; + + p.is-angular1.is-hidden. + This is a pretty typical angular app, except the ng-outlet directive. ng-outlet is like + ng-view; it's a placeholder for part of your app loaded dynamically based on the route configuration. + + p.is-angular2. + This is the same as you've seen in the rest of Angular 2, except the router-outlet directive. + router-outlet is a placeholder for part of your app loaded dynamically based on the route configuration. + + p So how do we configure the app? Let's open app.ts and find out. Add this to the file: + + //ANGULAR 1 + pre.prettyprint.lang-javascript.is-angular1.is-hidden + code. + angular.module('app', ['ngNewRouter']) + .controller('AppController', ['$router', AppController]); + + AppController.$routeConfig = [ + {path: '/', component: 'home' } + ]; + function AppController ($router) {} + + // ANGULAR 2 + .code-box + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + import {Component, View, bootstrap} from 'angular2/angular2'; + import {routerInjectables, RouterOutlet} from 'angular2/router'; + + import {HomeComp} from './components/home'; + + @Component({ + selector: 'my-app' + }) + @View({ + template: '<router-outlet></router-outlet>', + directives: [RouterOutlet] + }) + @RouteConfig([ + {path: '/', component: HomeComp } + ]) + class AppComp {} + + bootstrap(AppComp, routerInjectables); + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + var HomeComp = function() {}; + ... + + var AppComp = function() {}; + AppComp.annotations = [ + new angular.ComponentAnnotation({ + selector: 'my-app' + }), + new angular.ViewAnnotation({ + template: '<router-outlet></router-outlet>', + directives: [angular.router.RouterOutlet] + }), + new angular.router.RouteConfigAnnotation([ + {path: '/', component: HomeComp} + ]) + ]; + + angular.bootstrap(AppComp, routerInjectables); + + + p.is-angular1.is-hidden. + The ngComponentRouter module provides a new service, $router. In the configuration, we map paths + to components. What's a component? Let's talk about that for a bit. + + p.is-angular2. + The angular2/router module provides routerInjectables, which is an array of all of the services + you'll need to use the component router in your app. + +.l-main-section + h2#section-map-paths-to-components Map paths to components + + //- TODO - Alex - would it make more sense to have some paragraph styles conditionalized like this?? + p.angular1.is-hidden. + In Angular 1, a "routable component" is a template, plus a controller, plus a router. You can configure how to map + component names to controllers and templates in the $componentLoader service. + + p. + A component's template can have "outlets," which are holes in the DOM for loading parts of your app based on the + route configuration and it can ask the DI system for an instance of Router. A component's router tells the component what to put + inside the outlets based on URL. The configuration maps routes to components for each outlet. + + p Let's make a home component that our app can route to: + + pre.prettyprint.lang-bash + code. + mkdir -p components/home + touch components/home/home.html components/home/home.js + + p This creates our component directory and its corresponding files: a template and a JavaScript component. + + p Let's open home.html and add some content: + + pre.prettyprint.lang-html + code. + <h1>Hello {{home.name}}!</h1> + + p.is-angular1.is-hidden. + Components use the "controller as" syntax, so if we want to access property name of the controller, we + write the binding as home.name. + + p Let's make a controller: + //ANGULAR 1 + pre.prettyprint.lang-javascript.is-angular1.is-hidden + code. + angular.module('app.home', []) + .controller('HomeController', [function () { + this.name = 'Friend'; + }]); + + // ANGULAR 2 + .code-box + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + @Component({ + selector: 'home-cmp' + }) + @View({ + template: 'Hello {{name}}' + }) + export class HomeComponent { + name:string; + constructor() { + this.name = 'Friend'; + } + } + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + function HomeComponent() { + this.name = 'Friend'; + } + + AppComponent.annotations = [ + new angular.ComponentAnnotation({ + selector: 'home-cmp' + }), + new angular.ViewAnnotation({ + template: 'Hello {{name}}' + }) + ]; + + + p.is-angular1.is-hidden. + To wire this up, we need to add a <script> tag to our index.html: + pre.prettyprint.lang-html + code. + ... + <script src="./components/home/home.js"></script> + + //ANGULAR 1 + p.is-angular1.is-hidden. + And add the controller's module as a dependency to our main module in app.js: + pre.prettyprint.lang-javascript.is-angular1.is-hidden + code. + angular.module('app', ['ngNewRouter', 'app.home']) + .controller('AppController', ['$router', AppController]); + // ... + + p. + To wire this up, we need to import the component into the rest of our app. + // ANGULAR 2 + .code-box.is-angular2 + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + import {HomeComp} from './components/home'; + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + // Use your favorite module system / bundler for ES5. + + + p If you load up the app, you should see Hello Friend! + +.l-main-section + h2#section-link-to-routes Link to routes + + p Let's add another route and then link to it. This route will have a route parameter, id. + + p In app.js: + //ANGULAR 1 + pre.prettyprint.lang-javascript.is-hidden + code. + angular.module('app', ['ngNewRouter']) + .controller('AppController', ['$router', AppController]); + AppController.$routeConfig = [ + { path: '/', component: 'home' }, + { path: '/detail/:id', component: 'detail' } + ]; + function AppController ($router) {} + + // ANGULAR 2 + .code-box + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + ... + @RouteConfig([ + { path: '/', component: HomeComp }, + { path: '/detail/:id', component: DetailComp } + ]) + class AppComp {} + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + var AppComp = function() {}; + AppComp.annotations = [ + ... + new angular.router.RouteConfigAnnotation([ + { path: '/', component: HomeComp} + { path: '/detail/:id', component: DetailComp } + ]) + ]; + + angular.bootstrap(AppComp, routerInjectables); + + + p. + We can link to our detail component using the + router-link directive. + Add this to template: + + pre.prettyprint.lang-html.is-angular1.is-hidden. + code. + <body ng-app="myApp" ng-controller="AppController as app"> + <a ng-link="detail({id: 5})">link to detail</a> + ... + + pre.prettyprint.lang-html.is-angular2 + code. + <a ng-link="detail({id: 5})">link to detail</a> + + p This directive will generate an href and update the browser URL. + + p We should also implement our detail component. Let's make these new files: + + pre.prettyprint.lang-bash + code. + mkdir components/detail + touch components/detail/detail.html components/detail/detail.ts + + p In detail.ts, we implement a controller that uses the id route parameter: + + //ANGULAR 1 + pre.prettyprint.lang-javascript.is-hidden + code. + angular.module('app.detail', ['ngNewRouter']) + .controller('DetailController', ['$routeParams', DetailController]); + + function DetailController ($routeParams) { + this.id = $routeParams.id; + } + + // ANGULAR 2 + .code-box + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + @Component({ + selector: 'detail-cmp' + }) + @View({ + template: 'User ID: {{id}}' + }) + export class DetailComp { + id: string; + constructor(routeParams:RouteParams) { + this.id = routeParams.get('id'); + } + } + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + function DetailComp(routeParams) { + this.id = routeParams.get('id'); + } + + DetailComp.annotations = [ + new angular.ComponentAnnotation({ + selector: 'detail-cmp' + }), + new angular.ViewAnnotation({ + template: 'User ID: {{id}}' + }) + ]; + + DetailComp.parameters = [[RouteParams]]; + + + p.is-angular1.is-hidden. + And then we can display the id in our template by adding this to detail.html: + + pre.prettyprint.lang-html.is-angular1.is-hidden + code. + <p>detail {{detail.id}}</p> + + p.is-angular1.is-hidden. + Finally, we'd wire up the controller by adding a script tag and making our app module depend on + app.detail. + + +.l-main-section + + h2#section-configuring-the-router Configuring the Router + + p. + Unlike other routing systems, Component Router maps URLs to components. A router takes an array of pairings like + this: + + //ANGULAR 1 + pre.prettyprint.lang-javascript.is-angular1.is-hidden + code. + //ES5 + MyController.$routeConfig = [ + { path: '/user', component: 'user' } + ]; + + + //ANGULAR 2 + .code-box.is-angular2 + pre.prettyprint.linenums.lang-javascript(data-name="typescript") + code. + @Component() + @View() + @RouteConfig([ + { path: '/user', component: UserComponent } + ]) + class MyComp {} + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + function MyComp() {}; + MyComp.annotations = [ + new angular.ComponentAnnotation({ ... }), + new angular.ViewAnnotation({ ... }), + new angular.router.RouteConfigAnnotation([ + {path: '/', component: UserComponent} + ]) + + + .l-sub-section + h3#section-sibling-outlets Sibling Outlets + + + p You can configure multiple outlets on the same path like this: + + //ANGULAR 1 + .codebox.is-angular1.is-hidden + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + //ES5 + MyController.$routeConfig = [ + { path: '/user', + components: { + master: 'userList', + detail: 'user' + } } + ]; + + pre.prettyprint.linenums.lang-html(data-name="html") + code. + //HTML + <div ng-outlet="master"></div> + <div ng-outlet="detail"></div> + + + //ANGULAR 2 + .code-box.is-angular2 + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + //TypeScript + @Component({}) + @View({ + template: + `<div router-outlet="master"></div> + <div router-outlet="detail"></div>`, + directives: [RouterOutlet, RouterLink] + }) + @RouteConfig({ + path: '/user', components: { + master: UserListComp, + detail: UserComp + } + }) + class MyComponent {} + + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + function MyComponent() {}; + MyComponent.annotations = [ + new angular.ComponentAnnotation({ ... }), + new angular.ViewAnnotation({ + template: + '<div router-outlet="master"></div>' + + '<div router-outlet="detail"></div>', + directives: [RouterOutlet] + }), + new angular.router.RouteConfigAnnotation([{ + path: '/user', components: { + master: UserComponent, + detail: UserComp + } + }]) + ]; + + p You can link to any sibling just as you normally would: + + //ANGULAR 1 + pre.prettyprint.linenums.lang-html.is-angular1.is-hidden + code. + //HTML + <p>These both link to the same view:</p> + <a ng-link="userList">link to userList</a> + <a ng-link="user">link to user component</a> + + //ANGULAR 2 + pre.prettyprint.linenums.lang-html.is-angular2 + code. + //HTML + <p>These both link to the same view:</p> + <a router-link="userList">link to userList</a> + <a router-link="user">link to user component</a> + + + p Or, you can explicitly link to a outlet-component pair like this: + + //ANGULAR 1 + pre.prettyprint.linenums.lang-html.is-angular1.is-hidden + code. + //HTML + <p>These both link to the same view:</p> + <a ng-link="master:userList">link to userList</a> + <a ng-link="detail:user">link to user component</a> + + //ANGULAR 2 + pre.prettyprint.linenums.lang-html.is-angular2 + code. + //HTML + <p>These both link to the same view:</p> + <a router-link="master:userList">link to userList</a> + <a router-link="detail:user">link to user component</a> + + .l-sub-section + h3#section-redirecting-routes Redirecting routes + + p You can use `redirectTo` for migrating to a new URL scheme and setting up default routes. + + p. + For example, as specified below, when a user navigates to `/`, the URL changes to `/user` and the outlet + at that level loads the `user` component. + + //ANGULAR 1 + pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5") + code. + //ES5 + MyController.$routeConfig = [ + { path: '/', redirectTo: '/user' }, + { path: '/user', component: 'user' } + ]; + function MyController() {} + + //ANGULAR 2 + .code-box.is-angular2 + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + //TypeScript + @Component({}) + @View({ + directives: [RouterOutlet] + }) + @RouteConfig([ + { path: '/', redirectTo: '/user' }, + { path: '/user', component: UserComp } + ]) + class MyComp {} + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + function MyComponent() {}; + MyComponent.annotations = [ + new angular.ComponentAnnotation({ ... }), + new angular.ViewAnnotation({ + directives: [RouterOutlet] + }), + new angular.router.RouteConfigAnnotation([ + { path: '/user', component: UserComp } + { path: '/', redirectTo: '/user' }, + ]) + ]; + + + .l-sub-section + h3#section-aliases Aliases + + p. + When linking to a route, you normally use the name of the component. You can also specify an alias to use + instead. + + p Consider the following route configuration: + + //ANGULAR 1 + pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5") + code. + //ES5 + MyController.$routeConfig = [ + { path: '/', component: 'user' } + ]; + + //ANGULAR 2 + .code-box.is-angular2 + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + //TypeScript + @Component({ + selector: 'my-comp' + }) + @View({ + directives: [RouterOutlet] + }) + @RouteConfig([ + { path: '/', component: UserComp } + ]) + class MyComp {} + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + function MyComp() {}; + MyComp.annotations = [ + new angular.ComponentAnnotation({ ... }), + new angular.ViewAnnotation({ + directives: [RouterOutlet] + }), + new angular.router.RouteConfigAnnotation([ + { path: '/', component: UserComp } + ]) + ]; + + + p We can link to the route in our template with the name of the component: + //ANGULAR 1 + pre.prettyprint.linenums.lang-html.is-angular1.is-hidden + code. + //HTML + <a ng-link="user">link to user component</a> + + //ANGULAR 2 + pre.prettyprint.linenums.lang-html + code. + //HTML + <a router-link="user">link to user component</a> + + p Or, we can define an alias myUser like this: + + //ANGULAR 1 + pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5") + code. + //ES5 + MyController.$routeConfig = [ + { path: '/', component: 'user', as: 'myUser' } + ]; + + //ANGULAR 2 + .code-box.is-angular2 + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + //TypeScript + @Component() + @View() + @RouteConfig([ + { path: '/', component: UserComp, as: 'myUser' } + ]) + class MyComp {} + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + //ES5 + //TODO: Need Angular 2 ES5 Example here + + + p And refer instead to the alias for the component in our template, with the same end-result: + //ANGULAR 1 + pre.prettyprint.linenums.lang-html.is-angular1.is-hidden + code. + //HTML + <a ng-link="myUser">link to user component</a> + + //ANGULAR 2 + pre.prettyprint.linenums.lang-html + code. + //HTML + <a router-link="myUser">link to user component</a> + + + p. + This is especially useful when you have sibling components, but want to refer to an entire level of routing in + your controller. For example: + + //ANGULAR 1 + pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5") + code. + //ES5 + MyController.$routeConfig = [ + { path: '/', + components: { + master: 'userList', + detail: 'user' + }, + as: 'myUser' + } + ]; + + //ANGULAR 2 + .code-box.is-angular2 + pre.prettyprint.linenums.lang-typescript(data-name="typescript") + code. + //TypeScript + @RouteConfig([ + { path: '/', components: + { master: UserListComp, detail: UserComp }, + as: 'myUser' } + ]) + pre.prettyprint.linenums.lang-javascript(data-name="es5") + code. + new angular.router.RouteConfigAnnotation([ + { path: '/', components: + { master: UserListComp, detail: UserComp }, + as: 'myUser' } + ]) + + + //- TODO(btford): expand on this. + .l-sub-section + h3#dynamic-configuration Dynamic Configuration + + p.is-angular2. + You can configure dynamic routing by asking the DI system for a Router. + + p.is-angular1.is-hidden + You can configure dynamic routing by making a request for $router. diff --git a/public/docs/ts/latest/guide/router.jade b/public/docs/ts/latest/guide/router.jade index 2d23c624b4..c305310efc 100644 --- a/public/docs/ts/latest/guide/router.jade +++ b/public/docs/ts/latest/guide/router.jade @@ -1,9 +1,1043 @@ include ../../../../_includes/_util-fns :marked - # App Navigation with the Router - It's coming soon! + In most applications, users navigate from one [view](./glossary.html#view) to the next + as they perform application tasks. + + The browser is a familiar model of application navigation. + We enter a URL in the address bar and the browser navigates to a corresponding page. + We click links on the page and the browser navigates to a new page. + We click the browser's back and forward buttons and the browser navigates + backward and forward through the history of pages we've seen. + + The Angular "**Component Router**" (AKA "the router") borrows from this model. + It can interpret a browser URL as an instruction + to navigate to a client-generated view and pass optional parameters along to the supporting view component + to help it decide what specific content to present. + We can bind the router to links on a page and it will navigate to + the appropriate application view when the user clicks a link. + We can navigate imperatively when the user clicks a button, selects from a drop box, + 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/src/plnkr.html). +.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. + + The **`Router`** is a service that presents a particular Component view for a given URL. + When the browser's URL changes, the router looks for a corresponding **`RouteDefinition`** + from which it can determine the Component to display. + + A new router has no route definitions. We have to configure it. + The preferred way to configure the router is with a **`@RouteConfig`** [decorator](glossary.html#decorator) + applied to a host component. + + In this example, we configure the top-level `AppComponent` with three route definitions ++makeExample('router/ts/app/app.component.2.ts', 'route-config', 'app.component.ts (excerpt)')(format=".") +:marked + +.l-sub-section + :marked + There are several flavors of `RouteDefinition`. + The most common by far is the named **`Route`** which maps a URL path to a Component + + The `name` field is the name of the `Route`. The name **must** be spelled in **PascalCase**. + + The `:id` in the third route is a token for a route parameter. In a URL such as `/hero/42`, "42" + is the value of the `id` parameter. The corresponding `HeroDetailComponent` + 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 + Now we know how the router gets its configuration. + When the browser URL for this application becomes `/heroes`, + the router finds the `RouteDefintion` named *Heroes* and then knows to display the `HeroListComponent`. + + Display it where? It will display in a **`RouterOutlet`** that we've placed in the host view's HTML. +code-example(format="", language="html"). + <!-- Routed views go here --> + <router-outlet></router-outlet> +:marked + 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 + an anchor tag. + + In an anchor tag we bind a **`RouterLink`** Directive to a template expression that + returns an **array of route link parameters**. The router ultimately resolves that array + into a URL and a component view. + + We see such bindings in the following `AppComponent` template: ++makeExample('router/ts/app/app.component.1.ts', 'template')(format=".") +.l-sub-section + :marked + We're adding two anchor tags with `RouterLink` directives. + We bind each `RouterLink` to an array containing the string name of a route definition. + 'CrisisCenter' and 'Heroes' are the names of the `Routes` we configured above. + + We'll learn to write more complex link expressions ... and why they are arrays ... later in the chapter. +:marked + ### Let's summarize + + The `@RouterConfig` configuration tied the `AppComponent` to a router configured with routes. + The component has a `RouterOutlet` where it can display views produced by the router. + It has `RouterLinks` that users can click to navigate via the router. + + The `AppComponent` has become a ***Routing Component***, a component that can route. + + Here are the key *Component Router* terms and their meanings: +table + tr + th Router Part + th Meaning + tr + td Router + td. + Displays the application component for the active URL. + Manages navigation from one component to the next. + tr + td @RouteConfig + td. + Configures a router with RouteDefinitions, each mapping a URL path to a Component. + tr + td RouteDefinition + td. + Defines how the router should navigate to a Component based on a URL pattern. + tr + td Route + td. + The most common form of RouteDefinition consisting of a path, a route name, + and a component type. + tr + td RouterOutlet + td. + The directive (<router-outlet>) that marks where the router should display a view. + tr + td RouterLink + td. + The directive for binding a clickable HTML element to + a route. Clicking an anchor tag with a routerLink directive + that is bound to a Link Parameters Array triggers a navigation. + tr + td Link Parameters Array + td. + An array that the router inteprets into a routing instruction. + We can bind a RouterLink to that array or pass the array as an argument to + the Router.navigate method. +:marked + We'll learn many more details in this chapter which covers + + * configuring a router + * the link parameter arrays that propel router navigation + * navigating when the user clicks a data-bound link + * navigating under program control + * passing information in route parameters + * creating a child router with its own routes + * setting a default route (the *otherwise* option) + * asking the user's permission to leave before navigating to a new view using lifecycle events + + We will proceed in phases marked by milestones. + Our first milestone is the ability to navigate between 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 concepts and tools + we introduced in the [QuickStart](../quickstart.html) and + the [Tour of Heroes](../tutorial/) tutorial. + + While there is a progression, this chapter is not a tutorial. + We discuss code and design decisions pertinent to routing and application design. + We gloss over everything else. + + The full source is available in the [live example](/resources/live-examples/router/ts/src/plnkr.html). + +.callout.is-critical + header Route Link Syntax - Note to self + :marked + The tutorial approach won't be the best way to fully describe the link parameters array. + + Create an appendix on route link syntax ... how the array is interpreted ... and + link to it at the appropriate time. .l-main-section :marked - ## What's not to love? \ No newline at end of file + ## The Sample Application + We have an application in mind as we move from milestone to milestone. + Our client is the Hero Employment Agency. + Heroes need work and The Agency finds Crises for them to solve. + + The application has two main feature areas: + 1. A *Crisis Center* where we maintain the list of crises for assignment to heroes. + 1. A *Heroes* area where we maintain the list of heroes employed by The Agency. + + Run the [live example](/resources/live-examples/router/ts/src/plnkr.html). + It opens in the *Crisis Center*. We'll come back to that. + + Click the *Heroes* link. We're presented with a list of Heroes. +figure.image-display + img(src='/resources/images/devguide/router/hero-list.png' alt="Hero List" width="250") +:marked + We select one and the applications takes us to a hero editing screen. +figure.image-display + img(src='/resources/images/devguide/router/hero-detail.png' alt="Crisis Center Detail" width="250") +:marked + Our changes take affect immediately. We click the "Back" button and the + app returns us to the Heroes list. + + We could have clicked the browser's back button instead. + That would have returned us to the Heroes List as well. + Angular app navigation updates the browser history as normal web navigation does. + + Now click the *Crisis Center* link. We go to the *Crisis Center* and its list of ongoing crises. +figure.image-display + img(src='/resources/images/devguide/router/crisis-center-list.png' alt="Crisis Center List" ) +:marked + We select one and the applications takes us to a crisis editing screen. +figure.image-display + img(src='/resources/images/devguide/router/crisis-center-detail.png' alt="Crisis Center Detail") +:marked + This is a bit different then the "Heroes Detail". We have two buttons, "Save" and "Cancel". + If we make a change and click "Save", we return to the *Crisis Center* and see our changes + reflected in the list. If we make a change and click "Cancel", + we return to the *Crisis Center* but this time our changes were discarded. + + Now we click a crisis, make a change, and ***do not click either button***. + We click the browser back button instead. Up pops a modal dialog box. +figure.image-display + img(src='/resources/images/devguide/router/confirm-dialog.png' alt="Confirm Dialog" width="300") +:marked + We can say "OK" and lose our changes or click "Cancel" and continue editing. + + The router supports a `CanDeactivate` lifecycle" method that gives us a chance to clean-up + or ask the user's permission before navigating away from the current view. + + Let's see a quick demonstration of the workflow in action. + +figure.image-display + img(src='/resources/images/devguide/router/router-anim.gif' alt="App in action" ) +:marked + Here's a diagram of all application routing options: +figure.image-display + img(src='/resources/images/devguide/router/complete-nav.png' alt="Navigation diagram" ) +:marked + This app illustrates the router features we'll cover in this chapter + + * navigating to a component (*Heroes* link to "Heroes List") + * including a route parameter (passing the Hero `id` while routing to the "Hero Detail") + * child routes (the *Crisis Center* has its own routes) + * the `CanLeave` lifecycle method (ask before discarding changes) + + +.l-main-section +:marked + ## Milestone #1: Getting Started with the Router + + Let's begin with a simple version of the app that navigates between two empty views. +figure.image-display + img(src='/resources/images/devguide/router/router-1-anim.gif' alt="App in action" ) +:marked + ### Load the Component Router library + The Component Router is not part of the Angular 2 core. It is its own library. + The router is an optional service and you might prefer a different router someday. + + 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. ++makeExample('router/ts/index.html','router-lib')(format=".") +:marked + ### Set the *<base href>* + The Component Router uses the browser's + [history.pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries) + for navigation. Thanks to `pushState`, we can make our in-app URL paths look the way we want them to + look, e.g. `localhost:3000/crisis-center`. Our in-app URLs can be indistinguishable from server URLs. + + Modern HTML 5 browsers were the first to support `pushState` which is why many people refer to these URLs as + "HTML 5 style" URLs. + + We must **add a [<base href> element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag** + to the `index.html` to make this work. + The `href` value specifies the base URL to use for all *relative* URLs within a document including + links to css files, scripts, and images. + + Add the base element just after the `` tag. + If the `app` folder is the application root, as it is for our application, + set the `href` value *exactly* as shown here. + ++makeExample('router/ts/index.html','base-href')(format=".") +.l-sub-section + :marked + HTML 5 style navigation is the Component Router default. + Learn why "HTML 5" style is preferred, how to adjust its behavior, and how to switch to the + older hash (#) style if necessary in the [Browser URL Styles](#browser-url-styles) appendix below. + +:marked +.l-sub-section + :marked + #### Live example note + We have to be get tricky when we run the live example because the host service sets + the application base address dynamically. That's why we replace the `` with a + script that writes a `` tag on the fly to match. + code-example(format="") + <script>document.write('<base href="' + document.location + '" />');</script> + :marked + 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. + It's short and all of it is relevant to routing. ++makeExample('router/ts/app/boot.1.ts','all', 'boot.ts')(format=".") +:marked + We import our root `AppComponent` and Angular's `bootstrap` function as expected. + + We also import `ROUTER_PROVIDERS` from Dependency Injection. + The router is a service implemented by a collection of providers, most of which are identified in the + `ROUTER_PROVIDERS` array. + + As usual, we're booting Angular with `AppComponent` as our app's root component and + registering providers in an array in the second parameter of the `bootstrap` function. + Providing the router providers at the root makes the router available everywhere in our application. +.l-sub-section + :marked + Learn about providers, the `provide` function, and injected services in the + [Dependency Injection chapter](dependency-injection.html). +:marked + ### The *AppComponent* shell + The root `AppComponent` is the shell of our application. It has title at the top, a navigation bar with two links, + and a "router outlet" below where the router swaps views on and off the page. Here's what we mean: +figure.image-display + img(src='/resources/images/devguide/router/shell-and-outlet.png' alt="Shell" width="300" ) +:marked + + The corresponding component template looks like this: ++makeExample('router/ts/app/app.component.1.ts','template')(format=".") +:marked + ### *RouterOutlet* + `RouterOutlet` is a component from the router library. + The router displays views within the bounds of the `` tags. + +.l-sub-section + :marked + A template may hold exactly one ***unnamed*** ``. + + It could have multiple ***named*** outlets (e.g., ``). + We'll learn about them when we get to "auxiliary routes". +:marked + ### *RouterLink* binding + Above the outlet, within the anchor tags, we see [Property Bindings](template-syntax.html#property-binding) to + the `RouterLink` Directive that look like `[routerLink]="[...]"`. We imported `RouterLink` from the router library. + + The template expression to the right of the equals (=) returns an *array of link parameters*. + + The arrays in this example each have a single string parameter, the name of a `Route` that + we'll configure for this application with `@RouteConfig()`. + + ### *@RouteConfig()* + A router holds a list of route definitions. The list is empty for a new router. We must configure it. + + A router also needs a **Host Component**, a point of origin for its navigations. + + It's natural to combine the creation of a new router, its configuration, and its assignment to a host component + in a single step. That's the purpose of the `@RouteConfig` decorator which we put to good use here: ++makeExample('router/ts/app/app.component.1.ts','route-config')(format=".") +:marked + The `@RouteConfig` decorator creates a new router. + We applied the decorator to `AppComponent` which makes that the router's host component. + The argument to `@RouteConfig()` is an array of **Route Definitions**. + + We're supplying two definitions: ++makeExample('router/ts/app/app.component.1.ts','route-defs')(format=".") +:marked + Each definition translates to a [Route](https://angular.io/docs/ts/latest/api/router/Route-class.html) which has a + * `path` - the URL path segment for this route + * `name` - the name of the route + * `component` - the Component associated with this route. + + The router draws upon its registry of route definition when + 1. the browser URL changes + 2. we tell the router to go to a named route + + Translating these two definitions into English, we might say: + 1. *When the browser's location URL changes to **match the path** segment `/crisis-center`, create or retrieve an instance of + the `CrisisCenterComponent` and display its view.* + + 1. *When the application requests navigation to a route **named** `CrisisCenter`, compose a browser URL + with the path segment `/crisis-center`, update the browser's address location and history, create or retrieve an instance of + the `CrisisCenterComponent`, and display that component's view.* + + ### "Getting Started" wrap-up + + We've got a very basic, navigating app, one that can switch between two views + when the user clicks a link. + + We've learned how to + * load the router library + * add a nav bar to the shell template with anchor tags and `routerLink` directives + * added a `router-outlet` to the shell template where views will be displayed + * configure the router with `@RouterConfig` + * set the router to compose "HTML 5" browser URLs. + + The rest of the starter app is mundane, with little interest from a router perspective. + Here are the details for readers inclined to build the sample through this milestone. + + Our starter app's structure looks like this: +code-example(format=""). + router-sample + ├── node_modules/ + ├── app/ + | ├── app.component.ts + │ ├── boot.ts + │ ├── crisis-list.component.ts + │ └── hero-list.component.ts + ├── index.html + ├── styles.css + ├── tsconfig.json + └── package.json +:marked + Here are the application-specific files ++makeTabs( + `router/ts/app/app.component.1.ts, + router/ts/app/boot.1.ts, + router/ts/app/hero-list.component.ts, + router/ts/app/crisis-list.component.ts`, + ',all,,', + `app.component.ts, boot.ts,hero-list.component.ts,crisis-list.component.ts`) +:marked + + +.l-main-section +:marked + ## Milestone #2: The Heroes Feature + + We've seen how to navigate using the `RouterLink` directive. + + Now we'll learn some new tricks such as how to + * organize our app into "feature areas" + * navigate imperatively from one component to another + * pass information along in route parameters (`RouteParams`) + + To demonstrate all of this we'll build out the *Heroes* feature. + + ### The Heroes "feature area" + + A typical application has multiple "feature areas", each an island of functionality + dedicated to an area of interest with its own workflow(s). + + We could continue to add files to the `app/` folder. + That's unrealistic and ultimately not maintainable. + We think it's best if each feature area is in its own folder. + + Our first step is to **create a separate `app/heroes/` folder**. + Then we'll add Hero management feature files. + + We won't be creative about this. Our example is pretty much a + copy of the code and capabilities in the "[Tutorial: Tour of Heroes](../tutorial/index.html)". + + Here's how the user will experience this version of the app +figure.image-display + img(src='/resources/images/devguide/router/router-2-anim.gif' alt="App in action" ) +:marked + ### Add Heroes functionality + + We delete the placeholder `hero-list.component.ts` that's in + the `app/` folder. + + We create a new `hero-list.component.ts` in the `app/heroes/` + folder and copy over the contents of the final `heroes.component.ts` from the tutorial. + We also copy the `hero-detail.component.ts` and the `hero.service.ts` files + into the `heroes/` folder while we're at it. + + When were done organizing, we have three "Hero" files: + +code-example(format=""). + app/heroes/ + ├── hero-detail.component.ts + ├── hero-list.component.ts + └── hero.service.ts +:marked + Here as in the tutorial, we'll provide the `HeroService` during bootstrapping + so that is available anywhere in the app (see `boot.ts`) . + + Now it's time for some surgery to bring these files and the rest of the app + into alignment with our application router. + + ### New route definition with route parameter + + The new Heroes feature has two interacting components, the list and the detail. + The list view is self-sufficient; we navigate to it, it gets a list of heroes and displays them. + It doesn't need any outside information. + + The detail view is different. It displays a particular hero. It can't know which hero on its own. + That information must come from outside. + + In our example, when the user selects a hero from the list, we navigate to the detail view to show that hero. + We'll tell the detail view which hero to display by including the selected hero's id in the route URL. + + With that plan in mind, we return to the `app.component.ts` to make changes to the router's configuration + + First, we import the two components from their new locations in the `app/heroes/` folder: ++makeExample('router/ts/app/app.component.2.ts','hero-import')(format=".") +:marked + Then we update the `@RouteConfig` route definitions : ++makeExample('router/ts/app/app.component.2.ts','route-config')(format=".") +:marked + The `CrisisCenter` and `Heroes` definitions didn't change. + While we moved `hero-list.component.ts` to a new location in the `app/heroes/` folder, that only affects the `import` statement; + it doesn't affect its route definition. + + We added a new route definition for the `HeroDetailComponent` ... and this definition has a twist. ++makeExample('router/ts/app/app.component.2.ts','hero-detail-route')(format=".") +:marked + 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 her `id` (15) to appear in the + browser URL like this: +code-example(format="." language="bash"). + localhost:3000/hero/15 +:marked + If someone enters that URL into the browser address bar, the router should recognize the + pattern and go to the same "Magenta" detail view. + + ### Navigate to the detail imperatively + + *We don't navigate to the detail component by clicking a link*. + We won't be adding a new anchor tag to the shell navigation bar. + + Instead, we'll *detect* when the user selects a hero from the list and *command* the router + to present the hero detail view of the selected hero. + + We'll adjust the `HeroListComponent` to implement these tasks beginning with its template: ++makeExample('router/ts/app/heroes/hero-list.component.ts','template') +: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 `select` method. + + The `select` method will call the router service which we acquire by dependency injection + (along with the `HeroService` that gives us heroes to show): ++makeExample('router/ts/app/heroes/hero-list.component.ts','ctor')(format=".") +:marked + Here's the `select` method: ++makeExample('router/ts/app/heroes/hero-list.component.ts','select')(format=".") +:marked + It calls the router's **`navigate`** method with a **Link Parameters Array**. + This one is similar to the *link parameters array* we met [earlier](#shell-template) in an anchor tag, + binding to the `RouterLink` directive, only this time we're seeing it in code rather than in HTML. + ### Setting the route parameter + + 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=".") +:marked + The router composes the appropriate two-part destination URL: +code-example(format="." language="bash"). + localhost:3000/hero/15 +:marked + ### Getting the route parameter + + + How does the target `HeroDetailComponent` get that `id`? + Certainly not by analyzing the URL! That's the router's job. + + The router extracts the route parameter (`id:15`) from the URL and supplies it to + the `HeroDetailComponent` via the **RouteParams** service. + + 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=".") +: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=".") + +.l-sub-section + :marked + Angular calls the `ngOnInit` method shortly after creating an instance of the `HeroDetailComponent`. + + We put the data access logic in the `ngOnInit` method rather than inside the constructor + to improve the component's testability. + We explore this point in greater detail in the [OnInit appendix](#onInit) below. +:marked + ### Navigating back to the list component + The `HeroDetailComponent` has a "Back" button wired to its `gotoHeroes` method that navigates imperatively + back to the `HeroListComponent`. + + The router `navigate` method takes the same, one-item *link parameters array*, holding + the **name of the `HeroListComponent` route**, that we used in the `[routerLink]` directive binding. ++makeExample('router/ts/app/heroes/hero-detail.component.ts','gotoHeroes')(format=".") +:marked + ### Heroes App Wrap-up + + We've reached the second milestone in our router education. + + We've learned how to + * organize our app into "feature areas" + * navigate imperatively from one component to another + * pass information along in route parameters (`RouteParams`) + + After these changes, the folder structure looks like this: +code-example(format=""). + router-sample + ├── node_modules/ + ├── app/ + │ ├── heroes/ + │ │ ├── hero-detail.component.ts + │ │ ├── hero-list.component.ts + │ │ └── hero.service.ts + │ ├── app.component.ts + | ├── boot.ts + │ └── crisis-list.component.ts + ├── index.html + ├── styles.css + ├── tsconfig.json + └── package.json +:marked + + ### The Heroes App code + 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.service.ts`, + `,v2,,,`, + `app.component.ts, + boot.ts, + hero-list.component.ts, + hero-detail.component.ts, + hero.service.ts`) +:marked + + +.l-main-section +:marked + ## Milestone #3: The Crisis Center + The *Crisis Center* is a fake view at the moment. Time to make it useful. + + The new *Crisis Center* begins as a virtual copy of the *Heroes* feature. + We create a new `app/crisis-center` folder, copy the Hero files, + and change every mention of "hero" to "crisis". + + A `Crisis` has an `id` and `name`, just like a `Hero` + The new `CrisisListComponent` displays lists of crises. + When the user selects a crisis, the app navigates to the `CrisisDetailComponent` + for display and editing of the crisis name. + + Voilà, instant feature module! + + Of course this is only a sample application. + There's no point to this exercise unless we can learn something new. + + We do have new points to make: + + * The application should navigate to the *Crisis Center* by default. + + * The user should be able to cancel unwanted changes. + + * The router should prevent navigation away from the detail view while there are pending changes. + + * When we return to the list from the detail, the previously edited crisis should be pre-selected in the list. + That will require passing information *back* to the list from the detail. + + There are also a few lingering annoyances in the *Heroes* implementation that we can cure in the *Crisis Center*. + + * We currently register every route of every view at the highest level of the application. + If we expand the *Crisis Center* with a 100 new views, we'll make 100 changes to the + `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 + 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`. + We need to [*separate our concerns*](https://blog.8thlight.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html). + + We'll fix all of these problems and add the new routing features to *Crisis Center*. + + The most important fix from a router perspective will be the introduction of a **child *Routing Component*** + with its **child router** + + We'll leave *Heroes* in its less-than-perfect state to + serve as a contrast with what we hope is a superior *Crisis Center*. + + ### A free-standing Crisis Center Feature Module + The *Crisis Center* is one of two application workflows. + Users navigate between them depending on whether they are managing crises or heroes. + + The `CrisisCenter` and `Heroes` components are children of the root `AppComponent`. + + Unfortunately, they and their related files are physically commingled in the same folder with the `AppComponent`. + We'd prefer to separate them in their own "feature areas" so they can operate and evolve independently. + Someday we might re-use one or the other of them in a different application. + Someday we might load one of them dynamically only when the user chose to enter its workflow. + + Some might call it [yagni](http://martinfowler.com/bliki/Yagni.html) to even think about such things. + But we're right to be nervous about the way *Heroes* and *Crisis Center* artifacts are + bubbling up to the root `AppComponent` and blending with each other. + That's a [code smell](http://martinfowler.com/bliki/CodeSmell.html). + + Isolating feature area modules from each other looks good to us. +.l-sub-section + :marked + It's looking good as a general pattern for Angular applications. + figure.image-display + img(src='/resources/images/devguide/router/component-tree.png' alt="Component Tree" ) + :marked + * each feature area in its own module folder + * each area with its own root component + * each area root component with its own router-outlet and child routes + * area routes rarely (if ever) cross + +:marked + We'll make the *Crisis Center* stand on its own and leave the *Heroes* as it is + so we can compare the effort, results, and consequences. + Then each of us can decide which path to prefer (as if we didn't already know). + + ### 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)') +:marked + The `CrisisCenterComponent` parallels the `AppComponent`. + + The `CrisisCenterComponent` is the root of the *Crisis Center* area + just as `AppComponent` is the root of the entire application. + + This `CrisisCenterComponent` is a shell for crisis management + just as the `AppComponent` is a shell to manage the high-level workflow. + + `AppComponent` has a `@RouteConfig` decorator that defines the top-level routes. + `CrisisCenterComponent` has a `@RouteConfig` decorator that defines *Crisis Center* routes. + The two sets of routes *do not overlap*. + + `CrisisCenterComponent` template is dead simple — simpler even than the `AppComponent` template. + It has no content, no links, just a `` for the *Crisis Center* views. + + It has no selector either. It doesn't need one. We don't *embed* this component in a parent template. We navigate to it + from the outside, via a parent router (more on that soon). + + ### 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') +: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`. + By restricting its scope, we feel confident that we can evolve it independently without fear of breaking + unrelated application modules — modules that *shouldn't have access to it anyway*. + + ### 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 + that we did earlier. ++makeExample('router/ts/app/crisis-center/crisis-center.component.ts', 'route-config', 'crisis-center/crisis-center.component.ts (routes only)' ) +: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. + + We cannot tell just 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 entirely deliberate. The *Crisis Center* shouldn't know that it is the child of anything. + It might be the root of its own application. It might be repurposed in a different application. + The *Crisis Center* can be indifferent. + + *We know* that it is child component in our application because we re-configured the + routes of the top-level `AppComponent` to make it so. +:marked + ### Parent Route Configuration + 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 — *signficantly* — 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 (`/...`)**. + + That means this is a ***non-terminal route*** , a route that requires completion by a **child router** + attached to the designated component which must be a *Routing Component*. + + All is well. + The route's component is the `CrisisCenterComponent` which we know to be a *Routing Component* with its own routes. + + + + ### Default route (AKA *otherwise*) + The other big change is the addition of the `useAsDefault` property. + Its value is `true` which makes *this* route the *default* route. + + When the `AppComponent` router sees a URL that doesn't match any of these three route paths, + it redirects to this 'CrisisCenter' route. +.l-sub-section + :marked + Setting `useAsDefault = true` is the equivalent of an ***otherwise*** in other routing systems. +:marked + That's how we get to the *Crisis Center* when we first launch the application. + At launch the URL is a host and port with no path. That doesn't match any the configured route paths. + So the router redirects to the *Crisis Center*. + + Try any bogus address in the [live example](/resources/live-examples/router/ts/src/plnkr.html) and + we'll land back in the *Crisis Center*. + [NOT TRUE. SHOULD BE TRUE?] +:marked + +:marked + ### PICK UP HERE + TODO: + * The RouteLink to the Crisis Center ... and how it doesn't specify the child route ... but could + * The route name prefixes in the route links (push that below) + * query parameters (no time to build an example I'm afraid) + * The router lifecycle hooks below + * decide whether to keep the # option discussion + * How the router interprets the link parameters array + + ### Cancel and Save + [INTRO] +code-example(format="."). + <button (click)="save()">Save</button> + <button (click)="cancel()">Cancel</button> +:marked + [EXPLAIN] ++makeExample('router/ts/app/crisis-center/crisis-detail.component.ts', 'cancel-save', 'crisis-detail.component.ts (excerpt)') +:marked + [EXPLANATION] + + ### Confirm before leaving with unsaved changes + [INTRO] ++makeExample('router/ts/app/crisis-center/crisis-detail.component.ts', 'canDeactivate', 'crisis-detail.component.ts (excerpt)') +:marked + [EXPLANATION] + ### Re-select crisis in the list via route param + [INTRO] ++makeExample('router/ts/app/crisis-center/crisis-detail.component.ts', 'gotoCrises', 'crisis-detail.component.ts (excerpt)') +:marked + [EXPLANATION] ++makeExample('router/ts/app/crisis-center/crisis-list.component.ts', 'isSelected', 'crisis-list.component.ts (excerpt)') +:marked + [EXPLANATION] +code-example(format="."). + [class.selected]="isSelected(crisis)" +:marked + + + +.l-main-section +:marked + ## The Finished App + As we end our chapter together, we take a parting look at + the entire application. + + ### Folder structure + Our final project folder structure looks like this: +code-example(format=""). + router-sample + ├── node_modules/ + ├── src/ + ├── app/ + │ ├── crisis-center/... + │ ├── heroes/... + │ ├── app.component.ts + │ ├── boot.ts + │ └── dialog.service.ts + ├── index.html + ├── styles.css + ├── tsconfig.json + └── package.json +:marked + The top level application files are ++makeTabs( + `router/ts/app/app.component.ts, + router/ts/app/boot.ts, + router/ts/app/dialog.service.ts, + router/ts/index.html, + router/ts/styles.css + `, + null, + `app.component.ts, + boot.ts, + dialog.service.ts, + index.html, + styles.css + `) +:marked + + ### Crisis Center + The *Crisis Center* feature area within the `crisis-center` folder follows: +code-example(format=""). + app/ + crisis-center/ + ├── crisis-center.component.ts + ├── crisis-detail.component.ts + ├── crisis-list.component.ts + ├── crisis.service.ts + └── routes.ts +:marked ++makeTabs( + `router/ts/app/crisis-center/crisis-center.component.ts, + router/ts/app/crisis-center/crisis-list.component.ts, + router/ts/app/crisis-center/crisis-detail.component.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, + `) +:marked + ### Heroes + The *Heroes* feature area within the `heroes` folder is next: +code-example(format=""). + app/ + heroes/ + ├── hero-detail.component.ts + ├── hero-list.component.ts + └── hero.service.ts +:marked ++makeTabs( + `router/ts/app/heroes/hero-list.component.ts, + router/ts/app/heroes/hero-detail.component.ts, + router/ts/app/heroes/hero.service.ts + `, + null, + `hero-list.component.ts, + hero-detail.component.ts, + hero.service.ts + `) +:marked + + +.l-main-section +:marked + ## Appendix: Why use an *ngOnInit* method + + We implemented an `ngOnInit` method in many of our Component classes. + We did so, for example, in the [HeroDetailComponent](#hero-detail-ctor). + We might have put the `ngOnInit` logic inside the constructor instead. We didn't for a reason. The reason is *testability*. + + A constructor that has major side-effects can be difficult to test because it starts doing things as soon as + we create a test instance. In this case, it might have made a request to a remote server, something it shouldn't + do under test. It may even be impossible to reach the server in the test environment. + + The better practice is to limit what the constructor can do. Mostly it should stash parameters in + local variables and perform simple instance configuration. + + Yet we want an instance of this class to get the hero data from the `HeroService` soon after it is created. + How do we ensure that happens if not in the constructor? + + Angular detects when a component has certain lifecycle methods like + [ngOnInit](https://angular.io/docs/ts/latest/api/core/OnInit-interface.html) and + [ngOnDestroy](https://angular.io/docs/ts/latest/api/core/OnDestroy-interface.html) and calls them + at the appropriate moment. + + Angular will call `ngOnInit` when we navigate to the `HeroDetailComponent`, we'll get the `id` from the `RouteParams` + and ask the server for the hero with that `id`. + + We too can call that `ngOnInit` method in our tests if we wish ... after taking control of the injected + `HeroService` and (perhaps) mocking it. + + +.l-main-section +:marked + ## Appendix: Browser URL styles + + 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 + 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. + The router can compose a "natural" URL that is indistinguishable from + one that would otherwise require a page load. + + Here's the *Crisis Center* URL in this "HTML 5 pushState" style: +code-example(format=".", language="bash"). + localhost:3002/crisis-center/ +: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 + 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. +.l-sub-section + :marked + Learn about "providers" and the bootstrap process in the + [Dependency Injection chapter](dependency-injection#bootstrap) +:marked + ### Which Strategy is Best? + We must choose a strategy and we need to make the right call early in the project. + It won't be easy to change later once the application is in production + and there are lots of application URL references in the wild. + + Almost all Angular 2 projects should use the default HTML 5 style. + It produces URLs that are easier for users to understand. + And it preserves the option to do **server-side rendering** later. + + Rendering critical pages on the server is a technique that can greatly improve + perceived responsiveness when the app first loads. + An app that would otherwise take ten or more seconds to start + could be rendered on the server and delivered to the user's device + in less than a second. + + Thist option is only available if application URLs look like normal web URLs + without hashes (#) in the middle. + + Stick with the default unless you have a compelling reason to + resort to hash routes. + + ### HTML 5 URLs and the *<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. + + We *must* add a + [<base href> element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag + in the `` of the `index.html`. ++makeExample('router/ts/index.html','base-href')(format=".") +:marked + Without that tag, the browser may not be be able to load resources + (images, css, scripts) when "deep linking" into the app. + Bad things could happen when someone pastes an application link into the + browser's address bar or clicks such a link in an email link. + + Some developers may not be able to add the `` element, perhaps because they don't have + access to `` or the `index.html`. + + Those developers may still use HTML 5 URLs by taking two remedial steps: + + 1. Provide the router with an appropriate `APP_BASE_HREF` value. + 1. Use **absolute URLs** for all web resources: css, images, scripts, and template html files. + +.l-sub-section + :marked + Learn about the [APP_BASE_HREF](https://angular.io/docs/ts/latest/api/router/APP_BASE_HREF-const.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') diff --git a/public/docs/ts/latest/guide/template-syntax.jade b/public/docs/ts/latest/guide/template-syntax.jade index b1cc811543..a7043de079 100644 --- a/public/docs/ts/latest/guide/template-syntax.jade +++ b/public/docs/ts/latest/guide/template-syntax.jade @@ -784,15 +784,15 @@ figure.image-display Here’s an example: +makeExample('template-syntax/ts/app/app.component.html', 'NgSwitch')(format=".") :marked - We bind the parent `NgSwitch` element to an expression returning a “switch value”. The value is a string in this example but it can be a value of any type. + We bind the parent `NgSwitch` directive to an expression returning a “switch value”. The value is a string in this example but it can be a value of any type. - The parent `NgSwitch` element controls a set of child`