From 5bac678c60e6cbfb20d4784235f4e7d67fce3c37 Mon Sep 17 00:00:00 2001 From: Benoit Lateltin Date: Mon, 18 Jul 2016 18:35:04 +0200 Subject: [PATCH 01/25] docs(toh-pt5): fix typo in tutorial closes #1913 --- public/docs/ts/latest/tutorial/toh-pt5.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/docs/ts/latest/tutorial/toh-pt5.jade b/public/docs/ts/latest/tutorial/toh-pt5.jade index 9f17e0811a..243a92d7e1 100644 --- a/public/docs/ts/latest/tutorial/toh-pt5.jade +++ b/public/docs/ts/latest/tutorial/toh-pt5.jade @@ -624,7 +624,7 @@ figure.image-display **The *routerLinkActive* directive** The Angular Router provides a `routerLinkActive` directive we can use to - to add a class to the HTML navigation element whose route matches the active route. + add a class to the HTML navigation element whose route matches the active route. All we have to do is define the style for it. Sweet! +makeExample('toh-5/ts/app/app.component.3.ts', 'router-link-active', 'app/app.component.ts (active router links)')(format=".") :marked From 83ba850305c12384782f3daf05c23994895d706b Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Mon, 18 Jul 2016 11:35:57 -0700 Subject: [PATCH 02/25] chore(gulp): except for check-deploy, gulp targets are for (ts|js) only closes #1915 --- gulpfile.js | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 918b096bc5..ab6df29ec5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -106,11 +106,28 @@ var _exampleProtractorBoilerplateFiles = [ var _exampleConfigFilename = 'example-config.json'; -var lang, langs; +// Gulp flags: +// +// --lang=[all | ts | js | dart | (ts|js) | (ts|js|dart) | ...] +// +// This affects which language API docs and E2E tests are run. Can be 'all', +// or a regex pattern to match any one of 'ts', 'js', or 'dart'. +// Default: '(ts|js)' except for check-deploy for which it is 'all'. +// +var lang, langs, buildDartApiDocs = false; function configLangs(langOption) { - lang = (langOption || 'all').toLowerCase(); - if (lang === 'all') { lang = '(ts|js|dart)'; } + // TODO(chalin): temporary dependence on process.env.TRAVIS until #1910 lands. + const buildAllDocs = argv['_'] && argv['_'].indexOf('check-deploy' ) >= 0; + const langDefault = (!buildAllDocs || process.env.TRAVIS) ? '(ts|js)' : 'all'; + lang = (langOption || langDefault).toLowerCase(); + if (lang === 'all') lang = '(ts|js|dart)'; langs = lang.match(/\w+/g); // the languages in `lang` as an array + gutil.log('Build docs for: ' + lang); + if (langs.indexOf('dart') >= 0) { + buildDartApiDocs = true; + // For Dart, be proactive about checking for the repo + checkAngularProjectPath(ngPathFor('dart')); + } } configLangs(argv.lang); @@ -149,7 +166,6 @@ gulp.task('run-e2e-tests', runE2e); * all means (ts|js|dart) */ function runE2e() { - if (!argv.lang) configLangs('ts|js'); // Exclude dart by default var promise; if (argv.fast) { // fast; skip all setup @@ -545,9 +561,8 @@ gulp.task('build-docs', ['build-devguide-docs', 'build-api-docs', 'build-plunker // Stop zipping examples Feb 28, 2016 //gulp.task('build-docs', ['build-devguide-docs', 'build-api-docs', 'build-plunkers', '_zip-examples']); -gulp.task('build-api-docs', ['build-js-api-docs', 'build-ts-api-docs', 'build-dart-cheatsheet'] - // On TRAVIS? Skip building the Dart API docs for now. - .concat(process.env.TRAVIS ? [] : ['build-dart-api-docs'])); +gulp.task('build-api-docs', ['build-js-api-docs', 'build-ts-api-docs'] + .concat(buildDartApiDocs ? ['build-dart-api-docs', 'build-dart-cheatsheet'] : [])); gulp.task('build-devguide-docs', ['_shred-devguide-examples', '_shred-devguide-shared-jade'], function() { return buildShredMaps(true); @@ -1190,7 +1205,7 @@ function buildApiDocsForDart() { dabInfo.excludeLibRegExp = new RegExp(/^(?!angular2)|\.testing|_|codegen|^angular2$/); try { - checkAngularProjectPath('dart'); + checkAngularProjectPath(ngPathFor('dart')); var destPath = dabInfo.ngIoDartApiDocPath; var sourceDirs = fs.readdirSync(dabInfo.ngDartDocPath) .filter((name) => !name.match(/^index/)) @@ -1391,9 +1406,8 @@ function ngPathFor(lang) { return ANGULAR_PROJECT_PATH + (lang === 'dart' ? '-dart' : ''); } -function checkAngularProjectPath(lang) { - var ngPath = path.resolve(ngPathFor(lang || 'ts')); - if (!fs.existsSync(ngPath)) { - throw new Error('API related tasks require the angular2 repo to be at ' + ngPath); - } +function checkAngularProjectPath(_ngPath) { + var ngPath = path.resolve(_ngPath || ngPathFor('ts')); + if (fs.existsSync(ngPath)) return; + throw new Error('API related tasks require the angular2 repo to be at ' + ngPath); } From f056a2d5d272c450ad04b5953aa2aedef15e8893 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sat, 16 Jul 2016 17:34:26 -0500 Subject: [PATCH 03/25] docs(router): Added content updates to developer guide closes #1905 Added section for RouterLinkActive Added section for global query params and fragments Added section for RouterState Added wildcard route to example configuration Updated code samples Renamed .guard files to .service Renamed interfaces.ts to can-deactivate-guard.service.ts Removed unused files --- .../router/ts/app/app.component.1.ts | 4 +- .../router/ts/app/app.component.2.ts | 4 +- .../router/ts/app/app.component.3.ts | 7 +- .../router/ts/app/app.component.4.ts | 7 +- .../_examples/router/ts/app/app.component.ts | 9 +- .../_examples/router/ts/app/app.routes.1.ts | 4 +- .../_examples/router/ts/app/app.routes.ts | 2 +- ...uth.guard.1.ts => auth-guard.service.1.ts} | 2 + .../router/ts/app/auth-guard.service.2.ts | 22 ++ .../router/ts/app/auth-guard.service.ts | 32 +++ .../_examples/router/ts/app/auth.guard.ts | 15 - .../_examples/router/ts/app/auth.service.ts | 3 + ...ces.ts => can-deactivate-guard.service.ts} | 2 + .../crisis-center/crisis-admin.component.1.ts | 11 + .../crisis-center/crisis-admin.component.ts | 36 ++- .../crisis-center/crisis-center.routes.3.ts | 2 +- .../crisis-center/crisis-center.routes.4.ts | 4 +- .../app/crisis-center/crisis-center.routes.ts | 4 +- .../crisis-detail.component.1.ts | 4 +- .../crisis-center/crisis-detail.component.ts | 4 +- .../crisis-center/crisis-list.component.1.ts | 2 +- .../crisis-center/crisis-list.component.ts | 2 +- .../ts/app/heroes/hero-detail.component.ts | 2 +- .../ts/app/heroes/hero-list.component.2.ts | 64 ----- .../ts/app/heroes/hero-list.component.ts | 8 +- .../router/ts/app/heroes/heroes.routes.ts | 6 +- .../router/ts/app/login.component.ts | 9 +- .../_examples/router/ts/app/login.routes.ts | 2 +- .../router/ts/app/not-found.component.ts | 9 + public/docs/ts/latest/guide/router.jade | 262 ++++++++++++------ 30 files changed, 332 insertions(+), 212 deletions(-) rename public/docs/_examples/router/ts/app/{auth.guard.1.ts => auth-guard.service.1.ts} (76%) create mode 100644 public/docs/_examples/router/ts/app/auth-guard.service.2.ts create mode 100755 public/docs/_examples/router/ts/app/auth-guard.service.ts delete mode 100755 public/docs/_examples/router/ts/app/auth.guard.ts rename public/docs/_examples/router/ts/app/{interfaces.ts => can-deactivate-guard.service.ts} (88%) create mode 100644 public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.1.ts delete mode 100644 public/docs/_examples/router/ts/app/heroes/hero-list.component.2.ts create mode 100644 public/docs/_examples/router/ts/app/not-found.component.ts diff --git a/public/docs/_examples/router/ts/app/app.component.1.ts b/public/docs/_examples/router/ts/app/app.component.1.ts index f8a23d3f47..e124b6771a 100644 --- a/public/docs/_examples/router/ts/app/app.component.1.ts +++ b/public/docs/_examples/router/ts/app/app.component.1.ts @@ -12,8 +12,8 @@ import { ROUTER_DIRECTIVES } from '@angular/router'; template: `

Component Router

`, diff --git a/public/docs/_examples/router/ts/app/app.component.2.ts b/public/docs/_examples/router/ts/app/app.component.2.ts index f1580cf6f8..fcbde30967 100644 --- a/public/docs/_examples/router/ts/app/app.component.2.ts +++ b/public/docs/_examples/router/ts/app/app.component.2.ts @@ -25,8 +25,8 @@ import { HeroService } from './heroes/hero.service'; template: `

Component Router

`, diff --git a/public/docs/_examples/router/ts/app/app.component.3.ts b/public/docs/_examples/router/ts/app/app.component.3.ts index e20c980aef..66d485d339 100644 --- a/public/docs/_examples/router/ts/app/app.component.3.ts +++ b/public/docs/_examples/router/ts/app/app.component.3.ts @@ -31,12 +31,17 @@ import { HeroService } from './heroes/hero.service'; Dragon Crisis // #enddocregion Dragon-anchor */ + /* Crisis Center link with optional query params + // #docregion cc-query-params + Crisis Center + // #enddocregion cc-query-params + */ // #docregion template template: `

Component Router

diff --git a/public/docs/_examples/router/ts/app/app.component.4.ts b/public/docs/_examples/router/ts/app/app.component.4.ts index f39d71f5e9..80cf4b0d17 100644 --- a/public/docs/_examples/router/ts/app/app.component.4.ts +++ b/public/docs/_examples/router/ts/app/app.component.4.ts @@ -11,9 +11,10 @@ import { HeroService } from './heroes/hero.service'; template: `

Component Router

`, diff --git a/public/docs/_examples/router/ts/app/app.component.ts b/public/docs/_examples/router/ts/app/app.component.ts index 9196c34084..84dd5ea5aa 100644 --- a/public/docs/_examples/router/ts/app/app.component.ts +++ b/public/docs/_examples/router/ts/app/app.component.ts @@ -12,10 +12,11 @@ import { HeroService } from './heroes/hero.service'; template: `

Component Router

`, diff --git a/public/docs/_examples/router/ts/app/app.routes.1.ts b/public/docs/_examples/router/ts/app/app.routes.1.ts index 14a0ebe946..7919f34aba 100644 --- a/public/docs/_examples/router/ts/app/app.routes.1.ts +++ b/public/docs/_examples/router/ts/app/app.routes.1.ts @@ -10,6 +10,7 @@ import { provideRouter, RouterConfig } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { HeroDetailComponent } from './heroes/hero-detail.component'; +import { PageNotFoundComponent } from './not-found.component'; // #enddocregion base-routes // #docregion @@ -20,8 +21,9 @@ const routes: RouterConfig = [ { path: 'heroes', component: HeroListComponent }, // #enddocregion route-defs // #docregion hero-detail-route - { path: 'hero/:id', component: HeroDetailComponent } + { path: 'hero/:id', component: HeroDetailComponent }, // #enddocregion hero-detail-route + { path: '**', component: PageNotFoundComponent } ]; export const appRouterProviders = [ diff --git a/public/docs/_examples/router/ts/app/app.routes.ts b/public/docs/_examples/router/ts/app/app.routes.ts index 2f003bab55..eb238680da 100644 --- a/public/docs/_examples/router/ts/app/app.routes.ts +++ b/public/docs/_examples/router/ts/app/app.routes.ts @@ -7,7 +7,7 @@ import { heroesRoutes } from './heroes/heroes.routes'; import { loginRoutes, authProviders } from './login.routes'; -import { CanDeactivateGuard } from './interfaces'; +import { CanDeactivateGuard } from './can-deactivate-guard.service'; export const routes: RouterConfig = [ ...heroesRoutes, diff --git a/public/docs/_examples/router/ts/app/auth.guard.1.ts b/public/docs/_examples/router/ts/app/auth-guard.service.1.ts similarity index 76% rename from public/docs/_examples/router/ts/app/auth.guard.1.ts rename to public/docs/_examples/router/ts/app/auth-guard.service.1.ts index 1d93866a4b..c824bcb208 100644 --- a/public/docs/_examples/router/ts/app/auth.guard.1.ts +++ b/public/docs/_examples/router/ts/app/auth-guard.service.1.ts @@ -1,6 +1,8 @@ // #docregion +import { Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; +@Injectable() export class AuthGuard implements CanActivate { canActivate() { console.log('AuthGuard#canActivate called'); diff --git a/public/docs/_examples/router/ts/app/auth-guard.service.2.ts b/public/docs/_examples/router/ts/app/auth-guard.service.2.ts new file mode 100644 index 0000000000..9d4f3afb3b --- /dev/null +++ b/public/docs/_examples/router/ts/app/auth-guard.service.2.ts @@ -0,0 +1,22 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { CanActivate, Router, + ActivatedRouteSnapshot, + RouterStateSnapshot } from '@angular/router'; +import { AuthService } from './auth.service'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService, private router: Router) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + if (this.authService.isLoggedIn) { return true; } + + // Store the attempted URL for redirecting + this.authService.redirectUrl = state.url; + + // Navigate to the login page + this.router.navigate(['/login']); + return false; + } +} diff --git a/public/docs/_examples/router/ts/app/auth-guard.service.ts b/public/docs/_examples/router/ts/app/auth-guard.service.ts new file mode 100755 index 0000000000..a70fadb5cf --- /dev/null +++ b/public/docs/_examples/router/ts/app/auth-guard.service.ts @@ -0,0 +1,32 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { CanActivate, Router, + ActivatedRouteSnapshot, + RouterStateSnapshot } from '@angular/router'; +import { AuthService } from './auth.service'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService, private router: Router) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + if (this.authService.isLoggedIn) { return true; } + + // Store the attempted URL for redirecting + this.authService.redirectUrl = state.url; + + // Create a dummy session id + let sessionId = 123456789; + + // Set our navigation extras object + // that contains our global query params and fragment + let navigationExtras = { + queryParams: { 'session_id': sessionId }, + fragment: 'anchor' + }; + + // Navigate to the login page with extras + this.router.navigate(['/login'], navigationExtras); + return false; + } +} diff --git a/public/docs/_examples/router/ts/app/auth.guard.ts b/public/docs/_examples/router/ts/app/auth.guard.ts deleted file mode 100755 index 52317985c1..0000000000 --- a/public/docs/_examples/router/ts/app/auth.guard.ts +++ /dev/null @@ -1,15 +0,0 @@ -// #docregion -import { Injectable } from '@angular/core'; -import { CanActivate, Router } from '@angular/router'; -import { AuthService } from './auth.service'; - -@Injectable() -export class AuthGuard implements CanActivate { - constructor(private authService: AuthService, private router: Router) {} - - canActivate() { - if (this.authService.isLoggedIn) { return true; } - this.router.navigate(['/login']); - return false; - } -} diff --git a/public/docs/_examples/router/ts/app/auth.service.ts b/public/docs/_examples/router/ts/app/auth.service.ts index 8869bf69f3..d6f8414af1 100755 --- a/public/docs/_examples/router/ts/app/auth.service.ts +++ b/public/docs/_examples/router/ts/app/auth.service.ts @@ -10,6 +10,9 @@ import 'rxjs/add/operator/delay'; export class AuthService { isLoggedIn: boolean = false; + // store the URL so we can redirect after logging in + redirectUrl: string; + login() { return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true); } diff --git a/public/docs/_examples/router/ts/app/interfaces.ts b/public/docs/_examples/router/ts/app/can-deactivate-guard.service.ts similarity index 88% rename from public/docs/_examples/router/ts/app/interfaces.ts rename to public/docs/_examples/router/ts/app/can-deactivate-guard.service.ts index 5bd1344a53..e7774533bc 100644 --- a/public/docs/_examples/router/ts/app/interfaces.ts +++ b/public/docs/_examples/router/ts/app/can-deactivate-guard.service.ts @@ -1,4 +1,5 @@ // #docregion +import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @@ -6,6 +7,7 @@ export interface CanComponentDeactivate { canDeactivate: () => boolean | Observable; } +@Injectable() export class CanDeactivateGuard implements CanDeactivate { canDeactivate(component: CanComponentDeactivate): Observable | boolean { return component.canDeactivate ? component.canDeactivate() : true; diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.1.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.1.ts new file mode 100644 index 0000000000..4f4173a4e5 --- /dev/null +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.1.ts @@ -0,0 +1,11 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + template: ` +

CRISIS ADMINISTRATION

+

Manage your crises here

+ `, + directives: [] +}) +export class CrisisAdminComponent { } diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.ts index efb758a391..9783466d4c 100755 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-admin.component.ts @@ -1,13 +1,37 @@ // #docregion -import { Component } from '@angular/core'; -import { ROUTER_DIRECTIVES } from '@angular/router'; +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/map'; @Component({ template: `

CRISIS ADMINISTRATION

Manage your crises here

- `, - directives: [ROUTER_DIRECTIVES] -}) -export class CrisisAdminComponent { } +

Session ID: {{ sessionId | async }}

+ +

Token: {{ token | async }}

+ `, + directives: [] +}) +export class CrisisAdminComponent implements OnInit { + sessionId: Observable; + token: Observable; + + constructor(private router: Router) {} + + ngOnInit() { + // Capture the session ID if available + this.sessionId = this.router + .routerState + .queryParams + .map(params => params['session_id'] || 'None'); + + // Capture the fragment if available + this.token = this.router + .routerState + .fragment + .map(fragment => fragment || 'None'); + } +} diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts index 9a307f6541..b0d0fc9fca 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts @@ -6,7 +6,7 @@ import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisAdminComponent } from './crisis-admin.component'; -import { CanDeactivateGuard } from '../interfaces'; +import { CanDeactivateGuard } from '../can-deactivate-guard.service'; export const crisisCenterRoutes: RouterConfig = [ { diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts index 538044ebba..a7e1612297 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts @@ -6,8 +6,8 @@ import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisAdminComponent } from './crisis-admin.component'; -import { CanDeactivateGuard } from '../interfaces'; -import { AuthGuard } from '../auth.guard'; +import { CanDeactivateGuard } from '../can-deactivate-guard.service'; +import { AuthGuard } from '../auth-guard.service'; export const crisisCenterRoutes: RouterConfig = [ { diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts index f8b87190f8..6712b35ced 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts @@ -5,8 +5,8 @@ import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisAdminComponent } from './crisis-admin.component'; -import { CanDeactivateGuard } from '../interfaces'; -import { AuthGuard } from '../auth.guard'; +import { CanDeactivateGuard } from '../can-deactivate-guard.service'; +import { AuthGuard } from '../auth-guard.service'; export const crisisCenterRoutes: RouterConfig = [ { diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-detail.component.1.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-detail.component.1.ts index e226381108..fdb331dec7 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-detail.component.1.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-detail.component.1.ts @@ -5,8 +5,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/fromPromise'; -import { Crisis, CrisisService } from './crisis.service'; -import { DialogService } from '../dialog.service'; +import { Crisis, CrisisService } from './crisis.service'; +import { DialogService } from '../dialog.service'; 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 index 2557936899..95df7d567f 100644 --- 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 @@ -5,8 +5,8 @@ import { Router, ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/fromPromise'; -import { Crisis, CrisisService } from './crisis.service'; -import { DialogService } from '../dialog.service'; +import { Crisis, CrisisService } from './crisis.service'; +import { DialogService } from '../dialog.service'; @Component({ template: ` diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-list.component.1.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-list.component.1.ts index 79ad5152fe..c962ee0134 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-list.component.1.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-list.component.1.ts @@ -1,7 +1,7 @@ // #docplaster // #docregion import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; 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 index 5c964eaf58..df51b14ade 100644 --- 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 @@ -1,7 +1,7 @@ // #docplaster // #docregion import { Component, OnInit, OnDestroy } from '@angular/core'; -import { Router, ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; 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 index bfa9ca6d0f..7d5fb1cf96 100644 --- a/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts +++ b/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts @@ -56,7 +56,7 @@ export class HeroDetailComponent implements OnInit, OnDestroy { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. - this.router.navigate(['/heroes'], { queryParams: { id: heroId, foo: 'foo' } }); + this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]); } // #enddocregion gotoHeroes-navigate } diff --git a/public/docs/_examples/router/ts/app/heroes/hero-list.component.2.ts b/public/docs/_examples/router/ts/app/heroes/hero-list.component.2.ts deleted file mode 100644 index 482b70c381..0000000000 --- a/public/docs/_examples/router/ts/app/heroes/hero-list.component.2.ts +++ /dev/null @@ -1,64 +0,0 @@ -// #docplaster -// #docregion -// TODO SOMEDAY: Feature Componetized like CrisisCenter -import { Component, OnInit, OnDestroy } from '@angular/core'; -// #docregion import-router -import { Router } from '@angular/router'; -// #enddocregion import-router - -import { Hero, HeroService } from './hero.service'; - -@Component({ - // #docregion template - template: ` -

HEROES

-
    -
  • - {{hero.id}} {{hero.name}} -
  • -
- ` - // #enddocregion template -}) -export class HeroListComponent implements OnInit, OnDestroy { - heroes: Hero[]; - - // #docregion ctor - private selectedId: number; - private sub: any; - - constructor( - private service: HeroService, - private router: Router) {} - // #enddocregion ctor - - ngOnInit() { - this.sub = this.router - .routerState - .queryParams - .subscribe(params => { - this.selectedId = +params['id']; - this.service.getHeroes() - .then(heroes => this.heroes = heroes); - }); - } - - ngOnDestroy() { - this.sub.unsubscribe(); - } - // #enddocregion ctor - - // #docregion isSelected - isSelected(hero: Hero) { return hero.id === this.selectedId; } - // #enddocregion isSelected - - // #docregion select - onSelect(hero: Hero) { - this.router.navigate(['/hero', hero.id]); - } - // #enddocregion select - -} -// #enddocregion 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 index 482b70c381..72dd3e0b25 100644 --- a/public/docs/_examples/router/ts/app/heroes/hero-list.component.ts +++ b/public/docs/_examples/router/ts/app/heroes/hero-list.component.ts @@ -3,7 +3,7 @@ // TODO SOMEDAY: Feature Componetized like CrisisCenter import { Component, OnInit, OnDestroy } from '@angular/core'; // #docregion import-router -import { Router } from '@angular/router'; +import { Router, ActivatedRoute } from '@angular/router'; // #enddocregion import-router import { Hero, HeroService } from './hero.service'; @@ -31,13 +31,13 @@ export class HeroListComponent implements OnInit, OnDestroy { constructor( private service: HeroService, + private route: ActivatedRoute, private router: Router) {} // #enddocregion ctor ngOnInit() { - this.sub = this.router - .routerState - .queryParams + this.sub = this.route + .params .subscribe(params => { this.selectedId = +params['id']; this.service.getHeroes() diff --git a/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts b/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts index d5d092c016..f6c2b19c75 100644 --- a/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts +++ b/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts @@ -1,7 +1,7 @@ // #docregion -import { RouterConfig } from '@angular/router'; -import { HeroListComponent } from './hero-list.component'; -import { HeroDetailComponent } from './hero-detail.component'; +import { RouterConfig } from '@angular/router'; +import { HeroListComponent } from './hero-list.component'; +import { HeroDetailComponent } from './hero-detail.component'; export const heroesRoutes: RouterConfig = [ { path: 'heroes', component: HeroListComponent }, diff --git a/public/docs/_examples/router/ts/app/login.component.ts b/public/docs/_examples/router/ts/app/login.component.ts index 2790bfc79d..ddee339011 100755 --- a/public/docs/_examples/router/ts/app/login.component.ts +++ b/public/docs/_examples/router/ts/app/login.component.ts @@ -29,9 +29,12 @@ export class LoginComponent { this.authService.login().subscribe(() => { this.setMessage(); if (this.authService.isLoggedIn) { - // Todo: capture where the user was going and nav there. - // Meanwhile redirect the user to the crisis admin - this.router.navigate(['/crisis-center/admin']); + // Get the redirect URL from our auth service + // If no redirect has been set, use the default + let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/crisis-center/admin'; + + // Redirect the user + this.router.navigate([redirect]); } }); } diff --git a/public/docs/_examples/router/ts/app/login.routes.ts b/public/docs/_examples/router/ts/app/login.routes.ts index f50d2d7264..8bfc1ff10d 100644 --- a/public/docs/_examples/router/ts/app/login.routes.ts +++ b/public/docs/_examples/router/ts/app/login.routes.ts @@ -1,6 +1,6 @@ // #docregion import { RouterConfig } from '@angular/router'; -import { AuthGuard } from './auth.guard'; +import { AuthGuard } from './auth-guard.service'; import { AuthService } from './auth.service'; import { LoginComponent } from './login.component'; diff --git a/public/docs/_examples/router/ts/app/not-found.component.ts b/public/docs/_examples/router/ts/app/not-found.component.ts new file mode 100644 index 0000000000..4a9f60cc32 --- /dev/null +++ b/public/docs/_examples/router/ts/app/not-found.component.ts @@ -0,0 +1,9 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + template: ` +

Page Not Found

+ ` +}) +export class PageNotFoundComponent {} diff --git a/public/docs/ts/latest/guide/router.jade b/public/docs/ts/latest/guide/router.jade index c3199aff0c..b3930bdebf 100644 --- a/public/docs/ts/latest/guide/router.jade +++ b/public/docs/ts/latest/guide/router.jade @@ -45,13 +45,16 @@ include ../_util-fns * the [link parameters array](#link-parameters-array) that propels router navigation * navigating when the user clicks a data-bound [RouterLink](#router-link) * navigating under [program control](#navigate) + * toggling css classes for the [active router link](#router-link-active) * embedding critical information in the URL with [route parameters](#route-parameters) * add [child routes](#child-routing-component) under a feature section * [redirecting](#redirect) from one route to another * confirming or canceling navigation with [guards](#guards) * [CanActivate](#can-activate-guard) to prevent navigation to a route - * [CanDeactivate](#can-deactivate-deactivate) to prevent navigation away from the current route + * [CanDeactivate](#can-deactivate-guard) to prevent navigation away from the current route * passing optional information in [query parameters](#query-parameters) + * persisting information across routes with [global query parameters](#global-query-parameters) + * jumping to anchor elements using a [fragment](#fragment) * choosing the "HTML5" or "hash" [URL style](#browser-url-styles) We proceed in phases marked by milestones building from a simple two-pager with placeholder views @@ -92,7 +95,7 @@ include ../_util-fns A router has no routes until we configure it. The preferred way is to bootstrap our application with an array of routes using the **`provideRouter`** function. - In the following example, we configure our application with three route definitions. + In the following example, we configure our application with four route definitions. +makeExample('router/ts/app/app.routes.1.ts','route-config','app/app.routes.ts')(format='.') .l-sub-section @@ -108,6 +111,10 @@ include ../_util-fns will use that value to find and present the hero whose `id` is 42. We'll learn more about route parameters later in this chapter. + The `**` in the fourth route denotes a **wildcard** path for our route. The router will match this route + if the URL requested doesn't match any paths for routes defined in our configuration. This is useful for + displaying a 404 page or redirecting to another route. + We pass the configuration array to the `provideRouter()` function which returns (among other things) a configured *Router* [service provider](dependency-injection.html#!#injector-providers). @@ -132,20 +139,42 @@ code-example(format="", language="html"). But most of the time we navigate as a result of some user action such as the click of an anchor tag. - We add a **`RouterLink`** directive to the anchor tag and bind it to a template expression that + We add a **`RouterLink`** directive to the anchor tag. Since + we know our link doesn't contain any dynamic information, we can use a one-time binding to our route *path*. + + If our `RouterLink` needed to be more dynamic we could bind to a template expression that returns an array of route link parameters (the **link parameters array**). The router ultimately resolves that array into a URL and a component view. + We also add a **`RouterLinkActive`** directive to each anchor tag to add or remove CSS classes to the + element when the associated *RouterLink* becomes active. The directive can be added directly on the element + or on its parent element. + 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 path of a route. + We're adding two anchor tags with `RouterLink` and `RouterLinkActive` directives. + We bind each `RouterLink` to a string containing the path of a route. '/crisis-center' and '/heroes' are the paths of the `Routes` we configured above. - We'll learn to write more complex link expressions — and why they are arrays — + We'll learn to write link expressions — and why they are arrays — [later](#link-parameters-array) in the chapter. + + We define `active` as the CSS class we want toggled to each `RouterLink` when they become + the current route using the `RouterLinkActive ` directive. We could add multiple classes to + the `RouterLink` if we so desired. +:marked + ### Router State + After the end of each successful navigation lifecycle, the router builds a tree of `ActivatedRoute`s, + which make up the current state of the router. We can access the current `RouterState` from anywhere in our + application using the `Router` service and the `routerState` property. + + The router state provides us with methods to traverse up and down the route tree from any activated route + to get information we may need from parent, child and sibling routes. It also contains the URL *fragment* + and *query parameters* which are **global** to all routes. We'll use the `RouterState` to access + [Query Parameters](#query-parameters). + :marked ### Let's summarize @@ -181,7 +210,18 @@ table 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. + that is bound to a string or a Link Parameters Array triggers a navigation. + tr + td RouterLinkActive + td. + The directive for adding/removing classes from an HTML element when an associated + routerLink contained on or inside the element becomes active/inactive. + tr + td RouterState + td. + The current state of the router including a tree of the currently activated + activated routes in our application along with the URL query params, fragment + and convenience methods for traversing the route tree. tr td Link Parameters Array td. @@ -432,23 +472,37 @@ h3#router-outlet RouterOutlet h3#router-link RouterLink binding :marked 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 `RouterLink` directive that look like `routerLink="..."`. We imported `RouterLink` from the router library. - The template expression to the right of the equals (=) returns a *link parameters array*. - - A link parameters array holds the ingredients for router navigation: - * the *path* of the route to the destination component - * optional route and query parameters that go into the route URL - - The arrays in this example each have a single string parameter, the path of a route that + The links in this example each have a string path, the path of a route that we configured earlier. We don't have route parameters yet. + + We can also add more contextual information to our `RouterLink` by providing query string parameters + or a URL fragment for jumping to different areas on our page. Query string parameters + are provided through the `[queryParams]` binding which takes an object (e.g. `{ name: 'value' }`), while the URL fragment + takes a single value bound to the `[fragment]` input binding. .l-sub-section :marked - Learn more about the link parameters array in the [appendix below](#link-parameters-array). + Learn about the how we can also use the **link parameters array** in the [appendix below](#link-parameters-array). + +a#router-link-active +h3#router-link RouterLinkActive binding +:marked + On each anchor tag, we also see [Property Bindings](template-syntax.html#property-binding) to + the `RouterLinkActive` directive that look like `routerLinkActive="..."`. + + The template expression to the right of the equals (=) contains our space-delimited string of CSS classes. + We can also bind to the `RouterLinkActive` directive using an array of classes + such as `[routerLinkActive]="['...']"`. + + The `RouterLinkActive` directive toggles css classes for active `RouterLink`s based on the current `RouterState`. + This cascades down through each level in our route tree, so parent and child router links can be active at the same time. + To override this behavior, we can bind to the `[routerLinkActiveOptions]` input binding with the `{ exact: true }` expression. + By using `{ exact: true }`, a given `RouterLink` will only be active if its URL is an exact match to the current URL. h3#router-directives ROUTER_DIRECTIVES :marked - `RouterLink` and `RouterOutlet` are directives in the `ROUTER_DIRECTIVES` collection. + `RouterLink`, `RouterLinkActive` and `RouterOutlet` are directives in the `ROUTER_DIRECTIVES` collection. Remember to add them to the `directives` array of the `@Component` metadata. +makeExample('router/ts/app/app.component.1.ts','directives')(format=".") :marked @@ -463,7 +517,7 @@ h3#router-directives ROUTER_DIRECTIVES We've learned how to * load the router library - * add a nav bar to the shell template with anchor tags and `routerLink` directives + * add a nav bar to the shell template with anchor tags, `routerLink` and `routerLinkActive` directives * added a `router-outlet` to the shell template where views will be displayed * configure the router with `provideRouter` * set the router to compose "HTML 5" browser URLs. @@ -647,9 +701,8 @@ h3#navigate Navigate to hero detail imperatively which we implement as follows: +makeExample('router/ts/app/heroes/hero-list.component.1.ts','select')(format=".") :marked - It calls the router's **`navigate`** method with a **Link Parameters Array**. - This array is similar to the *link parameters array* we met [earlier](#shell-template) in an anchor tag while - binding to the `RouterLink` directive. This time we see it in code rather than in HTML. + It calls the router's **`navigate`** method with a **Link Parameters Array**. We can use this same syntax + with a `RouterLink` if we want to use it HTML rather than code. h3#route-parameters Setting the route parameters in the list view :marked @@ -755,7 +808,7 @@ h3#nav-to-list Navigating back to the list component back to the `HeroListComponent`. The router `navigate` method takes the same one-item *link parameters array* - that we bound to the application shell's *Heroes* `[routerLink]` directive. + that we can bind to a `[routerLink]` directive. It holds the **path to the `HeroListComponent`**: +makeExample('router/ts/app/heroes/hero-detail.component.1.ts','gotoHeroes')(format=".") :marked @@ -926,10 +979,10 @@ h3#child-routing-component Child Routing Component It has its own `RouterOutlet` and its own child routes. We create a `crisis-center.routes.ts` file as we did the `heroes.routes.ts` file. - But this time we define **child routes** *within* the parent `/crisis-center` route. + But this time we define **child routes** *within* the parent `crisis-center` route. +makeExample('router/ts/app/crisis-center/crisis-center.routes.1.ts', 'routes', 'app/crisis-center/crisis-center.routes.ts (Routes)' )(format='.') :marked - Notice that the parent `/crisis-center` route has a `children` property + Notice that the parent `crisis-center` route has a `children` property with an array of two routes. These two routes navigate to the two *Crisis Center* child components, `CrisisListComponent` and `CrisisDetailComponent`. @@ -1079,13 +1132,21 @@ h3#can-activate-guard CanActivate: requiring authentication We intend to extend the Crisis Center with some new *administrative* features. Those features aren't defined yet. So we add the following placeholder component. -+makeExample('router/ts/app/crisis-center/crisis-admin.component.ts', '', 'crisis-admin.component.ts')(format=".") ++makeExample('router/ts/app/crisis-center/crisis-admin.component.1.ts', '', 'crisis-admin.component.ts')(format=".") :marked Next, we add a child route to the `crisis-center.routes` with the path, `/admin`. +makeExample('router/ts/app/crisis-center/crisis-center.routes.3.ts', 'admin-route-no-guard', 'crisis-center.routes.ts (admin route)')(format=".") :marked And we add a link to the `AppComponent` shell that users can click to get to this feature. +makeExample('router/ts/app/app.component.4.ts', 'template', 'app/app.component.ts (template)')(format=".") + +.l-sub-section + :marked + Since our admin `RouterLink` is a child route of our `Crisis Center`, we only want the `Crisis Center` + link to be active when we visit that route. We've added an additional binding to our `/crisis-center` routerLink, + `[routerLinkActiveOptions]="{ exact: true }"` which will only mark the `/crisis-center` link as active when + we navigate the to `/crisis-center` URL and not when we navigate to one its child routes. + :marked #### Guard the admin feature Currently every route within our *Crisis Center* is open to everyone. @@ -1096,11 +1157,11 @@ h3#can-activate-guard CanActivate: requiring authentication Instead we'll write a `CanActivate` guard to redirect anonymous users to the login page when they try to reach the admin component. This is a general purpose guard — we can imagine other features that require authenticated users — - so we create an `auth.guard.ts` in the application root folder. + so we create an `auth-guard.service.ts` in the application root folder. At the moment we're interested in seeing how guards work so our first version does nothing useful. It simply logs to console and `returns` true immediately, allowing navigation to proceed: -+makeExample('router/ts/app/auth.guard.1.ts', '', 'app/auth.guard.ts')(format=".") ++makeExample('router/ts/app/auth-guard.service.1.ts', '', 'app/auth-guard.service.ts')(format=".") :marked Next we open `crisis-center.routes.ts `, import the `AuthGuard` class, and update the admin route with a `CanActivate` guard property that references it: @@ -1117,9 +1178,10 @@ h3#can-activate-guard CanActivate: requiring authentication Although it doesn't actually log in, it has what we need for this discussion. It has an `isLoggedIn` flag to tell us whether the user is authenticated. Its `login` method simulates an API call to an external service by returning an observable that resolves successfully after a short pause. + The `redirectUrl` property will store our attempted URL so we can navigate to it after authenticating. Let's revise our `AuthGuard` to call it. -+makeExample('router/ts/app/auth.guard.ts', '', 'app/auth.guard.ts (v.2)')(format=".") ++makeExample('router/ts/app/auth-guard.service.2.ts', '', 'app/auth-guard.service.ts (v.2)')(format=".") :marked Notice that we *inject* the `AuthService` and the `Router` in the constructor. We haven't provided the `AuthService` yet but it's good to know that we can inject helpful services into our routing guards. @@ -1127,11 +1189,16 @@ h3#can-activate-guard CanActivate: requiring authentication This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues. - If the user is not logged in, we tell the router to navigate to a login page — a page we haven't created yet. - This secondary navigation automatically cancels the current navigation; we return `false` just to be clear about that. + The `ActivatedRouteSnapshot` contains the _future_ route that will be activated and the `RouterStateSnapshot` + contains the _future_ `RouterState` of our application, should we pass through our guard check. + + If the user is not logged in, we store the attempted URL the user came from using the `RouterStateSnapshot.url` and + tell the router to navigate to a login page — a page we haven't created yet. + This secondary navigation automatically cancels the current navigation; we return `false` just to be clear about that. #### Add the *LoginComponent* - We need a `LoginComponent` for the user to log in to the app. + We need a `LoginComponent` for the user to log in to the app. After logging in, we'll redirect + to our stored URL if available, or use the default URL. There is nothing new about this component or the way we wire it into the router configuration. Here is the pertinent code, offered without comment: +makeTabs( @@ -1212,7 +1279,7 @@ h3#can-deactivate-guard CanDeactivate: handling unsaved changes We create a `Guard` that will check for the presence of a `canDeactivate` function in our component, in this case being `CrisisDetailComponent`. We don't need to know the details of how our `CrisisDetailComponent` confirms deactivation. This makes our guard reusable, which is an easy win for us. -+makeExample('router/ts/app/interfaces.ts', '', 'interfaces.ts') ++makeExample('router/ts/app/can-deactivate-guard.service.ts', '', 'can-deactivate-guard.service.ts') :marked Looking at our `CrisisDetailComponent`, we have implemented our confirmation workflow for unsaved changes. @@ -1245,23 +1312,23 @@ h3#can-deactivate-guard CanDeactivate: handling unsaved changes +makeTabs( `router/ts/app/app.component.ts, - router/ts/app/auth.guard.ts, + router/ts/app/auth-guard.service.2.ts, + router/ts/app/can-deactivate-guard.service.ts, router/ts/app/crisis-center/crisis-center.component.ts, router/ts/app/crisis-center/crisis-center.routes.ts, router/ts/app/crisis-center/crisis-list.component.1.ts, router/ts/app/crisis-center/crisis-detail.component.1.ts, - router/ts/app/crisis-center/crisis.service.ts, - router/ts/app/interfaces.ts + router/ts/app/crisis-center/crisis.service.ts `, null, `app.component.ts, - auth.guard.ts, + auth-guard.service.ts, + can-deactivate-guard.service.ts, crisis-center.component.ts, crisis-center.routes.ts, crisis-list.component.ts, crisis-detail.component.ts, - crisis.service.ts, - interfaces.ts + crisis.service.ts `) @@ -1297,7 +1364,7 @@ figure.image-display Almost anything serializable can appear in a query string. The Component Router supports navigation with query strings as well as route parameters. - We define query string parameters in the *route parameters object* just like we do with route parameters. + We define _optional_ query string parameters in an *object* after we define our required route parameters. ### Route Parameters or Query Parameters? @@ -1332,7 +1399,7 @@ figure.image-display Now we have a reason. We'd like to send the id of the current hero with the navigation request so that the `HeroListComponent` can highlight that hero in its list. - We do that with a `NavigationExtras` object with `queryParams`. + We do that with an object that contains our optional `id` parameter. We also defined a junk parameter (`foo`) that the `HeroListComponent` should ignore. Here's the revised navigation statement: +makeExample('router/ts/app/heroes/hero-detail.component.ts','gotoHeroes-navigate')(format=".") @@ -1347,15 +1414,27 @@ figure.image-display :marked It should look something like this, depending on where you run it: code-example(format="." language="bash"). - localhost:3000/heroes?id=15&foo=foo + localhost:3000/heroes;id=15&foo=foo :marked - The `id` value appears in the query string (`?id=15&foo=foo`), not in the URL path. + The `id` value appears in the query string (`;id=15&foo=foo`), not in the URL path. The path for the "Heroes" route doesn't have an `:id` token. - -// .alert.is-helpful +:marked + The query string parameters are not separated by "?" and "&". + They are **separated by semicolons (;)** + This is *matrix URL* notation — something we may not have seen before. +.l-sub-section :marked - The router replaces route path tokens with corresponding values from the route parameters object. - **Every parameter _not_ consumed by a route path goes in the query string.** + *Matrix URL* notation is an idea first floated + in a [1996 proposal](http://www.w3.org/DesignIssues/MatrixURIs.html) by the founder of the web, Tim Berners-Lee. + + Although matrix notation never made it into the HTML standard, it is legal and + it became popular among browser routing systems as a way to isolate parameters + belonging to parent and child routes. The Angular Component Router is such a system. + + The syntax may seem strange to us but users are unlikely to notice or care + as long as the URL can be emailed and pasted into a browser address bar + as this one can. + :marked ### Query parameters in the *ActivatedRoute* service @@ -1375,12 +1454,11 @@ code-example(format="." language="bash"). in the `ActivatedRoute` service. We injected that service in the constructor of the `HeroDetailComponent`. This time we'll be navigating in the opposite direction, from the `HeroDetailComponent` to the `HeroListComponent`. - This time we'll inject the `Router` service in the constructor of the `HeroListComponent`. First we extend the router import statement to include the `ActivatedRoute` service symbol; +makeExample('router/ts/app/heroes/hero-list.component.ts','import-router', 'hero-list.component.ts (import)')(format=".") :marked - Then we use the `routerState` to access the globally available query parameters `Observable` so we can subscribe + Then we use the `ActivatedRoute` to access the `params` _Observable_ so we can subscribe and extract the `id` parameter as the `selectedId`: +makeExample('router/ts/app/heroes/hero-list.component.ts','ctor', 'hero-list.component.ts (constructor)')(format=".") .l-sub-section @@ -1402,52 +1480,49 @@ figure.image-display :marked The `foo` query string parameter is harmless and continues to be ignored. - ### Child Routers and Query Parameters + + +:marked + ### Global Query parameters and Fragments +:marked + In our [query parameters](#query-parameters) example, we only dealt with parameters specific to + our route, but what if we wanted optional parameters available to all routes? This is where our + query parameters come into play and serve a special purpose in our application. - We can define query parameters for child routers too. + Traditional query string parameters (?name=value) **persist** across route navigations. This means we can pass these query params + around without having to specify them in each navigation method whether it be declaratively or imperatively. - The technique is precisely the same. - In fact, we made exactly the same changes to the *Crisis Center* feature. - Confirm the similarities in these *Hero* and *CrisisCenter* components, - arranged side-by-side for easy comparison: -+makeTabs( - `router/ts/app/heroes/hero-list.component.ts, - router/ts/app/crisis-center/crisis-list.component.ts, - router/ts/app/heroes/hero-detail.component.ts, - router/ts/app/crisis-center/crisis-detail.component.ts - `, - null, - `hero-list.component.ts, - crisis-list.component.ts, - hero-detail.component.ts, - crisis-detail.component.ts - `) + [Fragments](https://en.wikipedia.org/wiki/Fragment_identifier) refer to certain elements on the page + identified with an `id` attribute. + + We'll update our `AuthGuard` to provide a `session_id` query that will remain after navigating to another route. + + We'll also provide an arbitrary `anchor` fragment, which we would use to jump to a certain point on our page. + + We'll add the extra navigation object to our `router.navigate` method that navigates us to our `/login` route. ++makeExample('router/ts/app/auth-guard.service.ts','', 'auth-guard.service.ts (v.3)') :marked - When we navigate back from a `CrisisDetailComponent` that is showing the *Asteroid* crisis, - we see that crisis properly selected in the list like this: -figure.image-display - img(src='/resources/images/devguide/router/selected-crisis.png' alt="Selected crisis" ) + Since we'll be navigating to our *Crisis Admin* route after logging in, we'll update it to handle our global + query parameters and fragment. + ++makeExample('router/ts/app/crisis-center/crisis-admin.component.ts','', 'crisis-admin.component.ts (v.2)') :marked - **Look at the browser address bar again**. It's *different*. It looks something like this: -code-example(format="." language="bash"). - localhost:3000/crisis-center/;id=3;foo=foo -:marked - The query string parameters are no longer separated by "?" and "&". - They are **separated by semicolons (;)** - This is *matrix URL* notation — something we may not have seen before. + *Query Parameters* and *Fragments* are available through the `routerState` property in our `Router` service. + Just like our *route parameters*, global query parameters and fragments are provided as an `Observable`. + For our updated *Crisis Admin* component we'll feed the `Observable` directly into our template using the `AsyncPipe`, which + will handle _unsubscribing_ from the `Observable` for us when the component is destroyed. + .l-sub-section + img(src='/resources/images/devguide/plunker-separate-window-button.png' alt="pop out the window" align="right" style="margin-right:-20px") :marked - *Matrix URL* notation is an idea first floated - in a [1996 proposal](http://www.w3.org/DesignIssues/MatrixURIs.html) by the founder of the web, Tim Berners-Lee. - - Although matrix notation never made it into the HTML standard, it is legal and - it became popular among browser routing systems as a way to isolate parameters - belonging to parent and child routes. The Angular Component Router is such a system. - - The syntax may seem strange to us but users are unlikely to notice or care - as long as the URL can be emailed and pasted into a browser address bar - as this one can. + When running in plunker, pop out the preview window by clicking the blue 'X' button in the upper right corner. +:marked + Following the steps in this process, we can click on the *Crisis Admin* button, that takes us to the *Login* + page with our provided `query params` and `fragment`. After we click the login button, we notice that + we have been redirected to the `Crisis Admin` page with our `query params` and `fragment` still intact. We can use + these persistent bits of information for things that need to be provided with every page interaction like + authentication tokens or session ids. .l-main-section @@ -1472,13 +1547,20 @@ code-example(format="." language="bash"). ## Appendix: Link Parameters Array We've mentioned the *Link Parameters Array* several times. We've used it several times. - We've bound the `RouterLink` directive to such an array like this: + A link parameters array holds the ingredients for router navigation: + * the *path* of the route to the destination component + * required route parameters and optional query parameters that go into the route URL + + We can bind the `RouterLink` directive to such an array like this: +makeExample('router/ts/app/app.component.3.ts', 'h-anchor')(format=".") :marked We've written a two element array when specifying a route parameter like this +makeExample('router/ts/app/heroes/hero-list.component.1.ts', 'nav-to-detail')(format=".") :marked - These two examples cover our needs for an app with one level routing. + We can provide optional query parameters in an object like this: ++makeExample('router/ts/app/app.component.3.ts', 'cc-query-params')(format=".") +:marked + These three examples cover our needs for an app with one level routing. The moment we add a child router, such as the *Crisis Center*, we create new link array possibilities. Recall that we specified a default child route for *Crisis Center* so this simple `RouterLink` is fine. @@ -1499,7 +1581,7 @@ code-example(format="." language="bash"). * There are no parameters for this parent route so we're done with it. * The second item identifies the child route for details about a particular crisis ('/:id'). * The details child route requires an `id` route parameter - * We add `id` of the *Dragon Crisis* as the third item in the array (`1`) + * We add `id` of the *Dragon Crisis* as the second item in the array (`1`) It looks like this! +makeExample('router/ts/app/app.component.3.ts', 'Dragon-anchor')(format=".") From f50dff818a37ce1cf3043bfbcbc724d87694e4a7 Mon Sep 17 00:00:00 2001 From: Torgeir Helgevold Date: Mon, 23 May 2016 22:24:15 -0400 Subject: [PATCH 04/25] docs(toh-pt6): add observables to HTTP tutorial chapter closes #1528 --- public/docs/_examples/toh-6/e2e-spec.ts | 23 ++- .../_examples/toh-6/ts/app/app.component.ts | 3 + .../toh-6/ts/app/dashboard.component.html | 2 + .../toh-6/ts/app/dashboard.component.ts | 4 +- .../toh-6/ts/app/hero-search.component.html | 11 ++ .../toh-6/ts/app/hero-search.component.ts | 53 ++++++ .../toh-6/ts/app/hero-search.service.ts | 19 ++ .../_examples/toh-6/ts/app/hero.service.ts | 4 +- .../_examples/toh-6/ts/app/rxjs-extensions.ts | 13 ++ public/docs/_examples/toh-6/ts/sample.css | 17 ++ public/docs/ts/latest/tutorial/toh-pt6.jade | 164 +++++++++++++++++- .../images/devguide/toh/toh-hero-search.png | Bin 0 -> 10942 bytes 12 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.component.html create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.component.ts create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.service.ts create mode 100644 public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts create mode 100644 public/resources/images/devguide/toh/toh-hero-search.png diff --git a/public/docs/_examples/toh-6/e2e-spec.ts b/public/docs/_examples/toh-6/e2e-spec.ts index 96277a910a..e2ab2602dc 100644 --- a/public/docs/_examples/toh-6/e2e-spec.ts +++ b/public/docs/_examples/toh-6/e2e-spec.ts @@ -23,10 +23,31 @@ describe('TOH Http Chapter', function () { addButton: element.all(by.buttonText('Add New Hero')).get(0), - heroDetail: element(by.css('my-app my-hero-detail')) + heroDetail: element(by.css('my-app my-hero-detail')), + + searchBox: element(by.css('#search-box')), + searchResults: element.all(by.css('.search-result')) }; } + it('should search for hero and navigate to details view', function() { + let page = getPageStruct(); + + return sendKeys(page.searchBox, 'Magneta').then(function () { + expect(page.searchResults.count()).toBe(1); + let hero = page.searchResults.get(0); + return hero.click(); + }) + .then(function() { + browser.waitForAngular(); + let inputEle = page.heroDetail.element(by.css('input')); + return inputEle.getAttribute('value'); + }) + .then(function(value) { + expect(value).toBe('Magneta'); + }); + }); + it('should be able to add a hero from the "Heroes" view', function(){ let page = getPageStruct(); let heroCount: webdriver.promise.Promise; diff --git a/public/docs/_examples/toh-6/ts/app/app.component.ts b/public/docs/_examples/toh-6/ts/app/app.component.ts index 2a1ff50ba3..d49c87ccbf 100644 --- a/public/docs/_examples/toh-6/ts/app/app.component.ts +++ b/public/docs/_examples/toh-6/ts/app/app.component.ts @@ -4,6 +4,9 @@ import { Component } from '@angular/core'; import { ROUTER_DIRECTIVES } from '@angular/router'; import { HeroService } from './hero.service'; +// #docregion rxjs-extensions +import './rxjs-extensions'; +// #enddocregion rxjs-extensions @Component({ selector: 'my-app', diff --git a/public/docs/_examples/toh-6/ts/app/dashboard.component.html b/public/docs/_examples/toh-6/ts/app/dashboard.component.html index 028eab6eb3..e22a2a5ebb 100644 --- a/public/docs/_examples/toh-6/ts/app/dashboard.component.html +++ b/public/docs/_examples/toh-6/ts/app/dashboard.component.html @@ -9,3 +9,5 @@ + + diff --git a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts index 08ffecc0ea..f7b4100cee 100644 --- a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts +++ b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts @@ -5,11 +5,13 @@ import { Router } from '@angular/router'; import { Hero } from './hero'; import { HeroService } from './hero.service'; +import { HeroSearchComponent } from './hero-search.component'; @Component({ selector: 'my-dashboard', templateUrl: 'app/dashboard.component.html', - styleUrls: ['app/dashboard.component.css'] + styleUrls: ['app/dashboard.component.css'], + directives: [HeroSearchComponent] }) export class DashboardComponent implements OnInit { diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.html b/public/docs/_examples/toh-6/ts/app/hero-search.component.html new file mode 100644 index 0000000000..47c853746b --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.html @@ -0,0 +1,11 @@ + +
+

Hero Search

+ +
+
+ {{hero.name}} +
+
+
diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts new file mode 100644 index 0000000000..2b4d155046 --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts @@ -0,0 +1,53 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +import { HeroSearchService } from './hero-search.service'; +import { Hero } from './hero'; + +@Component({ + selector: 'hero-search', + templateUrl: 'app/hero-search.component.html', + providers: [HeroSearchService] +}) +export class HeroSearchComponent implements OnInit { + // #docregion subject + search = new Subject(); + // #enddocregion subject + // #docregion search + heroes: Observable; + // #enddocregion search + + constructor( + private heroSearchService: HeroSearchService, + private router: Router) {} + + + // #docregion search + ngOnInit() { + this.heroes = this.search + .asObservable() // "cast" as Observable + .debounceTime(300) // wait for 300ms pause in events + .distinctUntilChanged() // ignore if next search term is same as previous + .switchMap(term => term // switch to new observable each time + // return the http search observable + ? this.heroSearchService.search(term) + // or the observable of empty heroes if no search term + : Observable.of([])) + + .catch(error => { + // Todo: real error handling + console.log(error); + return Observable.throw(error); + }); + } + // #enddocregion search + + gotoDetail(hero: Hero) { + let link = ['/detail', hero.id]; + this.router.navigate(link); + } +} diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts new file mode 100644 index 0000000000..42018e3526 --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts @@ -0,0 +1,19 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroSearchService { + + constructor(private http: Http) {} + + // #docregion observable-search + search(term: string) { + return this.http + .get(`app/heroes/?name=${term}+`) + .map((r: Response) => r.json().data as Hero[]); + } + // #enddocregion observable-search +} diff --git a/public/docs/_examples/toh-6/ts/app/hero.service.ts b/public/docs/_examples/toh-6/ts/app/hero.service.ts index 8abbcc2778..04012768be 100644 --- a/public/docs/_examples/toh-6/ts/app/hero.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero.service.ts @@ -17,13 +17,13 @@ export class HeroService { constructor(private http: Http) { } - getHeroes(): Promise { + getHeroes() { return this.http.get(this.heroesUrl) // #docregion to-promise .toPromise() // #enddocregion to-promise // #docregion to-data - .then(response => response.json().data) + .then(response => response.json().data as Hero[]) // #enddocregion to-data // #docregion catch .catch(this.handleError); diff --git a/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts b/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts new file mode 100644 index 0000000000..a0facfe03e --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts @@ -0,0 +1,13 @@ +// #docregion +// Observable class extensions +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/throw'; + +// Observable operators +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/switchMap'; diff --git a/public/docs/_examples/toh-6/ts/sample.css b/public/docs/_examples/toh-6/ts/sample.css index 0c99008d2d..a5ac5b4d70 100644 --- a/public/docs/_examples/toh-6/ts/sample.css +++ b/public/docs/_examples/toh-6/ts/sample.css @@ -5,3 +5,20 @@ button.delete-button{ background-color: gray !important; color:white; } + +.search-result{ + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width:195px; + height: 20px; + padding: 5px; + background-color: white; + cursor: pointer; +} + +#search-box{ + width: 200px; + height: 20px; +} + diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index 5f65c5a421..0b00284f5c 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -131,9 +131,10 @@ block get-heroes-details :marked The Angular `http.get` returns an RxJS `Observable`. *Observables* are a powerful way to manage asynchronous data flows. - We'll learn about `Observables` *later*. + We'll learn about [Observables](#observables) later in this chapter. - For *now* we get back on familiar ground by immediately converting that `Observable` to a `Promise` using the `toPromise` operator. + For *now* we get back on familiar ground by immediately by + converting that `Observable` to a `Promise` using the `toPromise` operator. +makeExample('toh-6/ts/app/hero.service.ts', 'to-promise')(format=".") :marked Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box. @@ -358,6 +359,146 @@ block review figure.image-display img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editting w/ HTTP") +:marked + ## Observables + + Each `Http` method returns an `Observable` of HTTP `Response` objects. + + Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller. + In this section we learn to return the `Observable` directly and discuss when and why that might be + a good thing to do. + + ### Background + An *observable* is a stream of events that we can process with array-like operators. + + Angular core has basic support for observables. We developers augment that support with + operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library. + We'll see how shortly. + + Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`. + That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller. + + Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data. + When we receive the data, we're done. + A single result in the form of a promise is easy for the calling component to consume + and it helps that promises are widely understood by JavaScript programmers. + + But requests aren't always "one and done". We may start one request, + then cancel it, and make a different request ... before the server has responded to the first request. + Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*. + It's easy with *observables* as we'll see. + + ### Search-by-name + We're going to add a *hero search* feature to the Tour of Heroes. + As the user types a name into a search box, we'll make repeated http requests for heroes filtered by that name. + + We start by creating `HeroSearchService` that sends search queries to our server's web api. + ++makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".") + +:marked + The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`. + The notable difference: we no longer call `toPromise`. + We simply return the *observable* instead. + + ### HeroSearchComponent + Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`. + + The component template is simple - just a textbox and a list of matching search results. ++makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html') +:marked + As the user types in the search box, a *keyup* event binding calls `search.next` with the new search box value. + + The component's data bound `search` property returns a `Subject`. + A `Subject` is a producer of an _observable_ event stream. + Each call to `search.next` puts a new string into this subject's _observable_ stream. + + The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. + + But `heroes` is an `Observable` of heroes, not an array of heroes. + The `*ngFor` can't do anything with that until we flow it through the `AsyncPipe` (`heroes | async`). + The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`. + + Time to create the `HeroSearchComponent` class and metadata. ++makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts') +:marked + Scroll down to where we create the `search` subject. ++makeExample('toh-6/ts/app/hero-search.component.ts', 'subject') +:marked + We're binding to that `search` subject in our template. + The user is sending it a stream of strings, the filter criteria for the name search. + + A `Subject` is also an `Observable`. + We're going to access that `Observable` and append operators to it that turn the stream + of strings into a stream of `Hero[]` arrays. + + Each user keystroke could result in a new http request returning a new Observable array of heroes. + + This could be a very chatty, taxing our server resources and burning up our cellular network data plan. + Fortunately we can chain `Observable` operators to reduce the request flow + and still get timely results. Here's how: + ++makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") +:marked + * The `asObservable` operator casts the `Subject` as an `Observable` of filter strings. + + * `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds + before passing along the latest string. We'll never make requests more frequently than 300ms. + + * `distinctUntilChanged` ensures that we only send a request if the filter text changed. + There's no point in repeating a request for the same search term. + + * `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet. + It discards previous search observables, returning only the latest search service observable. + +.l-sub-section + :marked + The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) + (formerly known as "flatMapLatest") is very clever. + + Every qualifying key event can trigger an http call. + Even with a 300ms pause between requests, we could have multiple http requests in flight + and they may not return in the order sent. + + `switchMap` preserves the original request order while returning + only the observable from the most recent http call. + Results from prior calls will be discarded. + + We also short-circuit the http call and return an observable containing an empty array + if the search text is empty. +:marked + * `catch` intercepts a failed observable. + Our simple example prints the error to the console; a real life application should do better. + Then it re-throws the failed observable so that downstream processes know it failed. + The `AsyncPipe` in the template is downstream. It sees the failure and ignores it. + + ### Import RxJS operators + The RxJS operators are not available in Angular's base `Observable` implementation. + We have to extend `Observable` by *importing* them. + + We could extend `Observable` with just the operators we need here by + including the pertinent `import` statements at the top of this file. + +.l-sub-section + :marked + Many authorities say we should do just that. +:marked + We take a different approach in this example. + We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file. + ++makeExample('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".") +:marked + We load them all at once by importing `rxjs-extensions` in `AppComponent`. + ++makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app/app.component.ts')(format=".") +:marked + Finally, we add the `HeroSearchComponent` to the bottom of the `DashboardComponent`. + Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles. + At some point it might look like this. + +figure.image-display + img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") + .l-main-section :marked ## Application structure and code @@ -381,6 +522,10 @@ block filetree .file hero-detail.component.css .file hero-detail.component.html .file hero-detail.component.ts + .file hero-search.component.html + .file hero-search.component.ts + .file hero-search.service.ts + .file rxjs-operators.ts .file hero.service.ts .file heroes.component.css .file heroes.component.html @@ -407,7 +552,8 @@ block filetree - We extended HeroService to support post, put and delete calls. - We updated our components to allow adding, editing and deleting of heroes. - We configured an in-memory web API. - + - We learned how to use Observables. + Below is a summary of the files we changed and added. block file-summary @@ -430,3 +576,15 @@ block file-summary in-memory-data.service.ts, sample.css` ) + + +makeTabs( + `toh-6/ts/app/hero-search.service.ts, + toh-6/ts/app/hero-search.component.ts, + toh-6/ts/app/hero-search.component.html, + toh-6/ts/app/rxjs-operators.ts`, + null, + `hero-search.service.ts, + hero-search.component.ts, + hero-search.service.html, + rxjs-operators.ts` +) diff --git a/public/resources/images/devguide/toh/toh-hero-search.png b/public/resources/images/devguide/toh/toh-hero-search.png new file mode 100644 index 0000000000000000000000000000000000000000..f09fd45d7effd039bec1813174d823fd7a1fd204 GIT binary patch literal 10942 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 literal 0 HcmV?d00001 From 8c1bbbf3185bce506c099156cf06161003c4e9df Mon Sep 17 00:00:00 2001 From: Eric Jimenez Date: Wed, 13 Jul 2016 13:56:01 -0400 Subject: [PATCH 05/25] chore: alphabetize entries in resources page. Add margin under frontpage banner --- public/resources/css/module/_home-promos.scss | 2 +- public/resources/index.ejs | 2 +- .../js/controllers/resources-controller.js | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/public/resources/css/module/_home-promos.scss b/public/resources/css/module/_home-promos.scss index 6f37ecd0d9..6034b8fa8a 100644 --- a/public/resources/css/module/_home-promos.scss +++ b/public/resources/css/module/_home-promos.scss @@ -1,6 +1,6 @@ .home-rows { // NOTE (ericjim): if a banner is placed on the homescreen, add this margin. - //margin-top: 112px; + margin-top: 112px; } .home-row { diff --git a/public/resources/index.ejs b/public/resources/index.ejs index fa4f07bcb8..b0919d984c 100644 --- a/public/resources/index.ejs +++ b/public/resources/index.ejs @@ -13,7 +13,7 @@

{{subCategory}}

-
+
diff --git a/public/resources/js/controllers/resources-controller.js b/public/resources/js/controllers/resources-controller.js index ae477cbbf7..99b787e92e 100644 --- a/public/resources/js/controllers/resources-controller.js +++ b/public/resources/js/controllers/resources-controller.js @@ -38,4 +38,19 @@ angularIO.controller('ResourcesCtrl', ['$scope', '$element', '$window', '$fireba vm.selectedCategory = category; }; -}]); \ No newline at end of file +}]); + +angularIO.filter('orderObjectByOfTypeString', function() { + return function(items, field, reverse) { + var filtered = []; + angular.forEach(items, function(item) { + filtered.push(item); + }); + filtered.sort(function (a, b) { + // normalize sort by uppercasing values. + return (a[field].toUpperCase() > b[field].toUpperCase() ? 1 : -1); + }); + if(reverse) filtered.reverse(); + return filtered; + }; +}); From 72efdc5e5b5d9b57135fd426e202dd2e084f5682 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Tue, 19 Jul 2016 11:23:22 -0700 Subject: [PATCH 06/25] chore: add tslint to example package.json closes #1926 Not sure how it disappeared. --- public/docs/_examples/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 552731818b..21a52a65e3 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -73,6 +73,7 @@ "style-loader": "^0.13.1", "ts-loader": "^0.8.2", "ts-node": "^0.7.3", + "tslint": "^3.13.0", "typescript": "^1.8.10", "typings": "^1.0.4", "webpack": "^1.13.0", From ce2e14e4aec2e14f41db8216355ca29d2e6726ca Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Tue, 19 Jul 2016 12:26:14 -0700 Subject: [PATCH 07/25] docs(toh-pt6): bind to a component method rather than a RxJS Subject updated text accordingly --- .../toh-6/ts/app/hero-search.component.html | 2 +- .../toh-6/ts/app/hero-search.component.ts | 16 ++++--- public/docs/ts/latest/tutorial/toh-pt6.jade | 44 ++++++++++--------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.html b/public/docs/_examples/toh-6/ts/app/hero-search.component.html index 47c853746b..08c0560c5b 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.component.html +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.html @@ -1,7 +1,7 @@

Hero Search

- +
diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts index 2b4d155046..a69db4491d 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts @@ -14,22 +14,26 @@ import { Hero } from './hero'; providers: [HeroSearchService] }) export class HeroSearchComponent implements OnInit { - // #docregion subject - search = new Subject(); - // #enddocregion subject // #docregion search heroes: Observable; // #enddocregion search + // #docregion searchSubject + searchSubject = new Subject(); + // #enddocregion searchSubject constructor( private heroSearchService: HeroSearchService, private router: Router) {} + // #docregion searchSubject - + // Push a search term into the observable stream. + search(term: string) { this.searchSubject.next(term); } + // #enddocregion searchSubject // #docregion search + ngOnInit() { - this.heroes = this.search - .asObservable() // "cast" as Observable + this.heroes = this.searchSubject + .asObservable() // cast as Observable .debounceTime(300) // wait for 300ms pause in events .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index 0b00284f5c..342e9de7e1 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -407,39 +407,37 @@ block review The component template is simple - just a textbox and a list of matching search results. +makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html') :marked - As the user types in the search box, a *keyup* event binding calls `search.next` with the new search box value. - - The component's data bound `search` property returns a `Subject`. - A `Subject` is a producer of an _observable_ event stream. - Each call to `search.next` puts a new string into this subject's _observable_ stream. + As the user types in the search box, a *keyup* event binding calls the component's `search` with the new search box value. The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. - But `heroes` is an `Observable` of heroes, not an array of heroes. - The `*ngFor` can't do anything with that until we flow it through the `AsyncPipe` (`heroes | async`). + But, as we'll soon see, the `heroes` property returns an `Observable` of heroes, not an array of heroes. + The `*ngFor` can't do anything with an observable until we flow it through the `AsyncPipe` (`heroes | async`). The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`. Time to create the `HeroSearchComponent` class and metadata. +makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts') :marked - Scroll down to where we create the `search` subject. -+makeExample('toh-6/ts/app/hero-search.component.ts', 'subject') + Focus on the `searchSubject`. ++makeExample('toh-6/ts/app/hero-search.component.ts', 'searchSubject')(format=".") :marked - We're binding to that `search` subject in our template. - The user is sending it a stream of strings, the filter criteria for the name search. + A `Subject` is a producer of an _observable_ event stream. + This `searchSubject` produces an `Observable` of strings, the filter criteria for the name search. + + Each call to `search` puts a new string into this subject's _observable_ stream by calling `next`. A `Subject` is also an `Observable`. - We're going to access that `Observable` and append operators to it that turn the stream - of strings into a stream of `Hero[]` arrays. + We're going to access that `Observable` and turn the stream + of strings into a stream of `Hero[]` arrays, the `heroes` property. - Each user keystroke could result in a new http request returning a new Observable array of heroes. - - This could be a very chatty, taxing our server resources and burning up our cellular network data plan. - Fortunately we can chain `Observable` operators to reduce the request flow - and still get timely results. Here's how: - +makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") :marked + If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of http requests. + Bad idea. We don't want to tax our server resources and burn through our cellular network data plan. + + Fortunately we can chain `Observable` operators to the string `Observable` that reduce the request flow. + We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how: + * The `asObservable` operator casts the `Subject` as an `Observable` of filter strings. * `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds @@ -449,7 +447,7 @@ block review There's no point in repeating a request for the same search term. * `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet. - It discards previous search observables, returning only the latest search service observable. + It cancels and discards previous search observables, returning only the latest search service observable. .l-sub-section :marked @@ -462,10 +460,14 @@ block review `switchMap` preserves the original request order while returning only the observable from the most recent http call. - Results from prior calls will be discarded. + Results from prior calls are canceled and discarded. We also short-circuit the http call and return an observable containing an empty array if the search text is empty. + + Note that _canceling_ the `HeroSearchService` observable won't actually abort a pending http request + until the service supports that feature, a topic for another day. + We are content for now to discard unwanted results. :marked * `catch` intercepts a failed observable. Our simple example prints the error to the console; a real life application should do better. From 753452650c14c32b0e27eb4902708d65f30159e2 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Tue, 19 Jul 2016 12:35:39 -0700 Subject: [PATCH 08/25] docs(cheatsheet/dart): generate from dart sources (#1912) * docs(cheatsheet/dart): generate from dart sources Fixes #1906. --- gulpfile.js | 34 +++++-- public/docs/dart/latest/cheatsheet.jade | 2 +- tools/dart-api-builder/index.js | 92 +++++++++++++++++++ .../dart-api-builder/services/packageInfo.js | 38 ++++++++ 4 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 tools/dart-api-builder/index.js create mode 100644 tools/dart-api-builder/services/packageInfo.js diff --git a/gulpfile.js b/gulpfile.js index ab6df29ec5..3b0743ac34 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -577,7 +577,6 @@ gulp.task('build-js-api-docs', ['_shred-api-examples'], function() { }); gulp.task('build-dart-api-docs', ['_shred-api-examples', 'dartdoc'], function() { - // TODO(chalin): also build build-dart-cheatsheet return buildApiDocsForDart(); }); @@ -585,11 +584,8 @@ gulp.task('build-plunkers', ['_copy-example-boilerplate'], function() { return plunkerBuilder.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log }); }); -gulp.task('build-dart-cheatsheet', ['build-ts-api-docs'], function() { - gutil.log('build-dart-cheatsheet - NOT IMPLEMENTED YET - copying TS cheatsheet data'); - const src = './public/docs/ts/latest/guide/cheatsheet.json'; - fs.copy(src, './public/docs/dart/latest/guide/cheatsheet.json', {clobber: true}, - (err) => { if(err) throw err }); +gulp.task('build-dart-cheatsheet', [], function() { + return buildDartCheatsheet(); }); gulp.task('dartdoc', ['pub upgrade'], function() { @@ -1190,6 +1186,32 @@ function buildApiDocs(targetLanguage) { } +function buildDartCheatsheet() { + 'use strict'; + const ALLOWED_LANGUAGES = ['ts', 'js', 'dart']; + const lang = 'dart'; + const vers = 'latest'; + checkAngularProjectPath(ngPathFor(lang)); + try { + const pkg = new Package('dartApiDocs', [require(path.resolve(TOOLS_PATH, 'dart-api-builder'))]); + pkg.config(function(log, targetEnvironments, writeFilesProcessor) { + log.level = _dgeniLogLevel; + ALLOWED_LANGUAGES.forEach(function(target) { targetEnvironments.addAllowed(target); }); + targetEnvironments.activate(lang); + const outputPath = path.join(lang, vers, 'can-be-any-name-read-comment-below'); + // Note: cheatsheet data gets written to: outputPath + '/../guide'; + writeFilesProcessor.outputFolder = outputPath; + }); + var dgeni = new Dgeni([pkg]); + return dgeni.generate(); + } catch(err) { + console.error(err); + console.error(err.stack); + throw err; + } +} + + function buildApiDocsForDart() { const apiDir = 'api'; const vers = 'latest'; diff --git a/public/docs/dart/latest/cheatsheet.jade b/public/docs/dart/latest/cheatsheet.jade index 739ad031ab..77c0f11d36 100644 --- a/public/docs/dart/latest/cheatsheet.jade +++ b/public/docs/dart/latest/cheatsheet.jade @@ -2,7 +2,7 @@ .banner.grid-fluid .alert.is-important :marked - **Known issue:** Some cheat sheet entries are currently inaccurate, reflecting TypeScript instead of Dart. + This cheat sheet is provisional and subject to change. article(class="l-content-small grid-fluid docs-content") .cheatsheet diff --git a/tools/dart-api-builder/index.js b/tools/dart-api-builder/index.js new file mode 100644 index 0000000000..4d2a0618ef --- /dev/null +++ b/tools/dart-api-builder/index.js @@ -0,0 +1,92 @@ +var fs = require('fs'); +var path = require('canonical-path'); +var Package = require('dgeni').Package; +var basePackage = require('../api-builder/docs-package'); +var targetPackage = require('../api-builder/target-package'); +var cheatsheetPackage = require('../api-builder/cheatsheet-package'); + +var PROJECT_PATH = path.resolve(__dirname, "../.."); +var PUBLIC_PATH = path.resolve(PROJECT_PATH, 'public'); +var DOCS_PATH = path.resolve(PUBLIC_PATH, 'docs'); +var ANGULAR_REPO_PATH = path.resolve(__dirname, '../../../angular-dart'); +var ANGULAR2_DOCS_PATH = path.resolve(ANGULAR_REPO_PATH, 'docs'); +var NG_IO_PKG_PATH = path.resolve(__dirname, "../api-builder/angular.io-package"); + +function requireNgIoPkg(_path) { return require(path.resolve(NG_IO_PKG_PATH, _path)); } + +module.exports = new Package('dart-api-and-cheatsheet-builder', [basePackage, targetPackage, cheatsheetPackage]) + + // overrides base packageInfo and returns the one for the Angular repo. + .factory(require('./services/packageInfo')) + + // Configure rendering + .config(function (templateFinder, renderDocsProcessor) { + + templateFinder.templateFolders + .unshift(path.resolve(NG_IO_PKG_PATH, 'templates')); + + // helpers are made available to the nunjucks templates + renderDocsProcessor.helpers.relativePath = function (from, to) { + return path.relative(from, to); + }; + }) + + .config(function (parseTagsProcessor, getInjectables) { + const tagDefs = requireNgIoPkg('./tag-defs'); + parseTagsProcessor.tagDefinitions = + parseTagsProcessor.tagDefinitions.concat(getInjectables(tagDefs)); + }) + + .config(function (readFilesProcessor) { + // confirm that the angular repo is actually there. + if (!fs.existsSync(ANGULAR_REPO_PATH)) { + throw new Error('dart-api-and-cheatsheet-builder task requires the angular2 repo to be at ' + ANGULAR_REPO_PATH); + } + readFilesProcessor.basePath = DOCS_PATH; + readFilesProcessor.sourceFiles = [{ + basePath: ANGULAR2_DOCS_PATH, + include: path.resolve(ANGULAR2_DOCS_PATH, 'cheatsheet/*.md') + }]; + }) + + .config(function (convertPrivateClassesToInterfacesProcessor, + createOverviewDump, + extractDirectiveClassesProcessor, + extractJSDocCommentsProcessor, + extractTitleFromGuides, + generateNavigationDoc, + mergeDecoratorDocs, + readTypeScriptModules + ) { + // Clear out unwanted processors + createOverviewDump.$enabled = false; + convertPrivateClassesToInterfacesProcessor.$enabled = false; + extractDirectiveClassesProcessor.$enabled = false; + extractJSDocCommentsProcessor.$enabled = false; + extractTitleFromGuides.$enabled = false; + generateNavigationDoc.$enabled = false; + mergeDecoratorDocs.$enabled = false; + readTypeScriptModules.$enabled = false; + }) + + .config(function (computePathsProcessor) { + computePathsProcessor.pathTemplates.push({ + docTypes: ['cheatsheet-data'], + pathTemplate: '../guide/cheatsheet.json', + outputPathTemplate: '${path}' + }); + }) + + .config(function (getLinkInfo) { + getLinkInfo.relativeLinks = true; + }) + + .config(function (templateEngine, getInjectables) { + templateEngine.filters = templateEngine.filters.concat(getInjectables([ + requireNgIoPkg('./rendering/trimBlankLines'), + requireNgIoPkg('./rendering/toId'), + requireNgIoPkg('./rendering/indentForMarkdown') + ])); + }) + + ; diff --git a/tools/dart-api-builder/services/packageInfo.js b/tools/dart-api-builder/services/packageInfo.js new file mode 100644 index 0000000000..53db824168 --- /dev/null +++ b/tools/dart-api-builder/services/packageInfo.js @@ -0,0 +1,38 @@ +'use strict'; + +var fs = require('fs'); +var path = require('canonical-path'); + +/** + * Load information about this project from the pubspec.yaml + * @return {Object} The package information + */ +module.exports = function packageInfo() { + const ngPath = '../angular-dart'; + const angularPubspec = path.join(ngPath, 'pubspec.yaml'); + const pubspec = fs.readFileSync(angularPubspec, 'UTF-8').split('\n'); + + const info = { + version: _get(pubspec, 'version'), + repository: { + type: 'git', //? 'pub' @ 'https://pub.dartlang.org/packages/angular2' + // Not sure `url has a user visible impact on the generated cheatsheet. + url: 'https://github.com/angular/angular.git', + } + }; + return info; +} + +// Array.prototype.find doesn't seem to be working. +// console.error([1, 'a', 2].find((x) => x === 'a')); // --> -1 +function _find(arr, test) { + for (let x of arr) { + // console.error(`Looking at: ${x}`); + if (test(x)) return x; + } +} + +function _get(lines, tag) { + const line = _find(lines, (line) => line.startsWith(tag)); + return line.match(/^\w+: (.*)/)[1] || 'unknown'; +} From b4c92d9c9c596005bdab6fd7a93c0653ef8015c3 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Tue, 19 Jul 2016 15:30:42 -0700 Subject: [PATCH 09/25] docs(toh-6/dart): quickfix to "BAD FILENAME" errors (#1929) Exclude the new Observables section entirely for now. --- public/docs/dart/latest/tutorial/toh-pt6.jade | 3 + public/docs/ts/latest/tutorial/toh-pt6.jade | 269 +++++++++--------- 2 files changed, 138 insertions(+), 134 deletions(-) diff --git a/public/docs/dart/latest/tutorial/toh-pt6.jade b/public/docs/dart/latest/tutorial/toh-pt6.jade index eb24dd0d9d..6536c1a7d3 100644 --- a/public/docs/dart/latest/tutorial/toh-pt6.jade +++ b/public/docs/dart/latest/tutorial/toh-pt6.jade @@ -89,6 +89,9 @@ block heroes-comp-add block review //- Not showing animated gif due to differences between TS and Dart implementations. +block observables-section + //- TBC + block filetree .filetree .file angular2_tour_of_heroes diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index 342e9de7e1..4c6a46a8ec 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -359,147 +359,148 @@ block review figure.image-display img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editting w/ HTTP") -:marked - ## Observables - - Each `Http` method returns an `Observable` of HTTP `Response` objects. - - Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller. - In this section we learn to return the `Observable` directly and discuss when and why that might be - a good thing to do. - - ### Background - An *observable* is a stream of events that we can process with array-like operators. - - Angular core has basic support for observables. We developers augment that support with - operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library. - We'll see how shortly. - - Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`. - That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller. - - Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data. - When we receive the data, we're done. - A single result in the form of a promise is easy for the calling component to consume - and it helps that promises are widely understood by JavaScript programmers. - - But requests aren't always "one and done". We may start one request, - then cancel it, and make a different request ... before the server has responded to the first request. - Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*. - It's easy with *observables* as we'll see. - - ### Search-by-name - We're going to add a *hero search* feature to the Tour of Heroes. - As the user types a name into a search box, we'll make repeated http requests for heroes filtered by that name. - - We start by creating `HeroSearchService` that sends search queries to our server's web api. - -+makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".") - -:marked - The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`. - The notable difference: we no longer call `toPromise`. - We simply return the *observable* instead. - - ### HeroSearchComponent - Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`. - - The component template is simple - just a textbox and a list of matching search results. -+makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html') -:marked - As the user types in the search box, a *keyup* event binding calls the component's `search` with the new search box value. - - The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. - - But, as we'll soon see, the `heroes` property returns an `Observable` of heroes, not an array of heroes. - The `*ngFor` can't do anything with an observable until we flow it through the `AsyncPipe` (`heroes | async`). - The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`. - - Time to create the `HeroSearchComponent` class and metadata. -+makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts') -:marked - Focus on the `searchSubject`. -+makeExample('toh-6/ts/app/hero-search.component.ts', 'searchSubject')(format=".") -:marked - A `Subject` is a producer of an _observable_ event stream. - This `searchSubject` produces an `Observable` of strings, the filter criteria for the name search. - - Each call to `search` puts a new string into this subject's _observable_ stream by calling `next`. - - A `Subject` is also an `Observable`. - We're going to access that `Observable` and turn the stream - of strings into a stream of `Hero[]` arrays, the `heroes` property. - -+makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") -:marked - If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of http requests. - Bad idea. We don't want to tax our server resources and burn through our cellular network data plan. - - Fortunately we can chain `Observable` operators to the string `Observable` that reduce the request flow. - We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how: - - * The `asObservable` operator casts the `Subject` as an `Observable` of filter strings. - - * `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds - before passing along the latest string. We'll never make requests more frequently than 300ms. - - * `distinctUntilChanged` ensures that we only send a request if the filter text changed. - There's no point in repeating a request for the same search term. - - * `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet. - It cancels and discards previous search observables, returning only the latest search service observable. - -.l-sub-section +block observables-section :marked - The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) - (formerly known as "flatMapLatest") is very clever. - - Every qualifying key event can trigger an http call. - Even with a 300ms pause between requests, we could have multiple http requests in flight - and they may not return in the order sent. - - `switchMap` preserves the original request order while returning - only the observable from the most recent http call. - Results from prior calls are canceled and discarded. - - We also short-circuit the http call and return an observable containing an empty array - if the search text is empty. + ## Observables - Note that _canceling_ the `HeroSearchService` observable won't actually abort a pending http request - until the service supports that feature, a topic for another day. - We are content for now to discard unwanted results. -:marked - * `catch` intercepts a failed observable. - Our simple example prints the error to the console; a real life application should do better. - Then it re-throws the failed observable so that downstream processes know it failed. - The `AsyncPipe` in the template is downstream. It sees the failure and ignores it. + Each `Http` method returns an `Observable` of HTTP `Response` objects. - ### Import RxJS operators - The RxJS operators are not available in Angular's base `Observable` implementation. - We have to extend `Observable` by *importing* them. - - We could extend `Observable` with just the operators we need here by - including the pertinent `import` statements at the top of this file. + Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller. + In this section we learn to return the `Observable` directly and discuss when and why that might be + a good thing to do. + + ### Background + An *observable* is a stream of events that we can process with array-like operators. + + Angular core has basic support for observables. We developers augment that support with + operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library. + We'll see how shortly. + + Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`. + That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller. + + Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data. + When we receive the data, we're done. + A single result in the form of a promise is easy for the calling component to consume + and it helps that promises are widely understood by JavaScript programmers. + + But requests aren't always "one and done". We may start one request, + then cancel it, and make a different request ... before the server has responded to the first request. + Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*. + It's easy with *observables* as we'll see. + + ### Search-by-name + We're going to add a *hero search* feature to the Tour of Heroes. + As the user types a name into a search box, we'll make repeated http requests for heroes filtered by that name. + + We start by creating `HeroSearchService` that sends search queries to our server's web api. + + +makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".") -.l-sub-section :marked - Many authorities say we should do just that. -:marked - We take a different approach in this example. - We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file. + The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`. + The notable difference: we no longer call `toPromise`. + We simply return the *observable* instead. -+makeExample('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".") -:marked - We load them all at once by importing `rxjs-extensions` in `AppComponent`. + ### HeroSearchComponent + Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`. -+makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app/app.component.ts')(format=".") -:marked - Finally, we add the `HeroSearchComponent` to the bottom of the `DashboardComponent`. - Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles. - At some point it might look like this. + The component template is simple - just a textbox and a list of matching search results. + +makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html') + :marked + As the user types in the search box, a *keyup* event binding calls the component's `search` with the new search box value. -figure.image-display - img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") + The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. + + But, as we'll soon see, the `heroes` property returns an `Observable` of heroes, not an array of heroes. + The `*ngFor` can't do anything with an observable until we flow it through the `AsyncPipe` (`heroes | async`). + The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`. + + Time to create the `HeroSearchComponent` class and metadata. + +makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts') + :marked + Focus on the `searchSubject`. + +makeExample('toh-6/ts/app/hero-search.component.ts', 'searchSubject')(format=".") + :marked + A `Subject` is a producer of an _observable_ event stream. + This `searchSubject` produces an `Observable` of strings, the filter criteria for the name search. + + Each call to `search` puts a new string into this subject's _observable_ stream by calling `next`. + + A `Subject` is also an `Observable`. + We're going to access that `Observable` and turn the stream + of strings into a stream of `Hero[]` arrays, the `heroes` property. + + +makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") + :marked + If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of http requests. + Bad idea. We don't want to tax our server resources and burn through our cellular network data plan. + + Fortunately we can chain `Observable` operators to the string `Observable` that reduce the request flow. + We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how: + + * The `asObservable` operator casts the `Subject` as an `Observable` of filter strings. + + * `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds + before passing along the latest string. We'll never make requests more frequently than 300ms. + + * `distinctUntilChanged` ensures that we only send a request if the filter text changed. + There's no point in repeating a request for the same search term. + + * `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet. + It cancels and discards previous search observables, returning only the latest search service observable. + + .l-sub-section + :marked + The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) + (formerly known as "flatMapLatest") is very clever. + + Every qualifying key event can trigger an http call. + Even with a 300ms pause between requests, we could have multiple http requests in flight + and they may not return in the order sent. + + `switchMap` preserves the original request order while returning + only the observable from the most recent http call. + Results from prior calls are canceled and discarded. + + We also short-circuit the http call and return an observable containing an empty array + if the search text is empty. + + Note that _canceling_ the `HeroSearchService` observable won't actually abort a pending http request + until the service supports that feature, a topic for another day. + We are content for now to discard unwanted results. + :marked + * `catch` intercepts a failed observable. + Our simple example prints the error to the console; a real life application should do better. + Then it re-throws the failed observable so that downstream processes know it failed. + The `AsyncPipe` in the template is downstream. It sees the failure and ignores it. + + ### Import RxJS operators + The RxJS operators are not available in Angular's base `Observable` implementation. + We have to extend `Observable` by *importing* them. + + We could extend `Observable` with just the operators we need here by + including the pertinent `import` statements at the top of this file. + + .l-sub-section + :marked + Many authorities say we should do just that. + :marked + We take a different approach in this example. + We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file. + + +makeExample('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".") + :marked + We load them all at once by importing `rxjs-extensions` in `AppComponent`. + + +makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app/app.component.ts')(format=".") + :marked + Finally, we add the `HeroSearchComponent` to the bottom of the `DashboardComponent`. + Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles. + At some point it might look like this. + + figure.image-display + img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") .l-main-section :marked @@ -554,7 +555,7 @@ block filetree - We extended HeroService to support post, put and delete calls. - We updated our components to allow adding, editing and deleting of heroes. - We configured an in-memory web API. - - We learned how to use Observables. +
  • We learned how to use Observables.
  • Below is a summary of the files we changed and added. From 0387b810c64c60b8c934042cfe28a6dccc7c8772 Mon Sep 17 00:00:00 2001 From: Torgeir Helgevold Date: Tue, 19 Jul 2016 19:15:23 -0400 Subject: [PATCH 10/25] docs(toh-6): remove throw closes #1930 --- public/docs/_examples/toh-6/ts/app/hero-search.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts index a69db4491d..9ef06ca47c 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts @@ -15,7 +15,7 @@ import { Hero } from './hero'; }) export class HeroSearchComponent implements OnInit { // #docregion search - heroes: Observable; + heroes: Observable; // #enddocregion search // #docregion searchSubject searchSubject = new Subject(); @@ -45,7 +45,7 @@ export class HeroSearchComponent implements OnInit { .catch(error => { // Todo: real error handling console.log(error); - return Observable.throw(error); + return Observable.of([]); }); } // #enddocregion search From f00b96fab8b04500cc49a2d1591df28578c7a7c8 Mon Sep 17 00:00:00 2001 From: Torgeir Helgevold Date: Tue, 19 Jul 2016 22:48:55 -0400 Subject: [PATCH 11/25] docs(toh-6): remove details about throw --- public/docs/ts/latest/tutorial/toh-pt6.jade | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index 4c6a46a8ec..6e3fa96c65 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -472,8 +472,7 @@ block observables-section :marked * `catch` intercepts a failed observable. Our simple example prints the error to the console; a real life application should do better. - Then it re-throws the failed observable so that downstream processes know it failed. - The `AsyncPipe` in the template is downstream. It sees the failure and ignores it. + Then we return an observable containing an empty array to clear the search result. ### Import RxJS operators The RxJS operators are not available in Angular's base `Observable` implementation. From 13aa6b14982c4fca30a54a6e9fdc060fcf473e26 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Mon, 18 Jul 2016 15:59:42 -0700 Subject: [PATCH 12/25] chore(travis): include dart doc generation closes #1910 - Installs Dart SDK - Installs ng2/dart sources from the pub package site. - Re-enables doc build on Travis - install Dartium browser for eventual testing. - allow TMP and PKG to be set by developer before calling, e.g., install-dart-sdk.sh. - removes `npm install -g gulp --no-optional` has been removed since it does not appear to be needed. - Implements most of #1907. --- .travis.yml | 17 +++++---- gulpfile.js | 9 ++--- pubspec.yaml | 17 +++++++++ scripts/before-install.sh | 14 ++++++++ scripts/deploy-install.sh | 26 ++++++++++++-- scripts/env-info-and-check.sh | 47 ++++++++++++++++++++++++ scripts/env-set.sh | 39 ++++++++++++++++++++ scripts/install-dart-sdk.sh | 67 +++++++++++++++++++++++++++++++++++ scripts/install-ng2dart.sh | 29 +++++++++++++++ scripts/install.sh | 2 +- 10 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 pubspec.yaml create mode 100755 scripts/before-install.sh create mode 100755 scripts/env-info-and-check.sh create mode 100644 scripts/env-set.sh create mode 100755 scripts/install-dart-sdk.sh create mode 100755 scripts/install-ng2dart.sh diff --git a/.travis.yml b/.travis.yml index ef7145f388..755b29e6a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,23 +11,26 @@ env: - DISPLAY=:99.0 - CHROME_BIN=chromium-browser - LATEST_RELEASE=2.0.0-rc.4 + - TASK_FLAGS="--dgeni-log=warn" + # - TASK_FLAGS="" matrix: - TASK=lint - TASK="run-e2e-tests --fast" SCRIPT=examples-install.sh - TASK="run-e2e-tests --fast" SCRIPT=examples-install-preview.sh - - TASK=build-compile SCRIPT=deploy-install.sh - - TASK=build-compile SCRIPT=deploy-install-preview.sh + - TASK=build-compile SCRIPT=deploy-install.sh WAIT="travis_wait 50" + - TASK=build-compile SCRIPT=deploy-install-preview.sh WAIT="travis_wait 50" matrix: fast_finish: true allow_failures: - - env: "TASK=\"run-e2e-tests --fast\" SCRIPT=examples-install-preview.sh" - - env: "TASK=build-compile SCRIPT=deploy-install-preview.sh" + - env: TASK="run-e2e-tests --fast" SCRIPT=examples-install-preview.sh + - env: TASK=build-compile SCRIPT=deploy-install-preview.sh WAIT="travis_wait 50" before_install: - - npm install -g gulp --no-optional + - source ./scripts/env-set.sh + - ./scripts/before-install.sh install: - npm install --no-optional - - if [[ $SCRIPT ]]; then ./scripts/$SCRIPT; fi + - if [[ -n "$SCRIPT" ]]; then echo "EXTRA INSTALL $SCRIPT"; ./scripts/$SCRIPT; fi before_script: - sh -e /etc/init.d/xvfb start script: - - gulp $TASK + - $WAIT gulp $TASK $TASK_FLAGS diff --git a/gulpfile.js b/gulpfile.js index 3b0743ac34..b7be783ea8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -116,13 +116,14 @@ var _exampleConfigFilename = 'example-config.json'; // var lang, langs, buildDartApiDocs = false; function configLangs(langOption) { - // TODO(chalin): temporary dependence on process.env.TRAVIS until #1910 lands. - const buildAllDocs = argv['_'] && argv['_'].indexOf('check-deploy' ) >= 0; - const langDefault = (!buildAllDocs || process.env.TRAVIS) ? '(ts|js)' : 'all'; + const fullSiteBuildTasks = ['build-compile', 'check-serve', 'check-deploy']; + const buildAllDocs = argv['_'] && + fullSiteBuildTasks.some((task) => argv['_'].indexOf(task) >= 0); + const langDefault = buildAllDocs ? 'all' : '(ts|js)'; lang = (langOption || langDefault).toLowerCase(); if (lang === 'all') lang = '(ts|js|dart)'; langs = lang.match(/\w+/g); // the languages in `lang` as an array - gutil.log('Build docs for: ' + lang); + gutil.log('Building docs for: ' + lang); if (langs.indexOf('dart') >= 0) { buildDartApiDocs = true; // For Dart, be proactive about checking for the repo diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000000..8f82826558 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,17 @@ +name: angular2_io +description: Angular 2 for Dart Website +version: 0.0.1 +environment: + sdk: '>=1.13.0 <2.0.0' +dependencies: + angular2: 2.0.0-beta.17 + browser: ^0.10.0 + dart_to_js_script_rewriter: ^1.0.1 +transformers: +- angular2: + platform_directives: + - 'package:angular2/common.dart#COMMON_DIRECTIVES' + platform_pipes: + - 'package:angular2/common.dart#COMMON_PIPES' + entry_points: web/main.dart +- dart_to_js_script_rewriter diff --git a/scripts/before-install.sh b/scripts/before-install.sh new file mode 100755 index 0000000000..b99455e910 --- /dev/null +++ b/scripts/before-install.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +./scripts/env-info-and-check.sh + +if [[ 0 ]]; then + # Doesn't seem to be necessary. Disabling. + travis_fold start install.globals + set -x + npm install -g gulp --no-optional + set +x + travis_fold end install.globals +fi diff --git a/scripts/deploy-install.sh b/scripts/deploy-install.sh index 38cf6c7d19..f34a158fbf 100755 --- a/scripts/deploy-install.sh +++ b/scripts/deploy-install.sh @@ -1,5 +1,27 @@ #!/usr/bin/env bash -set -ex -o pipefail +set -e -o pipefail -(cd ../ && git clone https://github.com/angular/angular.git --branch $LATEST_RELEASE) \ No newline at end of file +[[ -z "$NGIO_ENV_DEFS" ]] && . ./scripts/env-set.sh + +if [[ -e "$NG2_REPO" ]]; then + echo Angular2 repo is already present at: $NG2_REPO +else + travis_fold start install.ng2 + echo GETTING Angular2 from GitHub ... + set -x + git clone https://github.com/angular/angular.git --branch $LATEST_RELEASE $NG2_REPO + set +x + travis_fold end install.ng2 +fi + +if [[ -e "$NG2DART_REPO" ]]; then + echo Angular2 Dart repo is already present at: $NG2DART_REPO +elif [[ -n "$TRAVIS" ]]; then + ./scripts/install-ng2dart.sh +# else +# echo WARNING: no Angular2 Dart repo found at: $NG2DART_REPO +fi + +echo INSTALLED repos: +ls -ld ../a* diff --git a/scripts/env-info-and-check.sh b/scripts/env-info-and-check.sh new file mode 100755 index 0000000000..c919602f10 --- /dev/null +++ b/scripts/env-info-and-check.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +travis_fold start env_info +echo ENVIRONMENT INFO +travis_fold start env_info.path +echo Path: +echo $PATH | tr : '\n' +echo +travis_fold end env_info.path +travis_fold start env_info.home +echo Home: $HOME +ls ~ -la +echo +travis_fold end env_info.home +travis_fold start env_info.pwd +echo Pwd: `pwd` +ls -la +echo +travis_fold end env_info.pwd +if [[ 0 ]]; then + # Not needed anymore, but keeping it at least for the first commit for archival purposes. + travis_fold start env_info.bash_profile + echo Bash profile ------------------------------------------------------------ + cat ~/.bash_profile + travis_fold end env_info.bash_profile + travis_fold start env_info.bashrc + echo Bashrc ------------------------------------------------------------------ + cat ~/.bashrc + echo ------------------------------------------------------------------------- + travis_fold end env_info.bashrc + travis_fold start env_info.build + echo build.sh ---------------------------------------------------------------- + cat ~/build.sh + echo ------------------------------------------------------------------------- + travis_fold end env_info.build +fi +travis_fold end env_info + +echo ENVIRONMENT CONFIG CHECK: +if [[ -z "$NGIO_ENV_DEFS" ]]; then + echo Environment variables are not being set. Aborting. + exit 1; +else + echo Environment variables successfully set. +fi diff --git a/scripts/env-set.sh b/scripts/env-set.sh new file mode 100644 index 0000000000..8341107a8c --- /dev/null +++ b/scripts/env-set.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +if [[ -z "$NGIO_ENV_DEFS" ]]; then + export ANSI_YELLOW="\033[33;1m" + export ANSI_RESET="\033[0m" + echo -e "${ANSI_YELLOW}Setting environment variables from scripts/env.sh${ANSI_RESET}" + + export NGIO_ENV_DEFS=1 + + export NG2_REPO=../angular + export NG2DART_REPO=$NG2_REPO-dart + + if [ ! $(type -t travis_fold) ]; then + # In case this is being run locally. Turn travis_fold into a noop. + travis_fold () { return; } + # Alternative definition: + # travis_fold () { echo -en "travis_fold:${1}:${2}"; } + fi + export -f travis_fold + + case "$(uname -a)" in + Darwin\ *) _OS_NAME=macos ;; + Linux\ *) _OS_NAME=linux ;; + *) _OS_NAME=linux ;; + esac + export _OS_NAME + + : ${TMP:=$HOME/tmp} + : ${PKG:=$TMP/pkg} + export TMP + export PKG + + if [[ -z "$(type -t dart)" && ! $PATH =~ */dart-sdk/* ]]; then + export DART_SDK="$PKG/dart-sdk" + # echo Updating PATH to include access to Dart bin. + export PATH="$DART_SDK/bin:$PATH" + export PATH="$HOME/.pub-cache/bin:$PATH" + fi +fi diff --git a/scripts/install-dart-sdk.sh b/scripts/install-dart-sdk.sh new file mode 100755 index 0000000000..688bc8b9da --- /dev/null +++ b/scripts/install-dart-sdk.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +[[ -z "$NGIO_ENV_DEFS" ]] && . ./scripts/env-set.sh + +if [[ -z "$(type -t dart)" ]]; then + travis_fold start install.dart + echo INSTALLING Dart SDK and Dartium ... + + # URLs for sdk and dartium: + # https://storage.googleapis.com/dart-archive/channels/stable/release/latest/sdk/dartsdk-linux-x64-release.zip + # https://storage.googleapis.com/dart-archive/channels/stable/release/latest/dartium/dartium-macos-x64-release.zip + + DART_ARCHIVE=https://storage.googleapis.com/dart-archive/channels + VERS=stable/release/latest + + mkUrl() { + local dir=$1 + local pkg=$2 + local arch=$3 + local zip=$pkg-$_OS_NAME-$arch-release.zip + echo "$DART_ARCHIVE/$VERS/$dir/$zip"; + } + + getAndInstall() { + local dir=$1 + local pkg=${2:-$dir}; + local arch=${3:-x64} + local URL=$(mkUrl $dir $pkg $arch) + local exitStatus=0; + local zip=$(basename $URL) + + echo "Getting $pkg from:" + echo " $URL" + + [[ ! -d "$TMP" ]] && mkdir "$TMP" + [[ ! -d "$PKG" ]] && mkdir "$PKG" + + curl $URL > "$TMP/$zip" # 2> /dev/null + + if [[ "1000" -lt "$(wc -c $TMP/$zip | awk '{print $1}')" ]]; then + unzip "$TMP/$zip" -d "$PKG" > /dev/null + rm -f "$TMP/$zip" + # PATH is set in ./scripts/env-set.sh + else + echo FAILED to download Dart $pkg. Check URL. + exitStatus=1; + fi + } + + if getAndInstall sdk dartsdk; then + # Install Dartium + if [[ "$_OS_NAME" == "macos" ]]; then + getAndInstall dartium dartium ia32 + else + getAndInstall dartium + fi + echo + dart --version + fi + travis_fold end install.dart +else + echo Dart SDK appears to be installed: `type dart` + # PATH is set in ./scripts/env-set.sh + dart --version +fi diff --git a/scripts/install-ng2dart.sh b/scripts/install-ng2dart.sh new file mode 100755 index 0000000000..a0e0283073 --- /dev/null +++ b/scripts/install-ng2dart.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +[[ -z "$NGIO_ENV_DEFS" ]] && . ./scripts/env-set.sh + +./scripts/install-dart-sdk.sh + +travis_fold start install.ng2dart +if [[ -z "$(type -t dart)" ]]; then + echo "No Dart SDK: aborting install of Angular2/Dart" + exit 1; +elif [[ -e "$NG2DART_REPO" ]]; then + echo Angular2/Dart found at: $NG2DART_REPO +else + echo GETTING Angular2/Dart from pub package ... + set -x + # Get ng2dart via pub on ng.io pubspec.yaml + pub upgrade > /dev/null + + NG2DART_PUB=`find ~/.pub-cache/ -type d -name "angular2*" | xargs ls -dtr | tail -1` + + cp -r $NG2DART_PUB $NG2DART_REPO +fi + +# Run pub on ng2dart +(cd $NG2DART_REPO && pub get) +set +x +travis_fold end install.ng2dart diff --git a/scripts/install.sh b/scripts/install.sh index cc95b6726b..79242a0d16 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -19,7 +19,7 @@ npm install --no-optional echo "Patching ..." source ./scripts/patch.sh -if [ "$TRAVIS" != "true" ]; then +if [ -z "$TRAVIS" ]; then echo "Rebuilding node-sass, just in case ..." npm rebuild node-sass; fi From 2e3f3e7fb12bd4516cef9a9bcf7ec89ecb23cb0a Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Tue, 19 Jul 2016 21:59:34 -0500 Subject: [PATCH 13/25] docs(router): Minor dev guide fixes closes #1932 --- public/docs/_examples/router/ts/app/app.component.3.ts | 2 +- public/docs/_examples/router/ts/app/app.component.ts | 2 +- public/docs/ts/latest/guide/router.jade | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/docs/_examples/router/ts/app/app.component.3.ts b/public/docs/_examples/router/ts/app/app.component.3.ts index 66d485d339..d8bb30b1e2 100644 --- a/public/docs/_examples/router/ts/app/app.component.3.ts +++ b/public/docs/_examples/router/ts/app/app.component.3.ts @@ -28,7 +28,7 @@ import { HeroService } from './heroes/hero.service'; */ /* Crisis Center Detail link // #docregion Dragon-anchor -
    Dragon Crisis + Dragon Crisis // #enddocregion Dragon-anchor */ /* Crisis Center link with optional query params diff --git a/public/docs/_examples/router/ts/app/app.component.ts b/public/docs/_examples/router/ts/app/app.component.ts index 84dd5ea5aa..a3ad280a45 100644 --- a/public/docs/_examples/router/ts/app/app.component.ts +++ b/public/docs/_examples/router/ts/app/app.component.ts @@ -13,7 +13,7 @@ import { HeroService } from './heroes/hero.service';

    Component Router