From a35935c11989e60d6bece0c3e30222b8fd17d529 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Tue, 29 Nov 2016 21:58:44 -0600 Subject: [PATCH 01/13] docs(rxjs): Added developer guide on Observables --- public/docs/_examples/rxjs/e2e-spec.ts | 12 ++ .../rxjs/ts/app/add-hero.component.html | 15 ++ .../rxjs/ts/app/add-hero.component.ts | 74 +++++++ .../rxjs/ts/app/api-error-handler.service.ts | 34 ++++ .../rxjs/ts/app/app-routing.module.ts | 22 +++ .../_examples/rxjs/ts/app/app.component.ts | 20 ++ .../docs/_examples/rxjs/ts/app/app.module.ts | 48 +++++ .../rxjs/ts/app/event-aggregator.service.ts | 27 +++ .../rxjs/ts/app/hero-detail.component.1.ts | 56 ++++++ .../rxjs/ts/app/hero-detail.component.ts | 55 ++++++ .../rxjs/ts/app/hero-list.component.1.ts | 30 +++ .../rxjs/ts/app/hero-list.component.ts | 30 +++ .../rxjs/ts/app/hero-search.component.css | 16 ++ .../rxjs/ts/app/hero-search.component.html | 12 ++ .../rxjs/ts/app/hero-search.component.ts | 67 +++++++ .../_examples/rxjs/ts/app/hero.service.ts | 44 +++++ public/docs/_examples/rxjs/ts/app/hero.ts | 4 + .../rxjs/ts/app/in-memory-data.service.ts | 19 ++ .../rxjs/ts/app/loading.component.css | 8 + .../rxjs/ts/app/loading.component.ts | 28 +++ .../_examples/rxjs/ts/app/loading.service.ts | 22 +++ public/docs/_examples/rxjs/ts/app/main.ts | 6 + .../_examples/rxjs/ts/example-config.json | 0 public/docs/_examples/rxjs/ts/index.html | 31 +++ public/docs/_examples/rxjs/ts/plnkr.json | 9 + public/docs/ts/latest/guide/rxjs.jade | 182 ++++++++++++++++++ 26 files changed, 871 insertions(+) create mode 100644 public/docs/_examples/rxjs/e2e-spec.ts create mode 100644 public/docs/_examples/rxjs/ts/app/add-hero.component.html create mode 100644 public/docs/_examples/rxjs/ts/app/add-hero.component.ts create mode 100644 public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts create mode 100644 public/docs/_examples/rxjs/ts/app/app-routing.module.ts create mode 100644 public/docs/_examples/rxjs/ts/app/app.component.ts create mode 100644 public/docs/_examples/rxjs/ts/app/app.module.ts create mode 100644 public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero-detail.component.1.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero-detail.component.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero-list.component.1.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero-list.component.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero-search.component.css create mode 100644 public/docs/_examples/rxjs/ts/app/hero-search.component.html create mode 100644 public/docs/_examples/rxjs/ts/app/hero-search.component.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero.service.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero.ts create mode 100644 public/docs/_examples/rxjs/ts/app/in-memory-data.service.ts create mode 100644 public/docs/_examples/rxjs/ts/app/loading.component.css create mode 100644 public/docs/_examples/rxjs/ts/app/loading.component.ts create mode 100644 public/docs/_examples/rxjs/ts/app/loading.service.ts create mode 100644 public/docs/_examples/rxjs/ts/app/main.ts create mode 100644 public/docs/_examples/rxjs/ts/example-config.json create mode 100644 public/docs/_examples/rxjs/ts/index.html create mode 100644 public/docs/_examples/rxjs/ts/plnkr.json create mode 100644 public/docs/ts/latest/guide/rxjs.jade diff --git a/public/docs/_examples/rxjs/e2e-spec.ts b/public/docs/_examples/rxjs/e2e-spec.ts new file mode 100644 index 0000000000..adb6e5e9be --- /dev/null +++ b/public/docs/_examples/rxjs/e2e-spec.ts @@ -0,0 +1,12 @@ +'use strict'; // necessary for es6 output in node + +import { browser +/*, element, by, ElementFinder*/ +} from 'protractor'; + +describe('RxJS', function () { + + beforeAll(function () { + browser.get(''); + }); +}); diff --git a/public/docs/_examples/rxjs/ts/app/add-hero.component.html b/public/docs/_examples/rxjs/ts/app/add-hero.component.html new file mode 100644 index 0000000000..f57c7976cd --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/add-hero.component.html @@ -0,0 +1,15 @@ +

ADD HERO

+
+

+ *Name:
+ Name is required +

+

+ Description: +

+

+ +

+
+ +The hero has been added diff --git a/public/docs/_examples/rxjs/ts/app/add-hero.component.ts b/public/docs/_examples/rxjs/ts/app/add-hero.component.ts new file mode 100644 index 0000000000..145dd0ff68 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/add-hero.component.ts @@ -0,0 +1,74 @@ +// #docplaster +// #docregion +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/do'; +import { Component, OnInit, OnDestroy, AfterViewInit, ViewChildren, ElementRef } from '@angular/core'; +import { FormBuilder, FormGroup, FormControlName, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; + +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + templateUrl: 'add-hero.component.html', + styles: [ '.error { color: red }' ] +}) +export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChildren(FormControlName, { read: ElementRef }) formControls: ElementRef[]; + + form: FormGroup; + sub: Subscription; + showErrors: boolean = false; + submitted: boolean = false; + success: boolean; + + constructor( + private formBuilder: FormBuilder, + private heroService: HeroService + ) {} + + ngOnInit() { + this.form = this.formBuilder.group({ + name: ['', [Validators.required]], + description: [''] + }); + } + + ngAfterViewInit() { + const controlBlurs: Observable[] = this.formControls.map(field => Observable.fromEvent(field.nativeElement, 'blur')); + + this.sub = Observable.merge( + this.form.valueChanges, + ...controlBlurs + ) + .debounceTime(300) + .subscribe(() => this.checkErrors()); + } + + checkErrors() { + if (!this.form.valid) { + this.showErrors = true; + } + } + + save(model: any) { + this.success = false; + this.submitted = true; + + this.heroService.addHero(model) + .do(() => { + this.success = true; + this.submitted = false; + }) + .subscribe(); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts b/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts new file mode 100644 index 0000000000..bae30435ea --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +export interface ApiError { + message: string; +} + +@Injectable() +export class ApiErrorHandlerService { + handle(resp: Response): Observable { + return Observable.of(resp) + .switchMap(response => { + + let error: ApiError; + + try { + error = response.json().error; + } catch (e) { + if (response.status === 404) { + error = { + message: 'The requested resource was not found' + }; + } else { + error = { + message: 'An unknown error has occurred' + }; + } + } + + return Observable.throw(error); + }); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/app-routing.module.ts b/public/docs/_examples/rxjs/ts/app/app-routing.module.ts new file mode 100644 index 0000000000..17b338383e --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/app-routing.module.ts @@ -0,0 +1,22 @@ +// #docplaster +// #docregion +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AddHeroComponent } from './add-hero.component'; +import { HeroDetailComponent } from './hero-detail.component'; +import { HeroSearchComponent } from './hero-search.component'; +import { HeroListComponent } from './hero-list.component'; + +const appRoutes: Routes = [ + { path: 'heroes/add', component: AddHeroComponent }, + { path: 'heroes/search', component: HeroSearchComponent }, + { path: 'heroes', component: HeroListComponent }, + { path: 'hero/:id', component: HeroDetailComponent }, + { path: '', redirectTo: '/heroes', pathMatch: 'full' }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(appRoutes)], + exports: [RouterModule] +}) +export class AppRoutingModule {} diff --git a/public/docs/_examples/rxjs/ts/app/app.component.ts b/public/docs/_examples/rxjs/ts/app/app.component.ts new file mode 100644 index 0000000000..435bd816fa --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/app.component.ts @@ -0,0 +1,20 @@ +// #docplaster +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-app', + template: ` +

RxJS in Angular

+ + Heroes
+ Add Hero
+ Hero Search + + + + + ` +}) +export class AppComponent { +} diff --git a/public/docs/_examples/rxjs/ts/app/app.module.ts b/public/docs/_examples/rxjs/ts/app/app.module.ts new file mode 100644 index 0000000000..67871109ef --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/app.module.ts @@ -0,0 +1,48 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpModule } from '@angular/http'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; +import { AddHeroComponent } from './add-hero.component'; +import { LoadingComponent } from './loading.component'; +import { HeroSearchComponent } from './hero-search.component'; +import { HeroDetailComponent } from './hero-detail.component'; +import { HeroListComponent } from './hero-list.component'; + +import { LoadingService } from './loading.service'; +import { HeroService } from './hero.service'; + +// Imports for loading & configuring the in-memory web api +import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; +import { ApiErrorHandlerService } from './api-error-handler.service'; + +@NgModule({ + imports: [ + BrowserModule, + HttpModule, + AppRoutingModule, + ReactiveFormsModule, + InMemoryWebApiModule.forRoot(InMemoryDataService) + ], + declarations: [ + AppComponent, + AddHeroComponent, + LoadingComponent, + HeroSearchComponent, + HeroDetailComponent, + HeroListComponent + ], + providers: [ + HeroService, + LoadingService, + ApiErrorHandlerService + ], + bootstrap: [ AppComponent ] +}) +export class AppModule { +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts b/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts new file mode 100644 index 0000000000..d32b776bd6 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +export interface AppEvent { + type: string; + message: string; +} + +@Injectable() +export class EventAggregatorService { + _events: AppEvent[]; + events$: BehaviorSubject = new BehaviorSubject([]); + + add(event: AppEvent) { + this._events.push(event); + this.next(); + } + + clear() { + this._events = []; + this.next(); + } + + next() { + this.events$.next(this._events); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/hero-detail.component.1.ts b/public/docs/_examples/rxjs/ts/app/hero-detail.component.1.ts new file mode 100644 index 0000000000..b29aec89ad --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-detail.component.1.ts @@ -0,0 +1,56 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/switchMap'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { HeroService } from './hero.service'; +import { Hero } from './hero'; +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +@Component({ + template: ` +
+ Loading Hero... +
+
+

HEROES

+
+ {{ (hero$ | async)?.id }} +
+
+ + +
+
+
+ No hero found +
+ ` +}) +export class HeroDetailComponent implements OnInit { + hero$: BehaviorSubject = new BehaviorSubject(null); + loading: boolean = true; + error: boolean; + + constructor( + private heroService: HeroService, + private route: ActivatedRoute + ) {} + + ngOnInit() { + this.route.params + .do(() => this.loading = true) + .switchMap((params: Params) => + this.heroService.getHero(params['id']) + .catch(error => { + this.error = true; + + return Observable.of(null); + }) + ) + .do(() => this.loading = false) + .subscribe(this.hero$); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/hero-detail.component.ts b/public/docs/_examples/rxjs/ts/app/hero-detail.component.ts new file mode 100644 index 0000000000..54a6d17df4 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-detail.component.ts @@ -0,0 +1,55 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/switchMap'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { HeroService } from './hero.service'; +import { Hero } from './hero'; +import { Observable } from 'rxjs/Observable'; + +@Component({ + template: ` +
+ Loading Hero... +
+
+

HEROES

+
+ {{ hero.id }} +
+
+ + +
+
+
+ No hero found +
+ ` +}) +export class HeroDetailComponent implements OnInit { + hero: Hero; + loading: boolean = true; + error: boolean; + + constructor( + private heroService: HeroService, + private route: ActivatedRoute + ) {} + + ngOnInit() { + this.route.params + .do(() => this.loading = true) + .switchMap((params: Params) => + this.heroService.getHero(params['id']) + .catch(error => { + this.error = true; + + return Observable.of(null); + }) + ) + .do(() => this.loading = false) + .subscribe((hero: Hero) => this.hero = hero); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/hero-list.component.1.ts b/public/docs/_examples/rxjs/ts/app/hero-list.component.1.ts new file mode 100644 index 0000000000..967b98a8e4 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-list.component.1.ts @@ -0,0 +1,30 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes: Hero[]; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + this.service.getHeroes() + .subscribe(heroes => this.heroes = heroes); + } +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/app/hero-list.component.ts b/public/docs/_examples/rxjs/ts/app/hero-list.component.ts new file mode 100644 index 0000000000..7d3740c5bb --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-list.component.ts @@ -0,0 +1,30 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes$: Observable; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + this.heroes$ = this.service.getHeroes(); + } +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/app/hero-search.component.css b/public/docs/_examples/rxjs/ts/app/hero-search.component.css new file mode 100644 index 0000000000..51e3ca4370 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-search.component.css @@ -0,0 +1,16 @@ +/* #docregion */ +.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/_examples/rxjs/ts/app/hero-search.component.html b/public/docs/_examples/rxjs/ts/app/hero-search.component.html new file mode 100644 index 0000000000..66a0c5d1b9 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-search.component.html @@ -0,0 +1,12 @@ + +
+

HERO SEARCH

+
+ +
+
+ +
+
diff --git a/public/docs/_examples/rxjs/ts/app/hero-search.component.ts b/public/docs/_examples/rxjs/ts/app/hero-search.component.ts new file mode 100644 index 0000000000..024717952e --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-search.component.ts @@ -0,0 +1,67 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/observable/merge'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Router, ActivatedRoute, Params } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + moduleId: module.id, + templateUrl: 'hero-search.component.html', + styleUrls: [ 'hero-search.component.css' ] +}) +export class HeroSearchComponent implements OnInit, OnDestroy { + heroes$: Observable; + destroy$: Subject = new Subject(); + form: FormGroup; + + constructor( + private heroService: HeroService, + private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit(): void { + this.form = this.formBuilder.group({ + searchTerms: [''] + }); + + const searchTerms$: Observable = this.form.valueChanges + .debounceTime(300) + .map(model => model.searchTerms); + + const querySearch$: Observable = this.route.queryParams + .map((params: Params) => params['q']) + .do(searchTerms => this.form.patchValue({ + searchTerms + })); + + this.heroes$ = Observable.merge(searchTerms$, querySearch$) + .distinctUntilChanged() + .takeUntil(this.destroy$) + .do(q => this.router.navigate(['./'], { queryParams: { q }, relativeTo: this.route })) + .switchMap((term: string) => term + ? this.heroService.search(term) + : Observable.of([]) + ) + .catch(error => { + return Observable.of([]); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/hero.service.ts b/public/docs/_examples/rxjs/ts/app/hero.service.ts new file mode 100644 index 0000000000..24f3cefde1 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero.service.ts @@ -0,0 +1,44 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/delay'; +import 'rxjs/add/operator/catch'; +import { Injectable } from '@angular/core'; +import { Http, Response, Headers } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './hero'; +import { ApiError, ApiErrorHandlerService } from './api-error-handler.service'; + +@Injectable() +export class HeroService { + private headers = new Headers({'Content-Type': 'application/json'}); + private heroesUrl = 'api/heroes'; + + constructor( + private http: Http, + private errorHandler: ApiErrorHandlerService + ) {} + + addHero(hero: Hero) { + return this.http.post(this.heroesUrl, JSON.stringify({ name: hero.name }), { headers: this.headers }); + } + + getHeroes(): Observable { + return this.http.get(this.heroesUrl) + .map(response => response.json().data as Hero[]); + } + + getHero(id: number): Observable { + const url = `${this.heroesUrl}/${id}`; + return this.http.get(url) + .map(response => response.json().data as Hero) + .catch(this.errorHandler.handle); + } + + search(term: string): Observable { + return this.http + .get(`${this.heroesUrl}/?name=${term}`) + .map((r: Response) => r.json().data as Hero[]); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/hero.ts b/public/docs/_examples/rxjs/ts/app/hero.ts new file mode 100644 index 0000000000..e3eac516da --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} diff --git a/public/docs/_examples/rxjs/ts/app/in-memory-data.service.ts b/public/docs/_examples/rxjs/ts/app/in-memory-data.service.ts new file mode 100644 index 0000000000..9b227bf2d3 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/in-memory-data.service.ts @@ -0,0 +1,19 @@ +// #docregion , init +import { InMemoryDbService } from 'angular-in-memory-web-api'; +export class InMemoryDataService implements InMemoryDbService { + createDb() { + let heroes = [ + {id: 1, name: 'Mr. Nice'}, + {id: 2, name: 'Narco'}, + {id: 3, name: 'Bombasto'}, + {id: 4, name: 'Celeritas'}, + {id: 5, name: 'Magneta'}, + {id: 6, name: 'RubberMan'}, + {id: 7, name: 'Dynama'}, + {id: 8, name: 'Dr IQ'}, + {id: 9, name: 'Magma'}, + {id: 10, name: 'Tornado'} + ]; + return {heroes}; + } +} diff --git a/public/docs/_examples/rxjs/ts/app/loading.component.css b/public/docs/_examples/rxjs/ts/app/loading.component.css new file mode 100644 index 0000000000..522401dae0 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/loading.component.css @@ -0,0 +1,8 @@ +.loading { + position: absolute; + width: 100%; + height: 100%; + font-size: 50px; + left: 50%; + top: 50%; +} diff --git a/public/docs/_examples/rxjs/ts/app/loading.component.ts b/public/docs/_examples/rxjs/ts/app/loading.component.ts new file mode 100644 index 0000000000..bf49e0b4fe --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/loading.component.ts @@ -0,0 +1,28 @@ +import 'rxjs/add/operator/delay'; +import { Component, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs/Subscription'; +import { LoadingService } from './loading.service'; + +@Component({ + moduleId: module.id, + selector: 'loading-component', + template: ` +
LOADING
+ `, + styleUrls: ['./loading.component.css'] +}) +export class LoadingComponent implements OnInit { + loading: boolean = true; + sub: Subscription; + + constructor(private loadingService: LoadingService) {} + + ngOnInit() { + this.sub = this.loadingService.loading$ + .subscribe(loading => this.loading = loading); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/loading.service.ts b/public/docs/_examples/rxjs/ts/app/loading.service.ts new file mode 100644 index 0000000000..8069ebaace --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/loading.service.ts @@ -0,0 +1,22 @@ +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/distinctUntilChanged'; +import { Injectable } from '@angular/core'; +import { Router, Event, RoutesRecognized, NavigationStart } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; + +@Injectable() +export class LoadingService { + loading$: Observable; + + constructor(private router: Router) { + this.loading$ = this.router.events.map((event: Event) => { + if ( event instanceof NavigationStart || event instanceof RoutesRecognized ) { + return true; + } else { + // return false for NavigationEnd, NavigationError and NavigationCancel events + return false; + } + }) + .distinctUntilChanged(); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/main.ts b/public/docs/_examples/rxjs/ts/app/main.ts new file mode 100644 index 0000000000..961a226688 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/main.ts @@ -0,0 +1,6 @@ +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/rxjs/ts/example-config.json b/public/docs/_examples/rxjs/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/rxjs/ts/index.html b/public/docs/_examples/rxjs/ts/index.html new file mode 100644 index 0000000000..ee23e5f6a4 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/index.html @@ -0,0 +1,31 @@ + + + + + + + RxJS in Angular + + + + + + + + + + + + + + + + + loading... + + + + diff --git a/public/docs/_examples/rxjs/ts/plnkr.json b/public/docs/_examples/rxjs/ts/plnkr.json new file mode 100644 index 0000000000..d85bc59fe3 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/plnkr.json @@ -0,0 +1,9 @@ +{ + "description": "RxJS", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[0-9].*" + ], + "tags": ["rxjs", "observable"] +} diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade new file mode 100644 index 0000000000..4d33d93acd --- /dev/null +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -0,0 +1,182 @@ +block includes + include ../_util-fns + +:marked + **Observables** provided by the Reactive Extensions for Javascript (RxJS) library provide applications with an extensive API + for handling asynchronous and event-based values produced over time. + + An application is made up of many different streams of information. Whether it be user input into + a form, navigating from one route another, making an HTTP request to fetch some data, updating the application view with + new data as its received, or many other examples, each of these events happen over time. Observables provide a interface to + handle the many different sources of events and help to transform these events as they flow throughout an application. + + This guide will serve as an introductory chapter to Observables, common uses cases for Observables in an Angular application + and how Observables are used and provided by the Angular framework. + + ## Table of Contents + * [The Observable](#definition "") + * [Observables and Promises](#promises "") + * [Observable Lifecycle](#lifecycle "") + * [Observer](#lifecycle "") + * [Subscriptions](#subscriptions "") + * [Creating Observables](#creating "") + * [Operators](#transforming "") + * [Error Handling](#error-handling "") + * [Framework APIs](#apis "") + * [Impact on Change Detection](#change-detection "") + * [Further Reading](#reading "") + +:marked + The Observable: a function at its core + + An Observable, simply put, is a specific type of function with a specific purpose. Its a function that accepts an `Observer` to produce values and + returns a function for cancellation. It represents an action that can be performed. This action may be performed right now, or at some point + in the future. + + An action can be anything, from simply "return a constant" to "make an HTTP request". Here’s a simple "action" function that increments a counter + and returns the new value. + + // Example of simple counter + + The same functionality can be produced with an Observable. Observables don't return values directly, as they can be produced synchronously asynchronously. + An Observer is used by an Observable to consume its produced values. + + // Example of observable counter + + There is a key difference between these two examples. In the first counter example, the results of the action were produced when the function was called. + In the Observable counter, the Observable was created, with the [Observer](https://en.wikipedia.org/wiki/Observer_pattern) to produce values and the value incremented but the action hasn't been performed yet. + It represents an action we've defined, but it hasn't been executed. + + // Example of subscribing to observable counter + + Observable streams are **cold** or **lazy** by nature, meaning that until you invoke them, they will not produce + any values. Invoking an Observable is done using the `subscribe` method, which makes the Observable **hot** and calls the observer's method to produce + values. The `subscribe` function also returns an object with an `unsubscribe` from an Observable and no longer receive the values it produces. + + // Example of unsubscribing + + Managing the lifecycle of an Observable will be discussed later in the chapter. + +:marked + Observables and Promises: More different than alike + + RxJS and Observables have been around for a long time, and they aren't the first concept of handling asynchronous events. Before Observables became more prevalent, + the `Promise` was the primary way of handling asynchronous events. Promises and Observables share some similarities as they both handle asynchronous events, + both implement a function to handle execution and error handling, but they are more different then alike. + + Promises + * Produce a one-time value + * Can be composed + * Are always resolved/rejected asynchronously + * Are always multicast to multiple receivers + + Observables + * Produce a number of values over time + * Can be composed + * Resolve synchronously/asynchronously + * Multicast when needed + + The strength of Observables is producing and handling values over time, which is something a Promise wasn't designed to do. Observables also provide mechanisms + for easy cancellation, retrying upon failure and transformations. Observables include a rich library of operators, along with the extensibility to provide a more powerful + tool to handle the various streams of events in an application. So does this mean Promises are no longer needed? Absolutely not. Promises will continue to serve a purpose as the right tool for the job in some situations. + The good news is Observables support conversion to a Promise for instances where a one-off value needs to be produced without managing the lifecycle + of an Observable. + +:marked + Observable Anatomy: Next, Error, and Complete + + Even though an Observable is a function that performs an action, it has a lifecycle. An Observable has 3 types of notifications it produces through its + lifetime: `next`, `error`, and `complete`. + + The `next` is called whenever the observable produces a new value, notifying the Observer + that some value was produced. The `next` Observables may produce a single value, or multiple values over time. These values can be a number, string, object + + + Let's modify the HeroesReadyComponent to produce two values when subscribed. + + // Example of Observable that produces 2 values + + When click the `increment` button this time and subscribes to the Observable, the counter will be incremented twice and produce two values. + + Whenever an Observable produces an error, it uses the `error` event. An error event can only happen once during the invocation of an Observable action. + + // Example of Observable that errors + + The Observable notifies its Observer through that an error has occurred. + + Observables can also notify observers of completion. A completion event signals that no more values will be produced by the Observable. Like the error event, + a completion event can only happen once. + + // Example of Observable that produces values, then completes + +:marked + Observer: The Observable's Consumer + An Observer is provided to an Observable to consume its values produced. An observer provides callbacks for the notification types produced by an Observable: `next`, + `error`, and `complete`. + + An Observer is provided to an Observable through the `subscribe` method in 2 ways + + * As a single object containing 3 callbacks for each notification. + * As 3 arguments in the subscribe method for each notification + + // Example of single object and logging out each notification + + // Example of 3 arguments to subscribe method + +:marked + Subscription: Maintaining of resources + + As mentioned earlier, an Observable is not invoked until its `subscribed` to. This starts the execution of the Observable to produce values or events to an Observer. + This subscription is an allocation of resources for the action performed by the Observable. Naturally, you want to clean up resources used by the Observable when + finished with its execution. Each time an Observable is subscribed, it returns a `Subscription`. The `Subscription` is an object that handles the resources provided + by the Observable, along with the `unsubscribe`. The `unsubscribe` method provided by the `Subscription` disposes of the resources allocated by the Observable. + + // Example of unsubscribing + + As a general rule and good practice, resources that are allocated and used must be cleaned up, and the Observable subscription is no different. Angular provides + APIs that manage Observable lifecycles without the need to unsubscribe, but for those Observables you create, cleanup of resources is a must to protect against + memory leaks and other unwanted side effects. + + // Example of unsubscribe in ngOnDestroy + + In this example, only one subscription is used and disposed of. Managing multiple subscriptions using the `unsubscribe` method + has the potential to get unwieldy. You'll learn about different ways to to end subscriptions of multiple Observables later in the chapter. + + // Example of unsubscribe using a Subject + +:marked + Operators: Observable functions + + The Observable prototype contains many `operators`, which are methods that perform an operation. Operators are **pure functions** + that are stateless. Stateless operators are less error-prone as they are provided an Observable, in which an operation is performed on, + and return a new Observable, without modifying the original Observable. The Observable prototype comes with a minimal set of operators by default, + with a large set of operators that can be added. Operators are also used to (create Observable instances) from existing events. + + There are different ways to access the operators provided by RxJS: By patching the Observable prototype or by importing the operator functions directly. + Each operator only needs to be patched on the prototype once, but where you choose to patch the Observable requires consideration. We'll examine a + few options below: + + // Example of importing entire rxjs/Rx library + + By importing `rxjs/Rx`, the **entire** set of RxJS operators are added to the Observable prototype. In most cases, you will only use a subset of + the available operators and adding the entire set increases the overall bundle size of your application. This is + only recommended for sample apps, and in testing environments. + + // Example of patching the Observable prototype + + Since minimizing bundle size is a recommended practice, you should only import the operators you need, where you need them. + This approach includes importing operators multiple times in different files, but safeguards against using operators + without having patched the Observable prototype first. Since feature areas can be loaded lazily, this also allows you the benefit + of keeping certain operators in separate bundles and only loaded them when needed. + + // Example of importing operators directly + + // Note about tree-shaking when pat + + NOTE: Operators patched onto the Observable prototype that are unused will not be tree-shaken. + + Operators can also be directly imported and used without patching the Observable prototype. This is the + recommended option for third-party libraries, staying with the approach to only import what you need. It + is also more suited for tree-shaking when bundling your Angular application. + +:marked From d45fa98addc97adae2ad7ae7e5a61e75d794f90e Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Wed, 25 Jan 2017 22:59:55 -0600 Subject: [PATCH 02/13] Added more prose for managing subscriptions and async pipe --- .../docs/_examples/rxjs/ts/app/app.module.ts | 5 +- .../rxjs/ts/app/hero-counter.component.1.ts | 30 +++ .../rxjs/ts/app/hero-counter.component.ts | 43 ++++ public/docs/ts/latest/guide/rxjs.jade | 235 ++++++++++-------- 4 files changed, 205 insertions(+), 108 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/app/hero-counter.component.1.ts create mode 100644 public/docs/_examples/rxjs/ts/app/hero-counter.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/app.module.ts b/public/docs/_examples/rxjs/ts/app/app.module.ts index 67871109ef..1f7b754ff0 100644 --- a/public/docs/_examples/rxjs/ts/app/app.module.ts +++ b/public/docs/_examples/rxjs/ts/app/app.module.ts @@ -11,7 +11,7 @@ import { LoadingComponent } from './loading.component'; import { HeroSearchComponent } from './hero-search.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroListComponent } from './hero-list.component'; - +import { HeroCounterComponent } from './hero-counter.component'; import { LoadingService } from './loading.service'; import { HeroService } from './hero.service'; @@ -34,7 +34,8 @@ import { ApiErrorHandlerService } from './api-error-handler.service'; LoadingComponent, HeroSearchComponent, HeroDetailComponent, - HeroListComponent + HeroListComponent, + HeroCounterComponent ], providers: [ HeroService, diff --git a/public/docs/_examples/rxjs/ts/app/hero-counter.component.1.ts b/public/docs/_examples/rxjs/ts/app/hero-counter.component.1.ts new file mode 100644 index 0000000000..9067fd4642 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-counter.component.1.ts @@ -0,0 +1,30 @@ +// // #docplaster +// // #docregion +// import { Component, OnInit } from '@angular/core'; +// +// import { HeroService } from './hero.service'; +// import { Hero } from './hero'; +// +// @Component({ +// template: ` +//

HEROES

+//
    +//
  • +// {{ hero.id }} {{ hero.name }} +//
  • +//
+// ` +// }) +// export class HeroListComponent implements OnInit { +// heroes: Hero[]; +// +// constructor( +// private service: HeroService +// ) {} +// +// ngOnInit() { +// this.service.getHeroes() +// .subscribe(heroes => this.heroes = heroes); +// } +// } +// // #enddocregion diff --git a/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts new file mode 100644 index 0000000000..83403e8926 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts @@ -0,0 +1,43 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; +import { Subscription } from 'rxjs/Subscription'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + selector: 'hero-counter', + template: ` +

HERO COUNTER

+

+ Heroes {{ count }} +

+ ` +}) +export class HeroCounterComponent implements OnInit { + count: number = 0; + counter$: Observable; + sub: Subscription; + destroy$ = new Subject(); + + ngOnInit() { + this.counter$ = Observable.create((observer: Observer) => { + const interval = setInterval(() => { + observer.next(this.count++); + }, 1000); + }); + + this.counter$ + .takeUntil(this.destroy$) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + } +} +// #enddocregion diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index 4d33d93acd..0a8121cd7f 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -16,46 +16,16 @@ block includes ## Table of Contents * [The Observable](#definition "") * [Observables and Promises](#promises "") - * [Observable Lifecycle](#lifecycle "") - * [Observer](#lifecycle "") - * [Subscriptions](#subscriptions "") - * [Creating Observables](#creating "") - * [Operators](#transforming "") - * [Error Handling](#error-handling "") * [Framework APIs](#apis "") - * [Impact on Change Detection](#change-detection "") + * [Error Handling](#error-handling "") * [Further Reading](#reading "") :marked The Observable: a function at its core - An Observable, simply put, is a specific type of function with a specific purpose. Its a function that accepts an `Observer` to produce values and + An Observable, simply put, is a specific type of function with a specific purpose. It’s a function that accepts an `Observer` to produce values and returns a function for cancellation. It represents an action that can be performed. This action may be performed right now, or at some point - in the future. - - An action can be anything, from simply "return a constant" to "make an HTTP request". Here’s a simple "action" function that increments a counter - and returns the new value. - - // Example of simple counter - - The same functionality can be produced with an Observable. Observables don't return values directly, as they can be produced synchronously asynchronously. - An Observer is used by an Observable to consume its produced values. - - // Example of observable counter - - There is a key difference between these two examples. In the first counter example, the results of the action were produced when the function was called. - In the Observable counter, the Observable was created, with the [Observer](https://en.wikipedia.org/wiki/Observer_pattern) to produce values and the value incremented but the action hasn't been performed yet. - It represents an action we've defined, but it hasn't been executed. - - // Example of subscribing to observable counter - - Observable streams are **cold** or **lazy** by nature, meaning that until you invoke them, they will not produce - any values. Invoking an Observable is done using the `subscribe` method, which makes the Observable **hot** and calls the observer's method to produce - values. The `subscribe` function also returns an object with an `unsubscribe` from an Observable and no longer receive the values it produces. - - // Example of unsubscribing - - Managing the lifecycle of an Observable will be discussed later in the chapter. + in the future. An action can be anything, from simply "return a constant", "make an HTTP request" or "navigate to another page". :marked Observables and Promises: More different than alike @@ -76,107 +46,160 @@ block includes * Resolve synchronously/asynchronously * Multicast when needed - The strength of Observables is producing and handling values over time, which is something a Promise wasn't designed to do. Observables also provide mechanisms + One of the strengths of Observables is producing and handling values over time, which is something a Promise wasn't designed to do. Observables also provide mechanisms for easy cancellation, retrying upon failure and transformations. Observables include a rich library of operators, along with the extensibility to provide a more powerful - tool to handle the various streams of events in an application. So does this mean Promises are no longer needed? Absolutely not. Promises will continue to serve a purpose as the right tool for the job in some situations. - The good news is Observables support conversion to a Promise for instances where a one-off value needs to be produced without managing the lifecycle - of an Observable. + tool to handle the various streams of events in an application. So does this mean Promises are no longer needed? Absolutely not. Promises will continue to serve a purpose as + the right tool for the job in some situations. :marked - Observable Anatomy: Next, Error, and Complete + Basic Principles - Even though an Observable is a function that performs an action, it has a lifecycle. An Observable has 3 types of notifications it produces through its - lifetime: `next`, `error`, and `complete`. + * Subscribe/unsubscribe + * Observer + * next/error/complete - The `next` is called whenever the observable produces a new value, notifying the Observer - that some value was produced. The `next` Observables may produce a single value, or multiple values over time. These values can be a number, string, object +:marked + Managing Subscriptions + Observables like any other instance use resources and those resources add to the overall resources used in your application. Observables + provide a `Subscription` for each `Subscriber` of the Observable that comes with a way to _unsubscribe_ or clean up any resources used + while listening for values produced by the Observable. We'll look at a simple example of how to unsubscribe from and Observable once + its no longer needed. - Let's modify the HeroesReadyComponent to produce two values when subscribed. + We'll create a component named `HeroCounterComponent` that will do a simple task of increasing a total of heroes. We'll simulate + that this hero counter is running as long as the component is active an in view. Once the component is destroyed, we no longer + want to listen for any changes coming from the Observable counter. - // Example of Observable that produces 2 values + // Example of hero counter - When click the `increment` button this time and subscribes to the Observable, the counter will be incremented twice and produce two values. + Since you know Angular has lifecycle hooks, we can use the `ngOnDestroy` lifecycle hook to unsubscribe from this Observable counter + and clean up its resources. - Whenever an Observable produces an error, it uses the `error` event. An error event can only happen once during the invocation of an Observable action. + // Example of unsubscribe - // Example of Observable that errors + Disposing of a single subscription when your component is destroyed is very manageable, but as you use more Observables managing + multiple subscriptions can get unwieldy. We can use a better approach to managing subscriptions. Observables have `operators` + that can cancel other observable streams. We can end multiple observable streams with one observable using the `takeUntil` operator. + The `takeUntil` operator takes an Observable and when that observable emits a value, the Observables that are producing values will + stop emitting values and complete. - The Observable notifies its Observer through that an error has occurred. + Let's update our hero counter example to use the `takeUntil` operator. In order to use the `takeUntil` operator, we must add it + to the base Observable prototype. We'll import the operator which will add it to the observable. - Observables can also notify observers of completion. A completion event signals that no more values will be produced by the Observable. Like the error event, - a completion event can only happen once. + // Example of import for takeUntil operator - // Example of Observable that produces values, then completes + Since we need an Observable that emits a value, we can use a `Subject`. We'll cover streams you can create on your own later in + the chapter, as a `Subject` is a special type of Observable. We'll create a `destroy$` observable using the Subject. -:marked - Observer: The Observable's Consumer - An Observer is provided to an Observable to consume its values produced. An observer provides callbacks for the notification types produced by an Observable: `next`, - `error`, and `complete`. + // Example of new Subject() - An Observer is provided to an Observable through the `subscribe` method in 2 ways + Now we can add the `takeUntil` operator to our Observable and once the `destroy$` Observable produces a value, + the counter Observable will complete and will no longer produce any values. - * As a single object containing 3 callbacks for each notification. - * As 3 arguments in the subscribe method for each notification + // example of destroy$ in ngOnDestroy - // Example of single object and logging out each notification - - // Example of 3 arguments to subscribe method + This approach scales and you can use a single observable to trigger completion across multiple subscriptions. :marked - Subscription: Maintaining of resources + Async Pipe: Less is more - As mentioned earlier, an Observable is not invoked until its `subscribed` to. This starts the execution of the Observable to produce values or events to an Observer. - This subscription is an allocation of resources for the action performed by the Observable. Naturally, you want to clean up resources used by the Observable when - finished with its execution. Each time an Observable is subscribed, it returns a `Subscription`. The `Subscription` is an object that handles the resources provided - by the Observable, along with the `unsubscribe`. The `unsubscribe` method provided by the `Subscription` disposes of the resources allocated by the Observable. + We can manage Observables imperatively through manually subscribing and unsubscribing when needed but we can also + manage them declaratively in our templates using the `Async Pipe`. The async pipe can also take care of our Subscription + management, as it can take an Observable or a Promise, listen for its emitted values and will destroy its subscriptions + with the disposing of the component. This allows us to use Observables with less boilerplate and that's always a good thing. - // Example of unsubscribing + We'll create another component that displays a list of heroes using these two options. Our component will retrieve a list of + Heroes from our `HeroService` and subscribe to set them to a variable in the component. - As a general rule and good practice, resources that are allocated and used must be cleaned up, and the Observable subscription is no different. Angular provides - APIs that manage Observable lifecycles without the need to unsubscribe, but for those Observables you create, cleanup of resources is a must to protect against - memory leaks and other unwanted side effects. + // example of hero list component - // Example of unsubscribe in ngOnDestroy + As you can see, we called and subscribed to the `getHeroes` function in our HeroService which returned an Observable provided + by the HTTP client and the `ngFor` directive is set up to display the heroes. In the `subscribe` function we assign the returned heroes to the heroes variable. Here we are only assigning + the `heroes` value to bind it to our template. The `Async Pipe` lets us skip the manual subscription, as it will handle this + for us. The updated template is below. - In this example, only one subscription is used and disposed of. Managing multiple subscriptions using the `unsubscribe` method - has the potential to get unwieldy. You'll learn about different ways to to end subscriptions of multiple Observables later in the chapter. + // example of hero list with async pipe - // Example of unsubscribe using a Subject + When our component is rendered, the async pipe will subscribe to the Observable to listen for emitted values. Once the values + are produced it will wire bind those values to the same `ngFor` directive. If we were to initiate another sequence of heroes + the pipe would handle updated the retrieved values along with destroying the Observable subscription once the component is destroyed. :marked - Operators: Observable functions - - The Observable prototype contains many `operators`, which are methods that perform an operation. Operators are **pure functions** - that are stateless. Stateless operators are less error-prone as they are provided an Observable, in which an operation is performed on, - and return a new Observable, without modifying the original Observable. The Observable prototype comes with a minimal set of operators by default, - with a large set of operators that can be added. Operators are also used to (create Observable instances) from existing events. - - There are different ways to access the operators provided by RxJS: By patching the Observable prototype or by importing the operator functions directly. - Each operator only needs to be patched on the prototype once, but where you choose to patch the Observable requires consideration. We'll examine a - few options below: - - // Example of importing entire rxjs/Rx library - - By importing `rxjs/Rx`, the **entire** set of RxJS operators are added to the Observable prototype. In most cases, you will only use a subset of - the available operators and adding the entire set increases the overall bundle size of your application. This is - only recommended for sample apps, and in testing environments. - - // Example of patching the Observable prototype + Data sharing - Since minimizing bundle size is a recommended practice, you should only import the operators you need, where you need them. - This approach includes importing operators multiple times in different files, but safeguards against using operators - without having patched the Observable prototype first. Since feature areas can be loaded lazily, this also allows you the benefit - of keeping certain operators in separate bundles and only loaded them when needed. - - // Example of importing operators directly - - // Note about tree-shaking when pat - - NOTE: Operators patched onto the Observable prototype that are unused will not be tree-shaken. - - Operators can also be directly imported and used without patching the Observable prototype. This is the - recommended option for third-party libraries, staying with the approach to only import what you need. It - is also more suited for tree-shaking when bundling your Angular application. +:marked + Error Handling :marked + Stream Integration +:marked + Framework APIs: Angular-provided streams + + Angular makes use of Observables internally and externally through its APIs template syntax using the Async pipe, user input + with Reactive or Model-Driven Forms, making external requests using Http and route information with the Router. + By using Observables underneath, these APIs provide a consistent way for you to use and become more comfortable + with using Observables in your application to handle your streams of data that are produced over time. + Another major advantage of the streams provided by Angular is that they are managed for you, + so no need to unsubscribe to clean up subscriptions. + + Http + + Making external requests from our application is a very common action in every application. You make a request, + handle the success or failure of that request and wait for the request to be made again. Sometimes it’s not as + simple as that as you need the be able to retry, cancel or delay requests. Being efficient with requests to save + on data transferred is vital when every byte counts. HTTP requests are not a one-off action as many elements of + your application is driven by external data. The Http client in Angular is built on top of Observables which + provide the ability to handle one single request or multiple requests seamlessly, along with retrying and + cancellations of requests. + + Example: Use hero service to make a request, make it fail to show retries, conditional retry. + + Async Pipe + + As we’ve talked about previously, Observables must be subscribed to in order to handle the data they produce, + and must be unsubscribed from in order to clean up the resources they have used. You’ve gone through how to + subscribe to an Observable, get its data and provide that data through a variable in your class. There is also + a built-in pipe available for use with template syntax to manage Observable subscriptions called the Async Pipe. + When used in a template, the async pipe will subscribe to the Observable or evaluate a Promise, receive its + emitted values and dispose of its subscription once the component is destroyed. The pipe also ties into Change Detection, + so that when new values are produced, the component is marked for detection in order to determine whether it’s changes + need to be reflected in your user interface. This is useful as it reduces the amount of boilerplate you need when setting + up data to be fetched and provided to your template. We’ll learn later about cases where using an async pipe is beneficial + versus managing your own subscription manually. + + Example: Fetching heroes using a service, subscribing/unsubscribing manually then removing the subscription and delegating responsibility to the async pipe. Another example would be showing hero details with multiple async pipes, but instead using a single subscription. + + Forms + With many applications, user input is required to initiate or complete an action. Whether it be logging in or filling out + an information page, user data is another stream that needs to be handled. In contrast to template-driven forms where the + responsibility is on the developer to gather the pieces of data from the form, the model-driven/Reactive forms uses + Observables to easily provide a continuous stream of user input. By using the reactive approach, our form will be ready + to handle user input streams from form fields, as well as provide that form data seamlessly to another stream for + processing. + + Example: Simple form that displays form status/value changes over time. Also displays creating an Observable from an existing event. valueChanges on individual field/entire form + + Router + + The browser URL is another stream of information. It provides you with a canonical link to the application view you are + displaying at any given moment, along with information about what data to display. The Angular Router uses the browser URL + to provide you with multiple streams that hook into the navigation process, URL data provided through your route + configuration, parameters provided for context and path information. These pieces of data are provided by the Router + through Observables, since we are certain that these streams happen in a continuous fashion as a user navigates throughout + your Angular application. + + Router Observables: Events, Parameters, Data, URL Segments + + Example: Use router events to build a small loading service and component. + + The Observables provided by these area can be used together. The same set of functionality and extensibility can be combined together in a very powerful and practical way. In order to demonstrate how these work together, you’re going to build a hero search component. Let’s start by gathering some requirements about what your typeahead will need. + + Take input from the user + Make a search based on that input + Only search when the user has changed the search terms + Cancel any in-progress requests if you user initiates a new search + Only make a request after the user hasn’t interacted within a certain time frame + Display search results + Sync the user’s search terms in the browser URL + + Example: Hero search typeahead component From 445c048012ab30d3e99e6912bc9b96bdd822c84f Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 29 Jan 2017 21:58:09 -0600 Subject: [PATCH 03/13] Added more writeup on data sharing. Added examples to guide --- .../rxjs/ts/app/api-error-handler.service.ts | 2 + .../rxjs/ts/app/app-routing.module.ts | 2 + .../_examples/rxjs/ts/app/app.component.ts | 15 +- .../docs/_examples/rxjs/ts/app/app.module.ts | 14 +- .../rxjs/ts/app/event-aggregator.service.ts | 25 ++- .../rxjs/ts/app/hero-counter.component.ts | 9 +- .../rxjs/ts/app/loading.component.ts | 7 +- .../_examples/rxjs/ts/app/loading.service.ts | 2 + .../rxjs/ts/app/message-log.component.ts | 25 +++ public/docs/ts/latest/guide/rxjs.jade | 148 +++++++++++++----- 10 files changed, 186 insertions(+), 63 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/app/message-log.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts b/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts index bae30435ea..0170133b9a 100644 --- a/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts +++ b/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts @@ -1,3 +1,5 @@ +// #docplaster +// #docregion import { Injectable } from '@angular/core'; import { Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; diff --git a/public/docs/_examples/rxjs/ts/app/app-routing.module.ts b/public/docs/_examples/rxjs/ts/app/app-routing.module.ts index 17b338383e..e7cfdf1392 100644 --- a/public/docs/_examples/rxjs/ts/app/app-routing.module.ts +++ b/public/docs/_examples/rxjs/ts/app/app-routing.module.ts @@ -6,10 +6,12 @@ import { AddHeroComponent } from './add-hero.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroSearchComponent } from './hero-search.component'; import { HeroListComponent } from './hero-list.component'; +import { HeroCounterComponent } from './hero-counter.component'; const appRoutes: Routes = [ { path: 'heroes/add', component: AddHeroComponent }, { path: 'heroes/search', component: HeroSearchComponent }, + { path: 'hero/counter', component: HeroCounterComponent }, { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, diff --git a/public/docs/_examples/rxjs/ts/app/app.component.ts b/public/docs/_examples/rxjs/ts/app/app.component.ts index 435bd816fa..5fdaefa447 100644 --- a/public/docs/_examples/rxjs/ts/app/app.component.ts +++ b/public/docs/_examples/rxjs/ts/app/app.component.ts @@ -1,6 +1,7 @@ // #docplaster // #docregion -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { EventAggregatorService } from './event-aggregator.service'; @Component({ selector: 'my-app', @@ -8,13 +9,23 @@ import { Component } from '@angular/core';

RxJS in Angular

Heroes
+ Hero Counter
Add Hero
Hero Search + ` }) -export class AppComponent { +export class AppComponent implements OnInit { + constructor(private eventService: EventAggregatorService) {} + + ngOnInit() { + this.eventService.add({ + type: 'init', + message: 'Application Initialized' + }); + } } diff --git a/public/docs/_examples/rxjs/ts/app/app.module.ts b/public/docs/_examples/rxjs/ts/app/app.module.ts index 1f7b754ff0..e3761f109d 100644 --- a/public/docs/_examples/rxjs/ts/app/app.module.ts +++ b/public/docs/_examples/rxjs/ts/app/app.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpModule } from '@angular/http'; -import { ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; @@ -12,13 +12,19 @@ import { HeroSearchComponent } from './hero-search.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroListComponent } from './hero-list.component'; import { HeroCounterComponent } from './hero-counter.component'; +import { MessageLogComponent } from './message-log.component'; + import { LoadingService } from './loading.service'; import { HeroService } from './hero.service'; +// #docregion event-aggregator-import +import { EventAggregatorService } from './event-aggregator.service'; +// #enddocregion event-aggregator-import +import { ApiErrorHandlerService } from './api-error-handler.service'; + // Imports for loading & configuring the in-memory web api import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; -import { ApiErrorHandlerService } from './api-error-handler.service'; @NgModule({ imports: [ @@ -35,11 +41,13 @@ import { ApiErrorHandlerService } from './api-error-handler.service'; HeroSearchComponent, HeroDetailComponent, HeroListComponent, - HeroCounterComponent + HeroCounterComponent, + MessageLogComponent ], providers: [ HeroService, LoadingService, + EventAggregatorService, ApiErrorHandlerService ], bootstrap: [ AppComponent ] diff --git a/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts b/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts index d32b776bd6..d8cb7b0369 100644 --- a/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts +++ b/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts @@ -1,27 +1,38 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +// #docplaster +// #docregion +// #docregion imports +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +// #enddocregion imports +// #docregion event-interface export interface AppEvent { type: string; message: string; } +// #enddocregion event-interface @Injectable() export class EventAggregatorService { - _events: AppEvent[]; - events$: BehaviorSubject = new BehaviorSubject([]); + _events: AppEvent[] = []; + events$: BehaviorSubject; + + constructor() { + this._events = []; + this.events$ = new BehaviorSubject(this._events); + } add(event: AppEvent) { this._events.push(event); - this.next(); + this.notify(); } clear() { this._events = []; - this.next(); + this.notify(); } - next() { + notify() { this.events$.next(this._events); } } diff --git a/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts index 83403e8926..caba154902 100644 --- a/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts +++ b/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts @@ -1,14 +1,11 @@ // #docplaster // #docregion -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subject } from 'rxjs/Subject'; import { Observable } from 'rxjs/Observable'; import { Observer } from 'rxjs/Observer'; import { Subscription } from 'rxjs/Subscription'; -import { HeroService } from './hero.service'; -import { Hero } from './hero'; - @Component({ selector: 'hero-counter', template: ` @@ -18,7 +15,7 @@ import { Hero } from './hero';

` }) -export class HeroCounterComponent implements OnInit { +export class HeroCounterComponent implements OnInit, OnDestroy { count: number = 0; counter$: Observable; sub: Subscription; @@ -26,7 +23,7 @@ export class HeroCounterComponent implements OnInit { ngOnInit() { this.counter$ = Observable.create((observer: Observer) => { - const interval = setInterval(() => { + setInterval(() => { observer.next(this.count++); }, 1000); }); diff --git a/public/docs/_examples/rxjs/ts/app/loading.component.ts b/public/docs/_examples/rxjs/ts/app/loading.component.ts index bf49e0b4fe..35e6646a62 100644 --- a/public/docs/_examples/rxjs/ts/app/loading.component.ts +++ b/public/docs/_examples/rxjs/ts/app/loading.component.ts @@ -1,5 +1,6 @@ -import 'rxjs/add/operator/delay'; -import { Component, OnInit } from '@angular/core'; +// #docplaster +// #docregion +import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { LoadingService } from './loading.service'; @@ -11,7 +12,7 @@ import { LoadingService } from './loading.service'; `, styleUrls: ['./loading.component.css'] }) -export class LoadingComponent implements OnInit { +export class LoadingComponent implements OnInit, OnDestroy { loading: boolean = true; sub: Subscription; diff --git a/public/docs/_examples/rxjs/ts/app/loading.service.ts b/public/docs/_examples/rxjs/ts/app/loading.service.ts index 8069ebaace..fa56fbb256 100644 --- a/public/docs/_examples/rxjs/ts/app/loading.service.ts +++ b/public/docs/_examples/rxjs/ts/app/loading.service.ts @@ -1,3 +1,5 @@ +// #docplaster +// #docregion import 'rxjs/add/operator/map'; import 'rxjs/add/operator/distinctUntilChanged'; import { Injectable } from '@angular/core'; diff --git a/public/docs/_examples/rxjs/ts/app/message-log.component.ts b/public/docs/_examples/rxjs/ts/app/message-log.component.ts new file mode 100644 index 0000000000..0588793360 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/message-log.component.ts @@ -0,0 +1,25 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { EventAggregatorService, AppEvent } from './event-aggregator.service'; +import { Observable } from 'rxjs/Observable'; + +@Component({ + selector: 'message-log', + template: ` +

Event Log

+ +
    +
  • {{ event.message }}
  • +
+ ` +}) +export class MessageLogComponent implements OnInit { + events$: Observable; + + constructor(private eventService: EventAggregatorService) {} + + ngOnInit() { + this.events$ = this.eventService.events$; + } +} diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index 0a8121cd7f..599c4c5116 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -15,52 +15,55 @@ block includes ## Table of Contents * [The Observable](#definition "") - * [Observables and Promises](#promises "") + * [Observables and Promises](#observables-vs-promises "") * [Framework APIs](#apis "") * [Error Handling](#error-handling "") * [Further Reading](#reading "") -:marked - The Observable: a function at its core +h3#definition The Observable: a function at its core +:marked An Observable, simply put, is a specific type of function with a specific purpose. It’s a function that accepts an `Observer` to produce values and returns a function for cancellation. It represents an action that can be performed. This action may be performed right now, or at some point in the future. An action can be anything, from simply "return a constant", "make an HTTP request" or "navigate to another page". :marked - Observables and Promises: More different than alike + TODO Overview of basic principles + + * Subscribe/unsubscribe + * Observer - next/error/complete + + Link to official RxJS docs +h3#observables-vs-promises Observables and Promises: More different than alike + +:marked RxJS and Observables have been around for a long time, and they aren't the first concept of handling asynchronous events. Before Observables became more prevalent, the `Promise` was the primary way of handling asynchronous events. Promises and Observables share some similarities as they both handle asynchronous events, both implement a function to handle execution and error handling, but they are more different then alike. - Promises - * Produce a one-time value - * Can be composed + ***Promises*** + * Always eagerly evaluated + * Produce a value/error once + * Cannot be composed * Are always resolved/rejected asynchronously * Are always multicast to multiple receivers - Observables - * Produce a number of values over time + ***Observables*** + * Can be evaluated lazily + * Produce multiple values/errors * Can be composed * Resolve synchronously/asynchronously - * Multicast when needed + * Multicast when needed using a Subject One of the strengths of Observables is producing and handling values over time, which is something a Promise wasn't designed to do. Observables also provide mechanisms for easy cancellation, retrying upon failure and transformations. Observables include a rich library of operators, along with the extensibility to provide a more powerful tool to handle the various streams of events in an application. So does this mean Promises are no longer needed? Absolutely not. Promises will continue to serve a purpose as the right tool for the job in some situations. -:marked - Basic Principles - - * Subscribe/unsubscribe - * Observer - * next/error/complete +h3#managing-subscriptions Managing Subscriptions :marked - Managing Subscriptions - Observables like any other instance use resources and those resources add to the overall resources used in your application. Observables provide a `Subscription` for each `Subscriber` of the Observable that comes with a way to _unsubscribe_ or clean up any resources used while listening for values produced by the Observable. We'll look at a simple example of how to unsubscribe from and Observable once @@ -100,41 +103,87 @@ block includes This approach scales and you can use a single observable to trigger completion across multiple subscriptions. -:marked - Async Pipe: Less is more ++makeExcerpt('app/hero-counter.component.ts', '') - We can manage Observables imperatively through manually subscribing and unsubscribing when needed but we can also +h3#async-pipe Async Pipe: Less is more +:marked + You can manage Observables imperatively through manually subscribing and unsubscribing when needed but you can also manage them declaratively in our templates using the `Async Pipe`. The async pipe can also take care of our Subscription management, as it can take an Observable or a Promise, listen for its emitted values and will destroy its subscriptions with the disposing of the component. This allows us to use Observables with less boilerplate and that's always a good thing. - We'll create another component that displays a list of heroes using these two options. Our component will retrieve a list of + You will create another component that displays a list of heroes using these two options. Our component will retrieve a list of Heroes from our `HeroService` and subscribe to set them to a variable in the component. - // example of hero list component ++makeExcerpt('app/hero-list.component.1.ts (subscribe)', '') +:marked As you can see, we called and subscribed to the `getHeroes` function in our HeroService which returned an Observable provided - by the HTTP client and the `ngFor` directive is set up to display the heroes. In the `subscribe` function we assign the returned heroes to the heroes variable. Here we are only assigning - the `heroes` value to bind it to our template. The `Async Pipe` lets us skip the manual subscription, as it will handle this - for us. The updated template is below. + by the HTTP client and the `ngFor` directive is set up to display the heroes. In the `subscribe` function we assign the returned heroes to the heroes variable. + Here you are only assigning the `heroes` value to bind it to our template. The `Async Pipe` lets us skip the manual subscription, + as it will handle this for you. The updated template is below. - // example of hero list with async pipe ++makeExcerpt('app/hero-list.component.ts (async pipe)', '') - When our component is rendered, the async pipe will subscribe to the Observable to listen for emitted values. Once the values - are produced it will wire bind those values to the same `ngFor` directive. If we were to initiate another sequence of heroes + When your component is rendered, the async pipe will subscribe to the Observable to listen for emitted values. Once the values + are produced it will wire those values to the same `ngFor` directive. If you were to initiate another sequence of heroes the pipe would handle updated the retrieved values along with destroying the Observable subscription once the component is destroyed. +h3#sharing-data Sharing data with a stream :marked - Data sharing + As you build out your Angular application, you will start sharing data between multiple components. These components may span across multiple routes + or application views in your application hierarchy. This allows you to centralize where that data comes from and allow multiple recipients of + that data to handle it according to their needs. With Observables, you can push changes to this data and notify all of the subscribers so they can react + to it. + + You will need a simple message bus provided by a service to aggregate events to share across multiple components. The name of your service will be + aptly named `EventAggregatorService`. Since you want your Observable subscribes to all get the "latest" value from the stream, you'll use a `BehaviorSubject`. + + A `BehaviorSubject` is a special type of Observable that has a memory of the current value or the last value produced, so each new subscriber of this Observable + will get its current value immediately. + + You'll import the `Injectable` decorator from `@angular/core` and the `BehaviorSubject` from the RxJS library to use it in the service. + ++makeExcerpt('app/event-aggregator.service.ts (event interface)', 'imports') :marked - Error Handling + You'll need an interface to provide consumers with to add messages to the event log. + ++makeExcerpt('app/event-aggregator.service.ts (event interface)', 'event-interface') :marked - Stream Integration + Next, you'll create your service. Since a `BehaviorSubject` keeps the latest value for subscribers, you'll need to provide it with an initial value also. + There is the `add` method for adding additional events to the log and `clear` method for clearing the message. You'll notice that the `notify` method + calls the `events$.next` method to notify the subscribers of a new value pushed to the stream. + ++makeExcerpt('app/event-aggregator.service.ts', '') + +:marked + Now that you have a central place to collect events, you can inject the `EventAggregatorService` throughout your application. In order to display + the message log, you'll create a simple message component to display the aggregated events. You can use the `Async Pipe` here also to wire up the + stream to the template. + ++makeExcerpt('app/message-log.component.ts (message log component)', '') + :marked - Framework APIs: Angular-provided streams + As with other services, you'll import the `EventAggregatorService` and `MessageLogComponent` and add it to the `AppModule` providers and declarations + arrays respectively. ++makeExcerpt('app/app.module.ts', '') + +:marked + To see your message bus in action, you'll import and inject the `EventAggregatorService` in the `AppComponent` and add an event when the Application + starts and add the `message-log` component to the `AppComponent` template. + ++makeExcerpt('app/app.component.ts (message log)', '') + +h3#error-handling Error Handling +:marked + ++makeExcerpt('app/api-error-handler.service.ts', '') + +h3#framework-apis Framework APIs: Angular-provided streams +:marked Angular makes use of Observables internally and externally through its APIs template syntax using the Async pipe, user input with Reactive or Model-Driven Forms, making external requests using Http and route information with the Router. By using Observables underneath, these APIs provide a consistent way for you to use and become more comfortable @@ -153,7 +202,9 @@ block includes cancellations of requests. Example: Use hero service to make a request, make it fail to show retries, conditional retry. ++makeExcerpt('app/hero-list.component.1.ts', '') +:marked Async Pipe As we’ve talked about previously, Observables must be subscribed to in order to handle the data they produce, @@ -168,7 +219,9 @@ block includes versus managing your own subscription manually. Example: Fetching heroes using a service, subscribing/unsubscribing manually then removing the subscription and delegating responsibility to the async pipe. Another example would be showing hero details with multiple async pipes, but instead using a single subscription. ++makeExcerpt('app/hero-detail.component.ts', '') +:marked Forms With many applications, user input is required to initiate or complete an action. Whether it be logging in or filling out an information page, user data is another stream that needs to be handled. In contrast to template-driven forms where the @@ -191,15 +244,26 @@ block includes Router Observables: Events, Parameters, Data, URL Segments Example: Use router events to build a small loading service and component. ++makeExcerpt('app/loading.service.ts', '') ++makeExcerpt('app/loading.component.ts', '') - The Observables provided by these area can be used together. The same set of functionality and extensibility can be combined together in a very powerful and practical way. In order to demonstrate how these work together, you’re going to build a hero search component. Let’s start by gathering some requirements about what your typeahead will need. - - Take input from the user - Make a search based on that input - Only search when the user has changed the search terms - Cancel any in-progress requests if you user initiates a new search - Only make a request after the user hasn’t interacted within a certain time frame - Display search results - Sync the user’s search terms in the browser URL +h3#integration Stream Integration +:marked + The Observables provided by these area can be used together. The same set of functionality and extensibility can be combined together in a very powerful and + practical way. In order to demonstrate how these work together, you’re going to build a hero search component. Let’s start by gathering some requirements about + what your typeahead will need. + + * Take input from the user + * Make a search based on that input + * Only search when the user has changed the search terms + * Cancel any in-progress requests if you user initiates a new search + * Only make a request after the user hasn’t interacted within a certain time frame + * Display search results + * Sync the user’s search terms in the browser URL Example: Hero search typeahead component ++makeExcerpt('app/hero-search.component.ts', '') + +h3#further-reading Further Reading +:marked + TODO From 829f11d2ce06953d09671e4c4de2c3c6cd11c7b5 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Wed, 1 Feb 2017 23:04:26 -0600 Subject: [PATCH 04/13] Added section on operators --- .../rxjs/ts/app/hero-counter.component.ts | 1 + .../ts/app/heroes-filtered.component.1.ts | 42 ++++++++++++ .../ts/app/heroes-filtered.component.2.ts | 36 ++++++++++ .../rxjs/ts/app/heroes-filtered.component.ts | 35 ++++++++++ public/docs/ts/latest/guide/rxjs.jade | 65 +++++++++++++++++-- 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/app/heroes-filtered.component.1.ts create mode 100644 public/docs/_examples/rxjs/ts/app/heroes-filtered.component.2.ts create mode 100644 public/docs/_examples/rxjs/ts/app/heroes-filtered.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts index caba154902..4e93a7e981 100644 --- a/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts +++ b/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts @@ -1,5 +1,6 @@ // #docplaster // #docregion +import 'rxjs/add/operator/takeUntil'; import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subject } from 'rxjs/Subject'; import { Observable } from 'rxjs/Observable'; diff --git a/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.1.ts b/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.1.ts new file mode 100644 index 0000000000..1ac5eb2b71 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.1.ts @@ -0,0 +1,42 @@ +// #docplaster +// #docregion +// #docregion operator-import +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/filter'; +// #docregion operator +import { Component, OnInit } from '@angular/core'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes: Hero[]; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + this.service.getHeroes() + .do(heroes => { + console.log(heroes.length); + }) + .filter(heroes => heroes.length > 2) + .subscribe(heroes => this.heroes = heroes); + } +} +// #enddocregion operator-import + +// #docregion import-all +import 'rxjs/Rx'; +// #enddocregion import-all diff --git a/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.2.ts b/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.2.ts new file mode 100644 index 0000000000..edf88e48a0 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.2.ts @@ -0,0 +1,36 @@ +// #docplaster +// #docregion +import { _do } from 'rxjs/operator/do'; +import { filter } from 'rxjs/operator/filter'; +import { Component, OnInit } from '@angular/core'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes: Hero[]; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + const heroes$ = this.service.getHeroes(); + const loggedHeroes$ = _do.call(heroes$, (heroes: Hero[]) => { + console.log(heroes.length); + }); + const filteredHeroes$ = filter.call(loggedHeroes$, (heroes: Hero[]) => heroes.length > 2); + + filteredHeroes$.subscribe((heroes: Hero[]) => this.heroes = heroes); + } +} diff --git a/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.ts b/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.ts new file mode 100644 index 0000000000..40f6b3196d --- /dev/null +++ b/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.ts @@ -0,0 +1,35 @@ +// #docplaster +// #docregion +// #docregion filter-import +import 'rxjs/add/operator/filter'; +// #enddocregion filter-import +// #docregion operator +import { Component, OnInit } from '@angular/core'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes: Hero[]; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + this.service.getHeroes() + .filter(heroes => heroes.length > 2) + .subscribe(heroes => this.heroes = heroes); + } +} +// #enddocregion diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index 599c4c5116..bbea69fec7 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -16,9 +16,13 @@ block includes ## Table of Contents * [The Observable](#definition "") * [Observables and Promises](#observables-vs-promises "") - * [Framework APIs](#apis "") + * [Using Operators](#operators "") + * [Managing Subscriptions](#managing-subscriptions "") + * [Sharing Data](#sharing-data "") * [Error Handling](#error-handling "") - * [Further Reading](#reading "") + * [Framework APIs](#framework-apis "") + * [Stream Integration](#integration "") + * [Further Reading](#further-reading "") h3#definition The Observable: a function at its core @@ -61,10 +65,59 @@ h3#observables-vs-promises Observables and Promises: More different than alike tool to handle the various streams of events in an application. So does this mean Promises are no longer needed? Absolutely not. Promises will continue to serve a purpose as the right tool for the job in some situations. +h3#operators Operators: Import them and use them + +:marked + Operators are pure functions that extend the Observable interface, allow you to perform an action against the Observable + and return a new Observable. An Observable comes with very few built-in operators and the rest of the operators are + added to the Observable on demand. There are multiple approaches to make these operators available for use. + One approach is to import the entire RxJS library. + ++makeExcerpt('app/heroes-filtered.component.1.ts', 'import-all') + +:marked + This is the **least recommended** method, as it brings in **all** the Observables operators, + even ones you never use. While convenient, this method is inefficient and can greatly impact the size of your application, + which is always a concern. This method is mainly reserved for prototyping and testing, where such concerns are less important. + + The second method is to import operators selectively by patching the Observable prototype. This allows you to chain + operators together, as each operator returns a new Observable. Below is an example of importing the `filter` and `do` operators. + The `filter` operator filters elements produced by an Observable based on a predicate function that returns a boolean. The `do` operator + provides the Observable value to perform an arbitrary action, such as console logging. + ++makeExcerpt('app/heroes-filtered.component.1.ts', 'operator-import') + +:marked + Had you not imported these common operators before using them with the Observable returned by `getHeroes`, + the Observable would fail to perform these actions as it these functions don't exist on the Observable yet. + + Another common example is two components with incomplete operator imports. Both files have components that use Observable operators + but only one file imports the operators it needs. Interestingly enough if you load the component with the imported operators first and then + load the second component, everything works fine. Conversely, loading the component without the imported operators first blows up because the + operators aren't available on the Observable. + + Another approach is to import the Observable operators directly and call them individually on the Observable. Let's + update your filtered heroes component to use direct imports. + ++makeExcerpt('app/heroes-filtered.component.2.ts (direct operator imports)', '') + +:marked + This approach has no side-effects as you're not patching the Observable prototype. It also is + more conducive to tree shaking versus patching the Observable prototype, which can't be tree-shaken. You're also only importing what you need where you need it, + but this approach doesn't give you the option to chain operators together. If you were building a third-party + library, this would be the recommended approach as you don't want your library to produce any side-effects + to the Observable for consumers of your library but for an application, its less desirable. + + The recommended approach is to import the operators in the file where you use them. Yes, this may lead to + duplicate imports of operators in multiple files, but more importantly this ensures that the operators + that are needed are provided by that file. This becomes especially important with lazy loading, where + certain feature areas may only make use of certain operators. Importing the operators this way ensures + the operators are available regardless of where and when you use them. + h3#managing-subscriptions Managing Subscriptions :marked - Observables like any other instance use resources and those resources add to the overall resources used in your application. Observables + Observables like any other instance use resources and those resources add to the overall weight of your application over time. Observables provide a `Subscription` for each `Subscriber` of the Observable that comes with a way to _unsubscribe_ or clean up any resources used while listening for values produced by the Observable. We'll look at a simple example of how to unsubscribe from and Observable once its no longer needed. @@ -249,9 +302,9 @@ h3#framework-apis Framework APIs: Angular-provided streams h3#integration Stream Integration :marked - The Observables provided by these area can be used together. The same set of functionality and extensibility can be combined together in a very powerful and - practical way. In order to demonstrate how these work together, you’re going to build a hero search component. Let’s start by gathering some requirements about - what your typeahead will need. + The Observables provided by these areas can be used together. The same set of functionality and extensibility can be combined together + in a very powerful and practical way. A prime example is a hero search component that implements a typeahead search feature. + Let’s start by gathering some requirements about what your typeahead will need. * Take input from the user * Make a search based on that input From fba087bab5e0283ba5aadc7e21775b4902d022d5 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Tue, 7 Feb 2017 19:58:11 -0600 Subject: [PATCH 05/13] Refactored files to fit new folder structure --- public/docs/_examples/rxjs/ts/plnkr.json | 1 + .../_examples/rxjs/ts/{ => src}/app/add-hero.component.html | 0 .../_examples/rxjs/ts/{ => src}/app/add-hero.component.ts | 0 .../rxjs/ts/{ => src}/app/api-error-handler.service.ts | 0 .../_examples/rxjs/ts/{ => src}/app/app-routing.module.ts | 0 public/docs/_examples/rxjs/ts/{ => src}/app/app.component.ts | 0 public/docs/_examples/rxjs/ts/{ => src}/app/app.module.ts | 0 .../rxjs/ts/{ => src}/app/event-aggregator.service.ts | 0 .../rxjs/ts/{ => src}/app/hero-counter.component.1.ts | 0 .../rxjs/ts/{ => src}/app/hero-counter.component.ts | 0 .../rxjs/ts/{ => src}/app/hero-detail.component.1.ts | 0 .../_examples/rxjs/ts/{ => src}/app/hero-detail.component.ts | 0 .../_examples/rxjs/ts/{ => src}/app/hero-list.component.1.ts | 0 .../_examples/rxjs/ts/{ => src}/app/hero-list.component.ts | 0 .../rxjs/ts/{ => src}/app/hero-search.component.css | 0 .../rxjs/ts/{ => src}/app/hero-search.component.html | 0 .../_examples/rxjs/ts/{ => src}/app/hero-search.component.ts | 0 public/docs/_examples/rxjs/ts/{ => src}/app/hero.service.ts | 0 public/docs/_examples/rxjs/ts/{ => src}/app/hero.ts | 0 .../rxjs/ts/{ => src}/app/heroes-filtered.component.1.ts | 0 .../rxjs/ts/{ => src}/app/heroes-filtered.component.2.ts | 0 .../rxjs/ts/{ => src}/app/heroes-filtered.component.ts | 0 .../rxjs/ts/{ => src}/app/in-memory-data.service.ts | 0 .../_examples/rxjs/ts/{ => src}/app/loading.component.css | 0 .../_examples/rxjs/ts/{ => src}/app/loading.component.ts | 0 .../docs/_examples/rxjs/ts/{ => src}/app/loading.service.ts | 0 .../_examples/rxjs/ts/{ => src}/app/message-log.component.ts | 0 public/docs/_examples/rxjs/ts/{ => src}/index.html | 5 +++-- public/docs/_examples/rxjs/ts/{app => src}/main.ts | 2 +- 29 files changed, 5 insertions(+), 3 deletions(-) rename public/docs/_examples/rxjs/ts/{ => src}/app/add-hero.component.html (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/add-hero.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/api-error-handler.service.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/app-routing.module.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/app.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/app.module.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/event-aggregator.service.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-counter.component.1.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-counter.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-detail.component.1.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-detail.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-list.component.1.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-list.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-search.component.css (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-search.component.html (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero-search.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero.service.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/hero.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/heroes-filtered.component.1.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/heroes-filtered.component.2.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/heroes-filtered.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/in-memory-data.service.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/loading.component.css (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/loading.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/loading.service.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/app/message-log.component.ts (100%) rename public/docs/_examples/rxjs/ts/{ => src}/index.html (88%) rename public/docs/_examples/rxjs/ts/{app => src}/main.ts (75%) diff --git a/public/docs/_examples/rxjs/ts/plnkr.json b/public/docs/_examples/rxjs/ts/plnkr.json index d85bc59fe3..63de2208e6 100644 --- a/public/docs/_examples/rxjs/ts/plnkr.json +++ b/public/docs/_examples/rxjs/ts/plnkr.json @@ -1,5 +1,6 @@ { "description": "RxJS", + "basePath": "src/", "files":[ "!**/*.d.ts", "!**/*.js", diff --git a/public/docs/_examples/rxjs/ts/app/add-hero.component.html b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html similarity index 100% rename from public/docs/_examples/rxjs/ts/app/add-hero.component.html rename to public/docs/_examples/rxjs/ts/src/app/add-hero.component.html diff --git a/public/docs/_examples/rxjs/ts/app/add-hero.component.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/add-hero.component.ts rename to public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts b/public/docs/_examples/rxjs/ts/src/app/api-error-handler.service.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/api-error-handler.service.ts rename to public/docs/_examples/rxjs/ts/src/app/api-error-handler.service.ts diff --git a/public/docs/_examples/rxjs/ts/app/app-routing.module.ts b/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/app-routing.module.ts rename to public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts diff --git a/public/docs/_examples/rxjs/ts/app/app.component.ts b/public/docs/_examples/rxjs/ts/src/app/app.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/app.component.ts rename to public/docs/_examples/rxjs/ts/src/app/app.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/app.module.ts b/public/docs/_examples/rxjs/ts/src/app/app.module.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/app.module.ts rename to public/docs/_examples/rxjs/ts/src/app/app.module.ts diff --git a/public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts b/public/docs/_examples/rxjs/ts/src/app/event-aggregator.service.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/event-aggregator.service.ts rename to public/docs/_examples/rxjs/ts/src/app/event-aggregator.service.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-counter.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.1.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-counter.component.1.ts rename to public/docs/_examples/rxjs/ts/src/app/hero-counter.component.1.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-counter.component.ts rename to public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-detail.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.1.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-detail.component.1.ts rename to public/docs/_examples/rxjs/ts/src/app/hero-detail.component.1.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-detail.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-detail.component.ts rename to public/docs/_examples/rxjs/ts/src/app/hero-detail.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-list.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.1.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-list.component.1.ts rename to public/docs/_examples/rxjs/ts/src/app/hero-list.component.1.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-list.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-list.component.ts rename to public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero-search.component.css b/public/docs/_examples/rxjs/ts/src/app/hero-search.component.css similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-search.component.css rename to public/docs/_examples/rxjs/ts/src/app/hero-search.component.css diff --git a/public/docs/_examples/rxjs/ts/app/hero-search.component.html b/public/docs/_examples/rxjs/ts/src/app/hero-search.component.html similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-search.component.html rename to public/docs/_examples/rxjs/ts/src/app/hero-search.component.html diff --git a/public/docs/_examples/rxjs/ts/app/hero-search.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-search.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero-search.component.ts rename to public/docs/_examples/rxjs/ts/src/app/hero-search.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero.service.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero.service.ts rename to public/docs/_examples/rxjs/ts/src/app/hero.service.ts diff --git a/public/docs/_examples/rxjs/ts/app/hero.ts b/public/docs/_examples/rxjs/ts/src/app/hero.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/hero.ts rename to public/docs/_examples/rxjs/ts/src/app/hero.ts diff --git a/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.1.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/heroes-filtered.component.1.ts rename to public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.1.ts diff --git a/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.2.ts b/public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.2.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/heroes-filtered.component.2.ts rename to public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.2.ts diff --git a/public/docs/_examples/rxjs/ts/app/heroes-filtered.component.ts b/public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/heroes-filtered.component.ts rename to public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/in-memory-data.service.ts b/public/docs/_examples/rxjs/ts/src/app/in-memory-data.service.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/in-memory-data.service.ts rename to public/docs/_examples/rxjs/ts/src/app/in-memory-data.service.ts diff --git a/public/docs/_examples/rxjs/ts/app/loading.component.css b/public/docs/_examples/rxjs/ts/src/app/loading.component.css similarity index 100% rename from public/docs/_examples/rxjs/ts/app/loading.component.css rename to public/docs/_examples/rxjs/ts/src/app/loading.component.css diff --git a/public/docs/_examples/rxjs/ts/app/loading.component.ts b/public/docs/_examples/rxjs/ts/src/app/loading.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/loading.component.ts rename to public/docs/_examples/rxjs/ts/src/app/loading.component.ts diff --git a/public/docs/_examples/rxjs/ts/app/loading.service.ts b/public/docs/_examples/rxjs/ts/src/app/loading.service.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/loading.service.ts rename to public/docs/_examples/rxjs/ts/src/app/loading.service.ts diff --git a/public/docs/_examples/rxjs/ts/app/message-log.component.ts b/public/docs/_examples/rxjs/ts/src/app/message-log.component.ts similarity index 100% rename from public/docs/_examples/rxjs/ts/app/message-log.component.ts rename to public/docs/_examples/rxjs/ts/src/app/message-log.component.ts diff --git a/public/docs/_examples/rxjs/ts/index.html b/public/docs/_examples/rxjs/ts/src/index.html similarity index 88% rename from public/docs/_examples/rxjs/ts/index.html rename to public/docs/_examples/rxjs/ts/src/index.html index ee23e5f6a4..94f44300e2 100644 --- a/public/docs/_examples/rxjs/ts/index.html +++ b/public/docs/_examples/rxjs/ts/src/index.html @@ -2,8 +2,10 @@ + + RxJS in Angular @@ -13,12 +15,11 @@ - diff --git a/public/docs/_examples/rxjs/ts/app/main.ts b/public/docs/_examples/rxjs/ts/src/main.ts similarity index 75% rename from public/docs/_examples/rxjs/ts/app/main.ts rename to public/docs/_examples/rxjs/ts/src/main.ts index 961a226688..f332d1d245 100644 --- a/public/docs/_examples/rxjs/ts/app/main.ts +++ b/public/docs/_examples/rxjs/ts/src/main.ts @@ -1,6 +1,6 @@ // #docregion import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app.module'; +import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule); From 0722ddf680d38ee2622e87a4749edf1ddbd6954c Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Tue, 7 Feb 2017 22:24:46 -0600 Subject: [PATCH 06/13] Added section on error handling --- .../rxjs/ts/src/app/hero-counter.component.ts | 6 +- .../rxjs/ts/src/app/hero-list.component.2.ts | 43 +++++++ .../rxjs/ts/src/app/hero-list.component.3.ts | 40 +++++++ .../rxjs/ts/src/app/hero-list.component.4.ts | 52 +++++++++ .../rxjs/ts/src/app/hero-list.component.ts | 12 +- .../rxjs/ts/src/app/hero.service.1.ts | 47 ++++++++ .../_examples/rxjs/ts/src/app/hero.service.ts | 9 +- public/docs/ts/latest/guide/rxjs.jade | 107 +++++++++++++----- 8 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts index 4e93a7e981..67ff991f39 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts @@ -20,7 +20,7 @@ export class HeroCounterComponent implements OnInit, OnDestroy { count: number = 0; counter$: Observable; sub: Subscription; - destroy$ = new Subject(); + onDestroy$ = new Subject(); ngOnInit() { this.counter$ = Observable.create((observer: Observer) => { @@ -30,12 +30,12 @@ export class HeroCounterComponent implements OnInit, OnDestroy { }); this.counter$ - .takeUntil(this.destroy$) + .takeUntil(this.onDestroy$) .subscribe(); } ngOnDestroy() { - this.destroy$.next(); + this.onDestroy$.complete(); } } // #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts new file mode 100644 index 0000000000..b67fb8f2ad --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts @@ -0,0 +1,43 @@ +// #docplaster +// #docregion +// #docregion retry-operator +import 'rxjs/add/operator/retry'; +// #enddocregion retry-operator +import 'rxjs/add/observable/of'; +// #docregion failed-heroes +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes$: Observable; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + // #docregion failed-heroes + this.heroes$ = this.service.getFailedHeroes() + // #enddocregion failed-heroes + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + // #docregion failed-heroes + } +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts new file mode 100644 index 0000000000..2c0cf9e22f --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts @@ -0,0 +1,40 @@ +// #docplaster +// #docregion +// #docregion retry-operator +import 'rxjs/add/operator/retry'; +// #enddocregion retry-operator +import 'rxjs/add/observable/of'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes$: Observable; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + this.heroes$ = this.service.getHeroes() + .retry(3) + .catch(error => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + } +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts new file mode 100644 index 0000000000..2179c1c0d7 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts @@ -0,0 +1,52 @@ +// #docplaster +// #docregion +// #docregion retry-when-operator +import 'rxjs/add/operator/retryWhen'; +// #enddocregion retry-when-operator +import 'rxjs/add/observable/of'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +}) +export class HeroListComponent implements OnInit { + heroes$: Observable; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + this.heroes$ = this.service.getFailedHeroes() + .retryWhen((errors: any) => { + return errors.scan((errorCount, err) => { + if (errorCount >= 2) { + throw err; + } + + if (err.status !== 500) { + return errorCount; + } else { + return errorCount + 1; + } + }, 0); + }) + .catch(error => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + } +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts index 7d3740c5bb..28db6bd572 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts @@ -1,5 +1,9 @@ // #docplaster // #docregion +// #docregion retry-operator +import 'rxjs/add/operator/retry'; +// #enddocregion retry-operator +import 'rxjs/add/observable/of'; import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; @@ -24,7 +28,13 @@ export class HeroListComponent implements OnInit { ) {} ngOnInit() { - this.heroes$ = this.service.getHeroes(); + this.heroes$ = this.service.getFailedHeroes() + .retry(3) + .catch(error => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); } } // #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts new file mode 100644 index 0000000000..4a566cbc57 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts @@ -0,0 +1,47 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/delay'; +import 'rxjs/add/operator/catch'; +import { Injectable } from '@angular/core'; +import { Http, Response, Headers } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './hero'; +import { ApiError, ApiErrorHandlerService } from './api-error-handler.service'; + +@Injectable() +export class HeroService { + private headers = new Headers({'Content-Type': 'application/json'}); + private heroesUrl = 'api/heroes'; + + constructor( + private http: Http, + private errorHandler: ApiErrorHandlerService + ) {} + + addHero(hero: Hero) { + return this.http.post(this.heroesUrl, JSON.stringify({ name: hero.name }), { headers: this.headers }); + } + + getHeroes(): Observable { + return this.http.get(this.heroesUrl) + .map(response => response.json().data as Hero[]); + } + + getFailedHeroes(): Observable { + return this.http.get(`${this.heroesUrl}/failed`) + } + + getHero(id: number): Observable { + const url = `${this.heroesUrl}/${id}`; + return this.http.get(url) + .map(response => response.json().data as Hero); + } + + search(term: string): Observable { + return this.http + .get(`${this.heroesUrl}/?name=${term}`) + .map((r: Response) => r.json().data as Hero[]); + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts index 24f3cefde1..4a566cbc57 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts @@ -29,11 +29,14 @@ export class HeroService { .map(response => response.json().data as Hero[]); } - getHero(id: number): Observable { + getFailedHeroes(): Observable { + return this.http.get(`${this.heroesUrl}/failed`) + } + + getHero(id: number): Observable { const url = `${this.heroesUrl}/${id}`; return this.http.get(url) - .map(response => response.json().data as Hero) - .catch(this.errorHandler.handle); + .map(response => response.json().data as Hero); } search(term: string): Observable { diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index bbea69fec7..b6a408e662 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -73,7 +73,7 @@ h3#operators Operators: Import them and use them added to the Observable on demand. There are multiple approaches to make these operators available for use. One approach is to import the entire RxJS library. -+makeExcerpt('app/heroes-filtered.component.1.ts', 'import-all') ++makeExcerpt('src/app/heroes-filtered.component.1.ts', 'import-all') :marked This is the **least recommended** method, as it brings in **all** the Observables operators, @@ -85,7 +85,7 @@ h3#operators Operators: Import them and use them The `filter` operator filters elements produced by an Observable based on a predicate function that returns a boolean. The `do` operator provides the Observable value to perform an arbitrary action, such as console logging. -+makeExcerpt('app/heroes-filtered.component.1.ts', 'operator-import') ++makeExcerpt('src/app/heroes-filtered.component.1.ts', 'operator-import') :marked Had you not imported these common operators before using them with the Observable returned by `getHeroes`, @@ -99,15 +99,19 @@ h3#operators Operators: Import them and use them Another approach is to import the Observable operators directly and call them individually on the Observable. Let's update your filtered heroes component to use direct imports. -+makeExcerpt('app/heroes-filtered.component.2.ts (direct operator imports)', '') ++makeExcerpt('src/app/heroes-filtered.component.2.ts (direct operator imports)', '') :marked This approach has no side-effects as you're not patching the Observable prototype. It also is more conducive to tree shaking versus patching the Observable prototype, which can't be tree-shaken. You're also only importing what you need where you need it, - but this approach doesn't give you the option to chain operators together. If you were building a third-party - library, this would be the recommended approach as you don't want your library to produce any side-effects - to the Observable for consumers of your library but for an application, its less desirable. + but this approach doesn't give you the option to chain operators together. +.l-sub-section + :marked + If you are building a third-party Angular library, this would be the recommended approach as you don't want your library to produce any side-effects + to the Observable for consumers of your library. + +:marked The recommended approach is to import the operators in the file where you use them. Yes, this may lead to duplicate imports of operators in multiple files, but more importantly this ensures that the operators that are needed are provided by that file. This becomes especially important with lazy loading, where @@ -156,7 +160,7 @@ h3#managing-subscriptions Managing Subscriptions This approach scales and you can use a single observable to trigger completion across multiple subscriptions. -+makeExcerpt('app/hero-counter.component.ts', '') ++makeExcerpt('src/app/hero-counter.component.ts', '') h3#async-pipe Async Pipe: Less is more :marked @@ -168,7 +172,7 @@ h3#async-pipe Async Pipe: Less is more You will create another component that displays a list of heroes using these two options. Our component will retrieve a list of Heroes from our `HeroService` and subscribe to set them to a variable in the component. -+makeExcerpt('app/hero-list.component.1.ts (subscribe)', '') ++makeExcerpt('src/app/hero-list.component.1.ts (subscribe)', '') :marked As you can see, we called and subscribed to the `getHeroes` function in our HeroService which returned an Observable provided @@ -176,7 +180,7 @@ h3#async-pipe Async Pipe: Less is more Here you are only assigning the `heroes` value to bind it to our template. The `Async Pipe` lets us skip the manual subscription, as it will handle this for you. The updated template is below. -+makeExcerpt('app/hero-list.component.ts (async pipe)', '') ++makeExcerpt('src/app/hero-list.component.ts (async pipe)', '') When your component is rendered, the async pipe will subscribe to the Observable to listen for emitted values. Once the values are produced it will wire those values to the same `ngFor` directive. If you were to initiate another sequence of heroes @@ -197,46 +201,97 @@ h3#sharing-data Sharing data with a stream You'll import the `Injectable` decorator from `@angular/core` and the `BehaviorSubject` from the RxJS library to use it in the service. -+makeExcerpt('app/event-aggregator.service.ts (event interface)', 'imports') ++makeExcerpt('src/app/event-aggregator.service.ts (event interface)', 'imports') :marked You'll need an interface to provide consumers with to add messages to the event log. -+makeExcerpt('app/event-aggregator.service.ts (event interface)', 'event-interface') ++makeExcerpt('src/app/event-aggregator.service.ts (event interface)', 'event-interface') :marked Next, you'll create your service. Since a `BehaviorSubject` keeps the latest value for subscribers, you'll need to provide it with an initial value also. There is the `add` method for adding additional events to the log and `clear` method for clearing the message. You'll notice that the `notify` method calls the `events$.next` method to notify the subscribers of a new value pushed to the stream. -+makeExcerpt('app/event-aggregator.service.ts', '') ++makeExcerpt('src/app/event-aggregator.service.ts', '') :marked Now that you have a central place to collect events, you can inject the `EventAggregatorService` throughout your application. In order to display the message log, you'll create a simple message component to display the aggregated events. You can use the `Async Pipe` here also to wire up the stream to the template. -+makeExcerpt('app/message-log.component.ts (message log component)', '') ++makeExcerpt('src/app/message-log.component.ts (message log component)', '') :marked As with other services, you'll import the `EventAggregatorService` and `MessageLogComponent` and add it to the `AppModule` providers and declarations arrays respectively. -+makeExcerpt('app/app.module.ts', '') ++makeExcerpt('src/app/app.module.ts', '') :marked To see your message bus in action, you'll import and inject the `EventAggregatorService` in the `AppComponent` and add an event when the Application starts and add the `message-log` component to the `AppComponent` template. -+makeExcerpt('app/app.component.ts (message log)', '') ++makeExcerpt('src/app/app.component.ts (message log)', '') h3#error-handling Error Handling :marked + As often as you strive for perfect conditions, errors will happen. Servers go down, invalid data is sent and other issues cause errors to happen + when processing data. While you can do your best to prevent these errors, its also wise to be ready for them when they do happen. The scenario + this is most likely to happen in is when you're making data requests to an external API. This is a common task done with the Angular HTTP client. + The HTTP client provides methods that return requests as Observables, which in turn can handle errors. Now you may wonder why you would want + to use an Observable when HTTP requests are usually a one-and-done operation, but an Observable provides robust error handling that wouldn't be + able to easily do with a Promise. Let's simulate a failed request in your in your `HeroListComponent` when retrieving heroes from the `HeroService`. + ++makeExcerpt('src/app/hero.service.1.ts (failed heroes)', '') + +:marked + Now you can update your HeroListComponent to make the failed request. + ++makeExcerpt('src/app/hero-list.component.2.ts (failed heroes)', 'failed-heroes') + +:marked + This is what the `HeroListComponent` currently looks like with no error handling. + With this current setup, you have no way to recover and that's less than ideal. So let's add some error handling with the `catch` operator. You don't + need to patch the Observable to use the `catch` operator, as its in the basic set of included operators. + + You'll also import the `of` operator, which lets you create an Observable sequence from a list of arguments. In this case, you're returning an empty array + of `Heroes` when an error occurs. + ++makeExcerpt('src/app/hero-list.component.2.ts (catch and return)', '') + +:marked + Now we have a path of recovery. The `catch` operator will continue the observable sequence even after an exception occurs. Since you know that each + Observable operator returns a new Observable, you can use this to return an empty array or even a new Observable HTTP request. -+makeExcerpt('app/api-error-handler.service.ts', '') +h3#retry Retry Failed Observable +:marked + This is a simple path of recovery, but we can go further. What if you also wanted to _retry_ a failed request? With Observables, this is as easy as adding a new operator, + aptly named `retry`. + + Of course you'll need to import the operator first. + ++makeExcerpt('src/app/hero-list.component.3.ts (retry operator)', 'retry-operator') + +:marked + You can add the `retry` operator to the Observable sequence. The retry operator takes an argument of the number of times you want to retry the sequence before completing. + ++makeExcerpt('src/app/hero-list.component.ts', '') -h3#framework-apis Framework APIs: Angular-provided streams :marked + Now your request will be attempted 3 times before giving up and going into the error sequence. + +// h3#retry-when Conditional Retry +// :marked + You've seen how to catch an error, how to retry an error a certain number of times, but you can also retry conditionally. This is useful + when you only want to retry under certain conditions like when a request returns a 500 error. This type of error would be out of your control + and could resolve itself quickly. The `retryWhen` operator does this seamlessly. You wouldn't use this on a login request for fear of being locked + out but for retrieving list of data, it can be very useful. + +// +makeExcerpt('src/app/api-error-handler.service.ts', '') + +// h3#framework-apis Framework APIs: Angular-provided streams +// :marked Angular makes use of Observables internally and externally through its APIs template syntax using the Async pipe, user input with Reactive or Model-Driven Forms, making external requests using Http and route information with the Router. By using Observables underneath, these APIs provide a consistent way for you to use and become more comfortable @@ -255,9 +310,9 @@ h3#framework-apis Framework APIs: Angular-provided streams cancellations of requests. Example: Use hero service to make a request, make it fail to show retries, conditional retry. -+makeExcerpt('app/hero-list.component.1.ts', '') +// +makeExcerpt('src/app/hero-list.component.1.ts', '') -:marked +// :marked Async Pipe As we’ve talked about previously, Observables must be subscribed to in order to handle the data they produce, @@ -272,9 +327,9 @@ h3#framework-apis Framework APIs: Angular-provided streams versus managing your own subscription manually. Example: Fetching heroes using a service, subscribing/unsubscribing manually then removing the subscription and delegating responsibility to the async pipe. Another example would be showing hero details with multiple async pipes, but instead using a single subscription. -+makeExcerpt('app/hero-detail.component.ts', '') +// +makeExcerpt('src/app/hero-detail.component.ts', '') -:marked +// :marked Forms With many applications, user input is required to initiate or complete an action. Whether it be logging in or filling out an information page, user data is another stream that needs to be handled. In contrast to template-driven forms where the @@ -297,11 +352,11 @@ h3#framework-apis Framework APIs: Angular-provided streams Router Observables: Events, Parameters, Data, URL Segments Example: Use router events to build a small loading service and component. -+makeExcerpt('app/loading.service.ts', '') -+makeExcerpt('app/loading.component.ts', '') +// +makeExcerpt('src/app/loading.service.ts', '') +// +makeExcerpt('src/app/loading.component.ts', '') -h3#integration Stream Integration -:marked +// h3#integration Stream Integration +// :marked The Observables provided by these areas can be used together. The same set of functionality and extensibility can be combined together in a very powerful and practical way. A prime example is a hero search component that implements a typeahead search feature. Let’s start by gathering some requirements about what your typeahead will need. @@ -315,7 +370,7 @@ h3#integration Stream Integration * Sync the user’s search terms in the browser URL Example: Hero search typeahead component -+makeExcerpt('app/hero-search.component.ts', '') +// +makeExcerpt('src/app/hero-search.component.ts', '') h3#further-reading Further Reading :marked From 66159cbcc8bef6d6b2b58e93c10d3f9691d7ffde Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 12 Feb 2017 15:10:03 -0600 Subject: [PATCH 07/13] Updated section on subscription management --- .../ts/src/app/hero-counter.component.1.ts | 70 +++++++++++-------- .../rxjs/ts/src/app/hero-counter.component.ts | 16 ++++- public/docs/ts/latest/guide/rxjs.jade | 26 ++++--- 3 files changed, 69 insertions(+), 43 deletions(-) diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.1.ts index 9067fd4642..5761dacbed 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.1.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.1.ts @@ -1,30 +1,40 @@ -// // #docplaster -// // #docregion -// import { Component, OnInit } from '@angular/core'; -// -// import { HeroService } from './hero.service'; -// import { Hero } from './hero'; -// -// @Component({ -// template: ` -//

HEROES

-//
    -//
  • -// {{ hero.id }} {{ hero.name }} -//
  • -//
-// ` -// }) -// export class HeroListComponent implements OnInit { -// heroes: Hero[]; -// -// constructor( -// private service: HeroService -// ) {} -// -// ngOnInit() { -// this.service.getHeroes() -// .subscribe(heroes => this.heroes = heroes); -// } -// } -// // #enddocregion +// #docplaster +// #docregion +// #docregion counter-unsubscribe +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; +import { Subscription } from 'rxjs/Subscription'; + +@Component({ + selector: 'hero-counter', + template: ` +

HERO COUNTER

+

+ Heroes {{ count }} +

+ ` +}) +export class HeroCounterComponent implements OnInit, OnDestroy { + count: number = 0; + counter$: Observable; + sub: Subscription; + + ngOnInit() { + this.counter$ = Observable.create((observer: Observer) => { + setInterval(() => { + observer.next(this.count++); + }, 1000); + }); + + this.sub = this.counter$.subscribe(); + } +// #enddocregion counter-unsubscribe +// #docregion ngOnDestroy-unsubscribe + ngOnDestroy() { + this.sub.unsubscribe(); + } +// #enddocregion ngOnDestroy-unsubscribe +// #docregion counter-unsubscribe +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts index 67ff991f39..fb318890a4 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts @@ -1,12 +1,17 @@ // #docplaster // #docregion +// #docregion takeUntil-operator import 'rxjs/add/operator/takeUntil'; -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { Subject } from 'rxjs/Subject'; +// #enddocregion takeUntil-operator +import { Component, OnInit, OnDestroy } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Observer } from 'rxjs/Observer'; import { Subscription } from 'rxjs/Subscription'; +// #docregion import-subject +import { Subject } from 'rxjs/Subject'; +// #enddocregion import-subject + @Component({ selector: 'hero-counter', template: ` @@ -20,7 +25,10 @@ export class HeroCounterComponent implements OnInit, OnDestroy { count: number = 0; counter$: Observable; sub: Subscription; + +// #docregion onDestroy-subject onDestroy$ = new Subject(); +// #enddocregion onDestroy-subject ngOnInit() { this.counter$ = Observable.create((observer: Observer) => { @@ -33,9 +41,11 @@ export class HeroCounterComponent implements OnInit, OnDestroy { .takeUntil(this.onDestroy$) .subscribe(); } - + +// #docregion ngOnDestroy-complete ngOnDestroy() { this.onDestroy$.complete(); } +// #enddocregion ngOnDestroy-complete } // #enddocregion diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index b6a408e662..f54805d595 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -127,16 +127,18 @@ h3#managing-subscriptions Managing Subscriptions its no longer needed. We'll create a component named `HeroCounterComponent` that will do a simple task of increasing a total of heroes. We'll simulate - that this hero counter is running as long as the component is active an in view. Once the component is destroyed, we no longer + that this hero counter is running as long as the component is active in the view. Once the component is destroyed, we no longer want to listen for any changes coming from the Observable counter. - // Example of hero counter ++makeExcerpt('src/app/hero-counter.component.1.ts', 'counter-unsubscribe') +:marked Since you know Angular has lifecycle hooks, we can use the `ngOnDestroy` lifecycle hook to unsubscribe from this Observable counter and clean up its resources. - // Example of unsubscribe ++makeExcerpt('src/app/hero-counter.component.1.ts', 'ngOnDestroy-unsubscribe') +:marked Disposing of a single subscription when your component is destroyed is very manageable, but as you use more Observables managing multiple subscriptions can get unwieldy. We can use a better approach to managing subscriptions. Observables have `operators` that can cancel other observable streams. We can end multiple observable streams with one observable using the `takeUntil` operator. @@ -146,19 +148,23 @@ h3#managing-subscriptions Managing Subscriptions Let's update our hero counter example to use the `takeUntil` operator. In order to use the `takeUntil` operator, we must add it to the base Observable prototype. We'll import the operator which will add it to the observable. - // Example of import for takeUntil operator ++makeExcerpt('src/app/hero-counter.component.ts', 'takeUntil-operator') +:marked Since we need an Observable that emits a value, we can use a `Subject`. We'll cover streams you can create on your own later in - the chapter, as a `Subject` is a special type of Observable. We'll create a `destroy$` observable using the Subject. + the chapter, as a `Subject` is a special type of Observable. - // Example of new Subject() ++makeExcerpt('src/app/hero-counter.component.ts', 'import-subject') - Now we can add the `takeUntil` operator to our Observable and once the `destroy$` Observable produces a value, - the counter Observable will complete and will no longer produce any values. +:marked + You'll need to create an `onDestroy$` observable using the Subject. - // example of destroy$ in ngOnDestroy ++makeExcerpt('src/app/hero-counter.component.ts', 'onDestroy-subject') - This approach scales and you can use a single observable to trigger completion across multiple subscriptions. +:marked + Now we can add the `takeUntil` operator to our Observable and once the `onDestroy$` Observable completes, + the counter Observable will complete and will no longer produce any values. This approach scales and you can use a single observable + to trigger completion across multiple subscriptions. +makeExcerpt('src/app/hero-counter.component.ts', '') From c3324ec849d639dccc6e345bea27141288ff3e0a Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 12 Feb 2017 20:47:48 -0600 Subject: [PATCH 08/13] Cleaned up error handling example --- .../rxjs/ts/src/app/add-hero.component.html | 15 ---- .../rxjs/ts/src/app/add-hero.component.ts | 74 ------------------ .../ts/src/app/api-error-handler.service.ts | 36 --------- .../rxjs/ts/src/app/app-routing.module.ts | 6 -- .../rxjs/ts/src/app/app.component.ts | 2 - .../_examples/rxjs/ts/src/app/app.module.ts | 18 ++--- .../rxjs/ts/src/app/hero-counter.component.ts | 12 ++- .../ts/src/app/hero-detail.component.1.ts | 56 -------------- .../rxjs/ts/src/app/hero-detail.component.ts | 55 -------------- .../rxjs/ts/src/app/hero-list.component.2.ts | 18 +---- .../rxjs/ts/src/app/hero-list.component.3.ts | 12 +-- .../rxjs/ts/src/app/hero-list.component.4.ts | 52 ------------- .../rxjs/ts/src/app/hero-list.component.ts | 12 +-- .../rxjs/ts/src/app/hero-search.component.css | 16 ---- .../ts/src/app/hero-search.component.html | 12 --- .../rxjs/ts/src/app/hero-search.component.ts | 67 ----------------- .../rxjs/ts/src/app/hero.service.1.ts | 26 +------ .../rxjs/ts/src/app/hero.service.2.ts | 32 ++++++++ .../rxjs/ts/src/app/hero.service.3.ts | 38 ++++++++++ .../_examples/rxjs/ts/src/app/hero.service.ts | 45 +++++------ public/docs/ts/latest/guide/_data.json | 5 ++ public/docs/ts/latest/guide/rxjs.jade | 75 +++++++++---------- 22 files changed, 151 insertions(+), 533 deletions(-) delete mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.html delete mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts delete mode 100644 public/docs/_examples/rxjs/ts/src/app/api-error-handler.service.ts delete mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-detail.component.1.ts delete mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-detail.component.ts delete mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts delete mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-search.component.css delete mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-search.component.html delete mode 100644 public/docs/_examples/rxjs/ts/src/app/hero-search.component.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html deleted file mode 100644 index f57c7976cd..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html +++ /dev/null @@ -1,15 +0,0 @@ -

ADD HERO

-
-

- *Name:
- Name is required -

-

- Description: -

-

- -

-
- -The hero has been added diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts deleted file mode 100644 index 145dd0ff68..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts +++ /dev/null @@ -1,74 +0,0 @@ -// #docplaster -// #docregion -import 'rxjs/add/observable/of'; -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/observable/merge'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/do'; -import { Component, OnInit, OnDestroy, AfterViewInit, ViewChildren, ElementRef } from '@angular/core'; -import { FormBuilder, FormGroup, FormControlName, Validators } from '@angular/forms'; -import { Observable } from 'rxjs/Observable'; -import { Subscription } from 'rxjs/Subscription'; - -import { HeroService } from './hero.service'; - -@Component({ - moduleId: module.id, - templateUrl: 'add-hero.component.html', - styles: [ '.error { color: red }' ] -}) -export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { - @ViewChildren(FormControlName, { read: ElementRef }) formControls: ElementRef[]; - - form: FormGroup; - sub: Subscription; - showErrors: boolean = false; - submitted: boolean = false; - success: boolean; - - constructor( - private formBuilder: FormBuilder, - private heroService: HeroService - ) {} - - ngOnInit() { - this.form = this.formBuilder.group({ - name: ['', [Validators.required]], - description: [''] - }); - } - - ngAfterViewInit() { - const controlBlurs: Observable[] = this.formControls.map(field => Observable.fromEvent(field.nativeElement, 'blur')); - - this.sub = Observable.merge( - this.form.valueChanges, - ...controlBlurs - ) - .debounceTime(300) - .subscribe(() => this.checkErrors()); - } - - checkErrors() { - if (!this.form.valid) { - this.showErrors = true; - } - } - - save(model: any) { - this.success = false; - this.submitted = true; - - this.heroService.addHero(model) - .do(() => { - this.success = true; - this.submitted = false; - }) - .subscribe(); - } - - ngOnDestroy() { - this.sub.unsubscribe(); - } -} diff --git a/public/docs/_examples/rxjs/ts/src/app/api-error-handler.service.ts b/public/docs/_examples/rxjs/ts/src/app/api-error-handler.service.ts deleted file mode 100644 index 0170133b9a..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/api-error-handler.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -// #docplaster -// #docregion -import { Injectable } from '@angular/core'; -import { Response } from '@angular/http'; -import { Observable } from 'rxjs/Observable'; - -export interface ApiError { - message: string; -} - -@Injectable() -export class ApiErrorHandlerService { - handle(resp: Response): Observable { - return Observable.of(resp) - .switchMap(response => { - - let error: ApiError; - - try { - error = response.json().error; - } catch (e) { - if (response.status === 404) { - error = { - message: 'The requested resource was not found' - }; - } else { - error = { - message: 'An unknown error has occurred' - }; - } - } - - return Observable.throw(error); - }); - } -} diff --git a/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts b/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts index e7cfdf1392..6a043fd0c3 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts @@ -2,18 +2,12 @@ // #docregion import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AddHeroComponent } from './add-hero.component'; -import { HeroDetailComponent } from './hero-detail.component'; -import { HeroSearchComponent } from './hero-search.component'; import { HeroListComponent } from './hero-list.component'; import { HeroCounterComponent } from './hero-counter.component'; const appRoutes: Routes = [ - { path: 'heroes/add', component: AddHeroComponent }, - { path: 'heroes/search', component: HeroSearchComponent }, { path: 'hero/counter', component: HeroCounterComponent }, { path: 'heroes', component: HeroListComponent }, - { path: 'hero/:id', component: HeroDetailComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, ]; diff --git a/public/docs/_examples/rxjs/ts/src/app/app.component.ts b/public/docs/_examples/rxjs/ts/src/app/app.component.ts index 5fdaefa447..c73ed45382 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app.component.ts @@ -10,8 +10,6 @@ import { EventAggregatorService } from './event-aggregator.service'; Heroes
Hero Counter
- Add Hero
- Hero Search diff --git a/public/docs/_examples/rxjs/ts/src/app/app.module.ts b/public/docs/_examples/rxjs/ts/src/app/app.module.ts index e3761f109d..8d80d5256c 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app.module.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app.module.ts @@ -6,13 +6,10 @@ import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; -import { AddHeroComponent } from './add-hero.component'; -import { LoadingComponent } from './loading.component'; -import { HeroSearchComponent } from './hero-search.component'; -import { HeroDetailComponent } from './hero-detail.component'; import { HeroListComponent } from './hero-list.component'; import { HeroCounterComponent } from './hero-counter.component'; import { MessageLogComponent } from './message-log.component'; +import { LoadingComponent } from './loading.component'; import { LoadingService } from './loading.service'; import { HeroService } from './hero.service'; @@ -20,7 +17,6 @@ import { HeroService } from './hero.service'; // #docregion event-aggregator-import import { EventAggregatorService } from './event-aggregator.service'; // #enddocregion event-aggregator-import -import { ApiErrorHandlerService } from './api-error-handler.service'; // Imports for loading & configuring the in-memory web api import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; @@ -36,19 +32,15 @@ import { InMemoryDataService } from './in-memory-data.service'; ], declarations: [ AppComponent, - AddHeroComponent, - LoadingComponent, - HeroSearchComponent, - HeroDetailComponent, - HeroListComponent, HeroCounterComponent, - MessageLogComponent + HeroListComponent, + MessageLogComponent, + LoadingComponent ], providers: [ HeroService, LoadingService, - EventAggregatorService, - ApiErrorHandlerService + EventAggregatorService ], bootstrap: [ AppComponent ] }) diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts index fb318890a4..6fcce2f117 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts @@ -37,11 +37,19 @@ export class HeroCounterComponent implements OnInit, OnDestroy { }, 1000); }); - this.counter$ + let counter1Sub = this.counter$ + .takeUntil(this.onDestroy$) + .subscribe(); + + let counter2Sub = this.counter$ + .takeUntil(this.onDestroy$) + .subscribe(); + + let counter3Sub = this.counter$ .takeUntil(this.onDestroy$) .subscribe(); } - + // #docregion ngOnDestroy-complete ngOnDestroy() { this.onDestroy$.complete(); diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.1.ts deleted file mode 100644 index b29aec89ad..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.1.ts +++ /dev/null @@ -1,56 +0,0 @@ -// #docplaster -// #docregion -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/switchMap'; -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; -import { HeroService } from './hero.service'; -import { Hero } from './hero'; -import { Observable } from 'rxjs/Observable'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; - -@Component({ - template: ` -
- Loading Hero... -
-
-

HEROES

-
- {{ (hero$ | async)?.id }} -
-
- - -
-
-
- No hero found -
- ` -}) -export class HeroDetailComponent implements OnInit { - hero$: BehaviorSubject = new BehaviorSubject(null); - loading: boolean = true; - error: boolean; - - constructor( - private heroService: HeroService, - private route: ActivatedRoute - ) {} - - ngOnInit() { - this.route.params - .do(() => this.loading = true) - .switchMap((params: Params) => - this.heroService.getHero(params['id']) - .catch(error => { - this.error = true; - - return Observable.of(null); - }) - ) - .do(() => this.loading = false) - .subscribe(this.hero$); - } -} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.ts deleted file mode 100644 index 54a6d17df4..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/hero-detail.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -// #docplaster -// #docregion -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/switchMap'; -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; -import { HeroService } from './hero.service'; -import { Hero } from './hero'; -import { Observable } from 'rxjs/Observable'; - -@Component({ - template: ` -
- Loading Hero... -
-
-

HEROES

-
- {{ hero.id }} -
-
- - -
-
-
- No hero found -
- ` -}) -export class HeroDetailComponent implements OnInit { - hero: Hero; - loading: boolean = true; - error: boolean; - - constructor( - private heroService: HeroService, - private route: ActivatedRoute - ) {} - - ngOnInit() { - this.route.params - .do(() => this.loading = true) - .switchMap((params: Params) => - this.heroService.getHero(params['id']) - .catch(error => { - this.error = true; - - return Observable.of(null); - }) - ) - .do(() => this.loading = false) - .subscribe((hero: Hero) => this.hero = hero); - } -} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts index b67fb8f2ad..1e8f22b775 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts @@ -1,10 +1,5 @@ // #docplaster // #docregion -// #docregion retry-operator -import 'rxjs/add/operator/retry'; -// #enddocregion retry-operator -import 'rxjs/add/observable/of'; -// #docregion failed-heroes import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; @@ -12,6 +7,7 @@ import { HeroService } from './hero.service'; import { Hero } from './hero'; @Component({ +// #docregion async-pipe template: `

HEROES

    @@ -20,7 +16,9 @@ import { Hero } from './hero';
` +// #enddocregion async-pipe }) +// #docregion observable-heroes export class HeroListComponent implements OnInit { heroes$: Observable; @@ -29,15 +27,7 @@ export class HeroListComponent implements OnInit { ) {} ngOnInit() { - // #docregion failed-heroes - this.heroes$ = this.service.getFailedHeroes() - // #enddocregion failed-heroes - .catch((error: any) => { - console.log(`An error occurred: ${error}`); - - return Observable.of([]); - }); - // #docregion failed-heroes + this.heroes$ = this.service.getHeroes(); } } // #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts index 2c0cf9e22f..45eb8679d8 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.ts @@ -1,9 +1,5 @@ // #docplaster // #docregion -// #docregion retry-operator -import 'rxjs/add/operator/retry'; -// #enddocregion retry-operator -import 'rxjs/add/observable/of'; import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; @@ -28,13 +24,7 @@ export class HeroListComponent implements OnInit { ) {} ngOnInit() { - this.heroes$ = this.service.getHeroes() - .retry(3) - .catch(error => { - console.log(`An error occurred: ${error}`); - - return Observable.of([]); - }); + this.heroes$ = this.service.getHeroes(true); // Simulate a failed request } } // #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts deleted file mode 100644 index 2179c1c0d7..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.4.ts +++ /dev/null @@ -1,52 +0,0 @@ -// #docplaster -// #docregion -// #docregion retry-when-operator -import 'rxjs/add/operator/retryWhen'; -// #enddocregion retry-when-operator -import 'rxjs/add/observable/of'; -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; - -import { HeroService } from './hero.service'; -import { Hero } from './hero'; - -@Component({ - template: ` -

HEROES

-
    -
  • - {{ hero.id }} {{ hero.name }} -
  • -
- ` -}) -export class HeroListComponent implements OnInit { - heroes$: Observable; - - constructor( - private service: HeroService - ) {} - - ngOnInit() { - this.heroes$ = this.service.getFailedHeroes() - .retryWhen((errors: any) => { - return errors.scan((errorCount, err) => { - if (errorCount >= 2) { - throw err; - } - - if (err.status !== 500) { - return errorCount; - } else { - return errorCount + 1; - } - }, 0); - }) - .catch(error => { - console.log(`An error occurred: ${error}`); - - return Observable.of([]); - }); - } -} -// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts index 28db6bd572..7d3740c5bb 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.ts @@ -1,9 +1,5 @@ // #docplaster // #docregion -// #docregion retry-operator -import 'rxjs/add/operator/retry'; -// #enddocregion retry-operator -import 'rxjs/add/observable/of'; import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; @@ -28,13 +24,7 @@ export class HeroListComponent implements OnInit { ) {} ngOnInit() { - this.heroes$ = this.service.getFailedHeroes() - .retry(3) - .catch(error => { - console.log(`An error occurred: ${error}`); - - return Observable.of([]); - }); + this.heroes$ = this.service.getHeroes(); } } // #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-search.component.css b/public/docs/_examples/rxjs/ts/src/app/hero-search.component.css deleted file mode 100644 index 51e3ca4370..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/hero-search.component.css +++ /dev/null @@ -1,16 +0,0 @@ -/* #docregion */ -.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/_examples/rxjs/ts/src/app/hero-search.component.html b/public/docs/_examples/rxjs/ts/src/app/hero-search.component.html deleted file mode 100644 index 66a0c5d1b9..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/hero-search.component.html +++ /dev/null @@ -1,12 +0,0 @@ - -
-

HERO SEARCH

-
- -
-
- -
-
diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-search.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-search.component.ts deleted file mode 100644 index 024717952e..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/hero-search.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -// #docplaster -// #docregion -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/distinctUntilChanged'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/switchMap'; -import 'rxjs/add/operator/takeUntil'; -import 'rxjs/add/observable/merge'; -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { Router, ActivatedRoute, Params } from '@angular/router'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; - -import { HeroService } from './hero.service'; -import { Hero } from './hero'; - -@Component({ - moduleId: module.id, - templateUrl: 'hero-search.component.html', - styleUrls: [ 'hero-search.component.css' ] -}) -export class HeroSearchComponent implements OnInit, OnDestroy { - heroes$: Observable; - destroy$: Subject = new Subject(); - form: FormGroup; - - constructor( - private heroService: HeroService, - private formBuilder: FormBuilder, - private route: ActivatedRoute, - private router: Router - ) {} - - ngOnInit(): void { - this.form = this.formBuilder.group({ - searchTerms: [''] - }); - - const searchTerms$: Observable = this.form.valueChanges - .debounceTime(300) - .map(model => model.searchTerms); - - const querySearch$: Observable = this.route.queryParams - .map((params: Params) => params['q']) - .do(searchTerms => this.form.patchValue({ - searchTerms - })); - - this.heroes$ = Observable.merge(searchTerms$, querySearch$) - .distinctUntilChanged() - .takeUntil(this.destroy$) - .do(q => this.router.navigate(['./'], { queryParams: { q }, relativeTo: this.route })) - .switchMap((term: string) => term - ? this.heroService.search(term) - : Observable.of([]) - ) - .catch(error => { - return Observable.of([]); - }); - } - - ngOnDestroy() { - this.destroy$.next(); - } -} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts index 4a566cbc57..ceba7871e7 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts @@ -1,14 +1,11 @@ // #docplaster // #docregion import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/delay'; -import 'rxjs/add/operator/catch'; import { Injectable } from '@angular/core'; import { Http, Response, Headers } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Hero } from './hero'; -import { ApiError, ApiErrorHandlerService } from './api-error-handler.service'; @Injectable() export class HeroService { @@ -16,32 +13,11 @@ export class HeroService { private heroesUrl = 'api/heroes'; constructor( - private http: Http, - private errorHandler: ApiErrorHandlerService + private http: Http ) {} - addHero(hero: Hero) { - return this.http.post(this.heroesUrl, JSON.stringify({ name: hero.name }), { headers: this.headers }); - } - getHeroes(): Observable { return this.http.get(this.heroesUrl) .map(response => response.json().data as Hero[]); } - - getFailedHeroes(): Observable { - return this.http.get(`${this.heroesUrl}/failed`) - } - - getHero(id: number): Observable { - const url = `${this.heroesUrl}/${id}`; - return this.http.get(url) - .map(response => response.json().data as Hero); - } - - search(term: string): Observable { - return this.http - .get(`${this.heroesUrl}/?name=${term}`) - .map((r: Response) => r.json().data as Hero[]); - } } diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts new file mode 100644 index 0000000000..7b439931e3 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts @@ -0,0 +1,32 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/map'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/catch'; +import { Injectable } from '@angular/core'; +import { Http, Headers } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroService { + private heroesUrl = 'api/heroes'; + + constructor( + private http: Http + ) {} + + // #docregion getHeroes-failed + getHeroes(fail?: boolean): Observable { + return this.http.get(`${this.heroesUrl}${fail ? '/failed' : ''}`) + .map(response => response.json().data as Hero[]) + // #enddocregion getHeroes-failed + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + // #docregion getHeroes-failed + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts new file mode 100644 index 0000000000..7752f0c70d --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts @@ -0,0 +1,38 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/map'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/catch'; +// #docregion retry-import +import 'rxjs/add/operator/retry'; +// #enddocregion retry-import +import { Injectable } from '@angular/core'; +import { Http, Response, Headers } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroService { + private headers = new Headers({'Content-Type': 'application/json'}); + private heroesUrl = 'api/heroes'; + + constructor( + private http: Http + ) {} + + // #docregion getHeroes-failed + getHeroes(fail?: boolean): Observable { + return this.http.get(`${this.heroesUrl}${fail ? '/failed' : ''}`) + // #enddocregion getHeroes-failed + .retry(3) + .map(response => response.json().data as Hero[]) + // #enddocregion getHeroes-failed + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + // #docregion getHeroes-failed + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts index 4a566cbc57..7752f0c70d 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts @@ -1,14 +1,16 @@ // #docplaster // #docregion import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/delay'; +import 'rxjs/add/observable/of'; import 'rxjs/add/operator/catch'; +// #docregion retry-import +import 'rxjs/add/operator/retry'; +// #enddocregion retry-import import { Injectable } from '@angular/core'; import { Http, Response, Headers } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Hero } from './hero'; -import { ApiError, ApiErrorHandlerService } from './api-error-handler.service'; @Injectable() export class HeroService { @@ -16,32 +18,21 @@ export class HeroService { private heroesUrl = 'api/heroes'; constructor( - private http: Http, - private errorHandler: ApiErrorHandlerService + private http: Http ) {} - addHero(hero: Hero) { - return this.http.post(this.heroesUrl, JSON.stringify({ name: hero.name }), { headers: this.headers }); - } - - getHeroes(): Observable { - return this.http.get(this.heroesUrl) - .map(response => response.json().data as Hero[]); - } - - getFailedHeroes(): Observable { - return this.http.get(`${this.heroesUrl}/failed`) - } - - getHero(id: number): Observable { - const url = `${this.heroesUrl}/${id}`; - return this.http.get(url) - .map(response => response.json().data as Hero); - } - - search(term: string): Observable { - return this.http - .get(`${this.heroesUrl}/?name=${term}`) - .map((r: Response) => r.json().data as Hero[]); + // #docregion getHeroes-failed + getHeroes(fail?: boolean): Observable { + return this.http.get(`${this.heroesUrl}${fail ? '/failed' : ''}`) + // #enddocregion getHeroes-failed + .retry(3) + .map(response => response.json().data as Hero[]) + // #enddocregion getHeroes-failed + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + // #docregion getHeroes-failed } } diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index 7a0c4aab83..74a309c2da 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -167,6 +167,11 @@ "intro": "Discover the basics of screen navigation with the Angular Router." }, + "rxjs": { + "title": "RxJS in Angular", + "intro": "Using Observables to manage application streams." + }, + "security": { "title": "Security", "intro": "Developing for content security in Angular applications." diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index f54805d595..0ae5c9deea 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -10,7 +10,7 @@ block includes new data as its received, or many other examples, each of these events happen over time. Observables provide a interface to handle the many different sources of events and help to transform these events as they flow throughout an application. - This guide will serve as an introductory chapter to Observables, common uses cases for Observables in an Angular application + This guide will serve as a reference for common uses cases for Observables in an Angular application and how Observables are used and provided by the Angular framework. ## Table of Contents @@ -31,13 +31,13 @@ h3#definition The Observable: a function at its core returns a function for cancellation. It represents an action that can be performed. This action may be performed right now, or at some point in the future. An action can be anything, from simply "return a constant", "make an HTTP request" or "navigate to another page". -:marked - TODO Overview of basic principles - - * Subscribe/unsubscribe - * Observer - next/error/complete + Using Observables starts with an understanding of the basic principles, in which there are a wealth of resources that cover these in-depth. You should + become more familiar with these principles, as it will help you with how to use these concepts in your Angular application. Below are a few resources + to guide you in the concepts of an Observable and reactivity in general. - Link to official RxJS docs + * [Learning Observable By Building Observable](https://medium.com/@benlesh/learning-observable-by-building-observable-d5da57405d87#.3lun8dyt7) + * [Thinking Reactively](https://www.youtube.com/watch?v=3LKMwkuK0ZE) + * [RxJS Official Documentation](http://reactivex.io/rxjs/) h3#observables-vs-promises Observables and Promises: More different than alike @@ -89,7 +89,7 @@ h3#operators Operators: Import them and use them :marked Had you not imported these common operators before using them with the Observable returned by `getHeroes`, - the Observable would fail to perform these actions as it these functions don't exist on the Observable yet. + the Observable would fail to perform these actions as these functions don't exist on the Observable instance yet. Another common example is two components with incomplete operator imports. Both files have components that use Observable operators but only one file imports the operators it needs. Interestingly enough if you load the component with the imported operators first and then @@ -168,12 +168,12 @@ h3#managing-subscriptions Managing Subscriptions +makeExcerpt('src/app/hero-counter.component.ts', '') -h3#async-pipe Async Pipe: Less is more +h3#async-pipe Async Pipe: Declarative Subscription Management :marked You can manage Observables imperatively through manually subscribing and unsubscribing when needed but you can also manage them declaratively in our templates using the `Async Pipe`. The async pipe can also take care of our Subscription management, as it can take an Observable or a Promise, listen for its emitted values and will destroy its subscriptions - with the disposing of the component. This allows us to use Observables with less boilerplate and that's always a good thing. + with the disposing of the component. This allows us to use Observables with less boilerplate and that's a good thing. You will create another component that displays a list of heroes using these two options. Our component will retrieve a list of Heroes from our `HeroService` and subscribe to set them to a variable in the component. @@ -186,10 +186,17 @@ h3#async-pipe Async Pipe: Less is more Here you are only assigning the `heroes` value to bind it to our template. The `Async Pipe` lets us skip the manual subscription, as it will handle this for you. The updated template is below. -+makeExcerpt('src/app/hero-list.component.ts (async pipe)', '') ++makeExcerpt('src/app/hero-list.component.2.ts (async pipe)', 'async-pipe') + +:marked + You will also update the `heroes` variable and name it `heroes$`, with the **$** denoting that its an Observable value. Its also + necessary to update the type from `Hero[]` to `Observable` since the Observable is being passed directly to the template. + ++makeExcerpt('src/app/hero-list.component.2.ts (observable heroes)', 'observable-heroes') +:marked When your component is rendered, the async pipe will subscribe to the Observable to listen for emitted values. Once the values - are produced it will wire those values to the same `ngFor` directive. If you were to initiate another sequence of heroes + are produced it will bind those values to the same `ngFor` directive. If you were to initiate another sequence of heroes the pipe would handle updated the retrieved values along with destroying the Observable subscription once the component is destroyed. h3#sharing-data Sharing data with a stream @@ -232,8 +239,6 @@ h3#sharing-data Sharing data with a stream As with other services, you'll import the `EventAggregatorService` and `MessageLogComponent` and add it to the `AppModule` providers and declarations arrays respectively. -+makeExcerpt('src/app/app.module.ts', '') - :marked To see your message bus in action, you'll import and inject the `EventAggregatorService` in the `AppComponent` and add an event when the Application starts and add the `message-log` component to the `AppComponent` template. @@ -245,58 +250,50 @@ h3#error-handling Error Handling As often as you strive for perfect conditions, errors will happen. Servers go down, invalid data is sent and other issues cause errors to happen when processing data. While you can do your best to prevent these errors, its also wise to be ready for them when they do happen. The scenario this is most likely to happen in is when you're making data requests to an external API. This is a common task done with the Angular HTTP client. - The HTTP client provides methods that return requests as Observables, which in turn can handle errors. Now you may wonder why you would want - to use an Observable when HTTP requests are usually a one-and-done operation, but an Observable provides robust error handling that wouldn't be - able to easily do with a Promise. Let's simulate a failed request in your in your `HeroListComponent` when retrieving heroes from the `HeroService`. + The HTTP client provides methods that return requests as Observables, which in turn can handle errors. Let's simulate a failed request in your in the `HeroService`. -+makeExcerpt('src/app/hero.service.1.ts (failed heroes)', '') ++makeExcerpt('src/app/hero.service.2.ts (failed heroes)', 'getHeroes-failed') :marked - Now you can update your HeroListComponent to make the failed request. + This is what the `HeroListComponent` currently looks like with no error handling and the simulated error. -+makeExcerpt('src/app/hero-list.component.2.ts (failed heroes)', 'failed-heroes') ++makeExcerpt('src/app/hero-list.component.3.ts (failed heroes)', '') :marked - This is what the `HeroListComponent` currently looks like with no error handling. - With this current setup, you have no way to recover and that's less than ideal. So let's add some error handling with the `catch` operator. You don't - need to patch the Observable to use the `catch` operator, as its in the basic set of included operators. + With this current setup, you have no way to recover and that's less than ideal. So let's add some error handling with the `catch` operator. You need + to import the `catch` operator. The `catch` operator will continue the observable sequence even after an exception occurs. Since you know that each + Observable operator returns a new Observable, you can use this to return an empty array or even a new Observable HTTP request. You'll also import the `of` operator, which lets you create an Observable sequence from a list of arguments. In this case, you're returning an empty array of `Heroes` when an error occurs. -+makeExcerpt('src/app/hero-list.component.2.ts (catch and return)', '') ++makeExcerpt('src/app/hero.service.2.ts (catch and return)', '') :marked - Now we have a path of recovery. The `catch` operator will continue the observable sequence even after an exception occurs. Since you know that each - Observable operator returns a new Observable, you can use this to return an empty array or even a new Observable HTTP request. + Now we have a path of recovery. When the `getHeroes` request is made and fails, an error notification is produced, which will be handled + in the `catch` operation. This error handling is simplified, so returning an Observable with an empty array will suffice. h3#retry Retry Failed Observable :marked This is a simple path of recovery, but we can go further. What if you also wanted to _retry_ a failed request? With Observables, this is as easy as adding a new operator, - aptly named `retry`. + aptly named `retry`. If you've ever done this with a Promise, its definitely not a painless operation. Of course you'll need to import the operator first. -+makeExcerpt('src/app/hero-list.component.3.ts (retry operator)', 'retry-operator') ++makeExcerpt('src/app/hero.service.3.ts (retry operator)', 'retry-import') :marked You can add the `retry` operator to the Observable sequence. The retry operator takes an argument of the number of times you want to retry the sequence before completing. -+makeExcerpt('src/app/hero-list.component.ts', '') ++makeExcerpt('src/app/hero.service.3.ts', '') :marked - Now your request will be attempted 3 times before giving up and going into the error sequence. - -// h3#retry-when Conditional Retry -// :marked - You've seen how to catch an error, how to retry an error a certain number of times, but you can also retry conditionally. This is useful - when you only want to retry under certain conditions like when a request returns a 500 error. This type of error would be out of your control - and could resolve itself quickly. The `retryWhen` operator does this seamlessly. You wouldn't use this on a login request for fear of being locked - out but for retrieving list of data, it can be very useful. + The `retry` operator will re-subscribe to the source Observable, in this case is the Observable returned by the `http.get` method. Instead of failing on the + first error produced by the Observable, now the request will be attempted 3 times before giving up and going into the error sequence. -// +makeExcerpt('src/app/api-error-handler.service.ts', '') +// TODO Diagram for retry sequence -// h3#framework-apis Framework APIs: Angular-provided streams +h3#framework-apis Framework APIs: Angular-provided Observables // :marked Angular makes use of Observables internally and externally through its APIs template syntax using the Async pipe, user input with Reactive or Model-Driven Forms, making external requests using Http and route information with the Router. From 5438569542a1d824c343f1675c4db1dd07505baf Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 19 Feb 2017 21:10:59 -0600 Subject: [PATCH 09/13] Added async form validation example for framework apis --- .../rxjs/ts/src/app/add-hero.component.html | 14 ++++ .../rxjs/ts/src/app/add-hero.component.ts | 83 +++++++++++++++++++ .../rxjs/ts/src/app/app-routing.module.ts | 2 + .../rxjs/ts/src/app/app.component.ts | 1 + .../_examples/rxjs/ts/src/app/app.module.ts | 4 +- .../rxjs/ts/src/app/hero-counter.component.ts | 8 +- .../rxjs/ts/src/app/hero.service.1.ts | 7 +- .../rxjs/ts/src/app/hero.service.2.ts | 2 +- .../rxjs/ts/src/app/hero.service.3.ts | 7 +- .../rxjs/ts/src/app/hero.service.4.ts | 50 +++++++++++ .../_examples/rxjs/ts/src/app/hero.service.ts | 12 +++ public/docs/ts/latest/guide/rxjs.jade | 9 +- 12 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.html create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html new file mode 100644 index 0000000000..77cd440258 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html @@ -0,0 +1,14 @@ +

ADD HERO

+
+

+ *Name:
+ Name is required + Checking if name is already taken + Hero name is already taken +

+

+ +

+
+ +The hero has been added diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts new file mode 100644 index 0000000000..d393bdea2a --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts @@ -0,0 +1,83 @@ +// #docplaster +// #docregion +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/take'; +import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +import { EventAggregatorService } from './event-aggregator.service'; +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + templateUrl: 'add-hero.component.html', + styles: [ '.error { color: red }' ] +}) +export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChild('heroName', { read: ElementRef }) heroName: ElementRef; + + form: FormGroup; + onDestroy$ = new Subject(); + showErrors: boolean = false; + success: boolean; + + constructor( + private formBuilder: FormBuilder, + private heroService: HeroService, + private eventService: EventAggregatorService + ) {} + + ngOnInit() { + this.form = this.formBuilder.group({ + name: ['', [Validators.required], [(control: FormControl) => { + return this.checkHeroName(control.value); + }]] + }); + } + + checkHeroName(name: string) { + return Observable.of(name) + .switchMap(heroName => this.heroService.isNameAvailable(heroName)) + .map(available => available ? null : { taken: true }); + } + + ngAfterViewInit() { + const controlBlur$ = Observable.fromEvent(this.heroName.nativeElement, 'blur'); + + Observable.merge( + this.form.valueChanges, + controlBlur$ + ) + .debounceTime(300) + .takeUntil(this.onDestroy$) + .subscribe(() => this.checkErrors()); + } + + checkErrors() { + if (!this.form.valid) { + this.showErrors = true; + } + } + + save(model: any) { + this.heroService.addHero(model.name) + .subscribe(() => { + this.success = true; + this.eventService.add({ + type: 'hero', + message: 'Hero Added' + }); + }); + } + + ngOnDestroy() { + this.onDestroy$.complete(); + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts b/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts index 6a043fd0c3..5c72b75b88 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts @@ -4,8 +4,10 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { HeroCounterComponent } from './hero-counter.component'; +import { AddHeroComponent } from './add-hero.component'; const appRoutes: Routes = [ + { path: 'hero/add', component: AddHeroComponent }, { path: 'hero/counter', component: HeroCounterComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, diff --git a/public/docs/_examples/rxjs/ts/src/app/app.component.ts b/public/docs/_examples/rxjs/ts/src/app/app.component.ts index c73ed45382..68b65cdd1c 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app.component.ts @@ -8,6 +8,7 @@ import { EventAggregatorService } from './event-aggregator.service'; template: `

RxJS in Angular

+ Add Hero
Heroes
Hero Counter
diff --git a/public/docs/_examples/rxjs/ts/src/app/app.module.ts b/public/docs/_examples/rxjs/ts/src/app/app.module.ts index 8d80d5256c..eb0724b3d4 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app.module.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app.module.ts @@ -10,6 +10,7 @@ import { HeroListComponent } from './hero-list.component'; import { HeroCounterComponent } from './hero-counter.component'; import { MessageLogComponent } from './message-log.component'; import { LoadingComponent } from './loading.component'; +import { AddHeroComponent } from './add-hero.component'; import { LoadingService } from './loading.service'; import { HeroService } from './hero.service'; @@ -35,7 +36,8 @@ import { InMemoryDataService } from './in-memory-data.service'; HeroCounterComponent, HeroListComponent, MessageLogComponent, - LoadingComponent + LoadingComponent, + AddHeroComponent ], providers: [ HeroService, diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts index 6fcce2f117..6ecd9edb2c 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts @@ -6,7 +6,6 @@ import 'rxjs/add/operator/takeUntil'; import { Component, OnInit, OnDestroy } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Observer } from 'rxjs/Observer'; -import { Subscription } from 'rxjs/Subscription'; // #docregion import-subject import { Subject } from 'rxjs/Subject'; @@ -24,7 +23,6 @@ import { Subject } from 'rxjs/Subject'; export class HeroCounterComponent implements OnInit, OnDestroy { count: number = 0; counter$: Observable; - sub: Subscription; // #docregion onDestroy-subject onDestroy$ = new Subject(); @@ -37,15 +35,15 @@ export class HeroCounterComponent implements OnInit, OnDestroy { }, 1000); }); - let counter1Sub = this.counter$ + this.counter$ .takeUntil(this.onDestroy$) .subscribe(); - let counter2Sub = this.counter$ + this.counter$ .takeUntil(this.onDestroy$) .subscribe(); - let counter3Sub = this.counter$ + this.counter$ .takeUntil(this.onDestroy$) .subscribe(); } diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts index ceba7871e7..977a266ed9 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts @@ -1,15 +1,14 @@ // #docplaster // #docregion import 'rxjs/add/operator/map'; -import { Injectable } from '@angular/core'; -import { Http, Response, Headers } from '@angular/http'; -import { Observable } from 'rxjs/Observable'; +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; import { Hero } from './hero'; @Injectable() export class HeroService { - private headers = new Headers({'Content-Type': 'application/json'}); private heroesUrl = 'api/heroes'; constructor( diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts index 7b439931e3..6157d3c428 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.2.ts @@ -4,7 +4,7 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/catch'; import { Injectable } from '@angular/core'; -import { Http, Headers } from '@angular/http'; +import { Http } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Hero } from './hero'; diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts index 7752f0c70d..28714c0e7e 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts @@ -6,15 +6,14 @@ import 'rxjs/add/operator/catch'; // #docregion retry-import import 'rxjs/add/operator/retry'; // #enddocregion retry-import -import { Injectable } from '@angular/core'; -import { Http, Response, Headers } from '@angular/http'; -import { Observable } from 'rxjs/Observable'; +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; import { Hero } from './hero'; @Injectable() export class HeroService { - private headers = new Headers({'Content-Type': 'application/json'}); private heroesUrl = 'api/heroes'; constructor( diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts new file mode 100644 index 0000000000..3c62e6575f --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts @@ -0,0 +1,50 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/map'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/catch'; +// #docregion retry-import +import 'rxjs/add/operator/retry'; +// #enddocregion retry-import +import { Injectable } from '@angular/core'; +import { Http, Response, Headers } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroService { + private headers = new Headers({'Content-Type': 'application/json'}); + private heroesUrl = 'api/heroes'; + + constructor( + private http: Http + ) {} + + // #docregion getHeroes-failed + getHeroes(fail?: boolean): Observable { + return this.http.get(`${this.heroesUrl}${fail ? '/failed' : ''}`) + // #enddocregion getHeroes-failed + .retry(3) + .map(response => response.json().data as Hero[]) + // #enddocregion getHeroes-failed + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + // #docregion getHeroes-failed + } + + addHero(name: string): Observable { + return this.http + .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}); + } + + isNameAvailable(name: string): Observable { + return this.http + .get(`api/heroes/?name=${name}`) + .map(response => response.json().data) + .map(heroes => heroes.length === 0); + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts index 7752f0c70d..dd7be4cf7a 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts @@ -35,4 +35,16 @@ export class HeroService { }); // #docregion getHeroes-failed } + + addHero(name: string): Observable { + return this.http + .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}); + } + + isNameAvailable(name: string): Observable { + return this.http + .get(`app/heroes/?name=${name}`) + .map(response => response.json().data) + .map(heroes => !heroes.find((hero: Hero) => hero.name === name)); + } } diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index 0ae5c9deea..d31703df06 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -294,8 +294,13 @@ h3#retry Retry Failed Observable // TODO Diagram for retry sequence h3#framework-apis Framework APIs: Angular-provided Observables -// :marked - Angular makes use of Observables internally and externally through its APIs template syntax using the Async pipe, user input +:marked + Angular makes extensive use of Observables internally and externally through its APIs to provide you + with built-in streams to use in your Angular application. Along with the `Async Pipe`, and the HTTP Client, + observable streams are made available through Reactive Forms, the Router and View Querying APIs. + +//:marked + are provided template syntax using the Async pipe, user input with Reactive or Model-Driven Forms, making external requests using Http and route information with the Router. By using Observables underneath, these APIs provide a consistent way for you to use and become more comfortable with using Observables in your application to handle your streams of data that are produced over time. From 538cd0abb3510d9766ab715669a300bd52bbbfc7 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Wed, 22 Feb 2017 21:52:56 -0600 Subject: [PATCH 10/13] Added writeup for stream integration --- .../rxjs/ts/src/app/add-hero.component.1.html | 14 ++ .../rxjs/ts/src/app/add-hero.component.1.ts | 32 +++++ .../rxjs/ts/src/app/add-hero.component.2.ts | 40 ++++++ .../rxjs/ts/src/app/add-hero.component.html | 4 +- .../rxjs/ts/src/app/add-hero.component.ts | 4 +- .../rxjs/ts/src/app/hero.service.4.ts | 9 +- .../_examples/rxjs/ts/src/app/hero.service.ts | 4 +- public/docs/ts/latest/guide/rxjs.jade | 122 ++++++------------ 8 files changed, 130 insertions(+), 99 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.html create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.html b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.html new file mode 100644 index 0000000000..de0f5af313 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.html @@ -0,0 +1,14 @@ + +

ADD HERO

+ +
+

+ Name:
+ Name is required +

+

+ +

+
+ +The hero has been added diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.ts new file mode 100644 index 0000000000..2996af0ec7 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.1.ts @@ -0,0 +1,32 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + templateUrl: './add-hero.component.html', + styles: [ '.error { color: red }' ] +}) +export class AddHeroComponent implements OnInit { + form: FormGroup; + showErrors: boolean = false; + success: boolean; + + constructor( + private formBuilder: FormBuilder, + private heroService: HeroService + ) {} + + ngOnInit() { + this.form = this.formBuilder.group({ + name: ['', [Validators.required]] + }); + } + + save(model: any) { + // TODO: Save hero + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts new file mode 100644 index 0000000000..b855937b99 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts @@ -0,0 +1,40 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/observable/merge'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { Hero } from './hero'; +import { HeroService } from './hero.service'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Observable'; + +@Component({ + moduleId: module.id, + templateUrl: './add-hero.component.html', + styles: [ '.error { color: red }' ] +}) +export class AddHeroComponent implements OnInit, OnDestroy { + form: FormGroup; + showErrors: boolean = false; + success: boolean; + + constructor( + private formBuilder: FormBuilder, + private heroService: HeroService + ) {} + + ngOnInit() { + this.form = this.formBuilder.group({ + name: ['', [Validators.required]] + }); + } + + save(model: Hero) { + this.heroService.addHero(model.name) + .subscribe(() => { + this.success = true; + }); + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html index 77cd440258..d148361e8a 100644 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html @@ -1,4 +1,6 @@ +

ADD HERO

+

*Name:
@@ -7,7 +9,7 @@

ADD HERO

Hero name is already taken

- +

diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts index d393bdea2a..4eb1d5d433 100644 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts @@ -6,7 +6,7 @@ import 'rxjs/add/observable/merge'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/switchMap'; -import 'rxjs/add/operator/take'; +import 'rxjs/add/operator/takeUntil'; import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; import { Observable } from 'rxjs/Observable'; @@ -24,9 +24,9 @@ export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { @ViewChild('heroName', { read: ElementRef }) heroName: ElementRef; form: FormGroup; - onDestroy$ = new Subject(); showErrors: boolean = false; success: boolean; + onDestroy$ = new Subject(); constructor( private formBuilder: FormBuilder, diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts index 3c62e6575f..3e5c6e9d5d 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts @@ -38,13 +38,6 @@ export class HeroService { addHero(name: string): Observable { return this.http - .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}); - } - - isNameAvailable(name: string): Observable { - return this.http - .get(`api/heroes/?name=${name}`) - .map(response => response.json().data) - .map(heroes => heroes.length === 0); + .post(this.heroesUrl, { name }, {headers: this.headers}); } } diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts index dd7be4cf7a..c04ce1ab86 100644 --- a/public/docs/_examples/rxjs/ts/src/app/hero.service.ts +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.ts @@ -38,12 +38,12 @@ export class HeroService { addHero(name: string): Observable { return this.http - .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}); + .post(this.heroesUrl, { name }, {headers: this.headers}); } isNameAvailable(name: string): Observable { return this.http - .get(`app/heroes/?name=${name}`) + .get(`${this.heroesUrl}/?name=${name}`) .map(response => response.json().data) .map(heroes => !heroes.find((hero: Hero) => hero.name === name)); } diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index d31703df06..6ac90259b1 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -21,7 +21,7 @@ block includes * [Sharing Data](#sharing-data "") * [Error Handling](#error-handling "") * [Framework APIs](#framework-apis "") - * [Stream Integration](#integration "") + * [Stream Integration](#stream-integration "") * [Further Reading](#further-reading "") h3#definition The Observable: a function at its core @@ -31,6 +31,12 @@ h3#definition The Observable: a function at its core returns a function for cancellation. It represents an action that can be performed. This action may be performed right now, or at some point in the future. An action can be anything, from simply "return a constant", "make an HTTP request" or "navigate to another page". + Angular makes extensive use of Observables internally and externally through its APIs to provide you + with built-in streams to use in your Angular application. These APIs also manage their provided Observables + efficiently. This means you don't need to unsubscribe from these provided Observables as their subscriptions + are managed locally and will be destroyed when your components are destroyed. Observable streams are made available through the HTTP client, + reactive forms, the router and view querying APIs. + Using Observables starts with an understanding of the basic principles, in which there are a wealth of resources that cover these in-depth. You should become more familiar with these principles, as it will help you with how to use these concepts in your Angular application. Below are a few resources to guide you in the concepts of an Observable and reactivity in general. @@ -293,93 +299,37 @@ h3#retry Retry Failed Observable // TODO Diagram for retry sequence -h3#framework-apis Framework APIs: Angular-provided Observables +h3#stream-integration Stream Integration :marked - Angular makes extensive use of Observables internally and externally through its APIs to provide you - with built-in streams to use in your Angular application. Along with the `Async Pipe`, and the HTTP Client, - observable streams are made available through Reactive Forms, the Router and View Querying APIs. - -//:marked - are provided template syntax using the Async pipe, user input - with Reactive or Model-Driven Forms, making external requests using Http and route information with the Router. - By using Observables underneath, these APIs provide a consistent way for you to use and become more comfortable - with using Observables in your application to handle your streams of data that are produced over time. - Another major advantage of the streams provided by Angular is that they are managed for you, - so no need to unsubscribe to clean up subscriptions. - - Http - - Making external requests from our application is a very common action in every application. You make a request, - handle the success or failure of that request and wait for the request to be made again. Sometimes it’s not as - simple as that as you need the be able to retry, cancel or delay requests. Being efficient with requests to save - on data transferred is vital when every byte counts. HTTP requests are not a one-off action as many elements of - your application is driven by external data. The Http client in Angular is built on top of Observables which - provide the ability to handle one single request or multiple requests seamlessly, along with retrying and - cancellations of requests. - - Example: Use hero service to make a request, make it fail to show retries, conditional retry. -// +makeExcerpt('src/app/hero-list.component.1.ts', '') - -// :marked - Async Pipe - - As we’ve talked about previously, Observables must be subscribed to in order to handle the data they produce, - and must be unsubscribed from in order to clean up the resources they have used. You’ve gone through how to - subscribe to an Observable, get its data and provide that data through a variable in your class. There is also - a built-in pipe available for use with template syntax to manage Observable subscriptions called the Async Pipe. - When used in a template, the async pipe will subscribe to the Observable or evaluate a Promise, receive its - emitted values and dispose of its subscription once the component is destroyed. The pipe also ties into Change Detection, - so that when new values are produced, the component is marked for detection in order to determine whether it’s changes - need to be reflected in your user interface. This is useful as it reduces the amount of boilerplate you need when setting - up data to be fetched and provided to your template. We’ll learn later about cases where using an async pipe is beneficial - versus managing your own subscription manually. - - Example: Fetching heroes using a service, subscribing/unsubscribing manually then removing the subscription and delegating responsibility to the async pipe. Another example would be showing hero details with multiple async pipes, but instead using a single subscription. -// +makeExcerpt('src/app/hero-detail.component.ts', '') - -// :marked - Forms - With many applications, user input is required to initiate or complete an action. Whether it be logging in or filling out - an information page, user data is another stream that needs to be handled. In contrast to template-driven forms where the - responsibility is on the developer to gather the pieces of data from the form, the model-driven/Reactive forms uses - Observables to easily provide a continuous stream of user input. By using the reactive approach, our form will be ready - to handle user input streams from form fields, as well as provide that form data seamlessly to another stream for - processing. - - Example: Simple form that displays form status/value changes over time. Also displays creating an Observable from an existing event. valueChanges on individual field/entire form - - Router - - The browser URL is another stream of information. It provides you with a canonical link to the application view you are - displaying at any given moment, along with information about what data to display. The Angular Router uses the browser URL - to provide you with multiple streams that hook into the navigation process, URL data provided through your route - configuration, parameters provided for context and path information. These pieces of data are provided by the Router - through Observables, since we are certain that these streams happen in a continuous fashion as a user navigates throughout - your Angular application. - - Router Observables: Events, Parameters, Data, URL Segments - - Example: Use router events to build a small loading service and component. -// +makeExcerpt('src/app/loading.service.ts', '') -// +makeExcerpt('src/app/loading.component.ts', '') - -// h3#integration Stream Integration -// :marked - The Observables provided by these areas can be used together. The same set of functionality and extensibility can be combined together - in a very powerful and practical way. A prime example is a hero search component that implements a typeahead search feature. - Let’s start by gathering some requirements about what your typeahead will need. - - * Take input from the user - * Make a search based on that input - * Only search when the user has changed the search terms - * Cancel any in-progress requests if you user initiates a new search - * Only make a request after the user hasn’t interacted within a certain time frame - * Display search results - * Sync the user’s search terms in the browser URL - - Example: Hero search typeahead component -// +makeExcerpt('src/app/hero-search.component.ts', '') + Knowing Angular provides multiple Observables through different APIs is good, but putting those + streams together in a valuable way is what you will be striving for. With a consistent interface + provided by Observables, its easy to combine streams together. Let's look at building + a form to add a Hero to your existing list. When adding a Hero, you'll want to check to see if the hero + name is available before adding the hero, as well as checking validation while the form is being filled out. + These definitely sound like streams of data we can tap into. + + Let's start by adding a hero form component that uses a `Reactive Form`. You'll begin with a simple + form template to enter and save the new hero. + ++makeExcerpt('src/app/add-hero.component.1.ts (hero form component)', '') + +:marked + The hero form template is just getting started also. + ++makeExcerpt('src/app/add-hero.component.1.html (hero form template)', '') + +:marked + You'll need to add a new method to the `HeroService` for adding a new hero. As mentioned earlier, the HTTP client returns an Observable + `Response` that you can use the process the request and operate on if needed. You'll add this request to the `AddHeroComponent` when + ready to save the hero data. + ++makeExcerpt('src/app/hero.service.4.ts (add hero)', '') +:marked + If you look at the template closer, you'll see the `showErrors` boolean, which hides the error messages until you're ready to display them. + A good form waits until the user has interacted with the fields before displaying any errors, and you'll want to follow that same rule. So + how can you display errors once an interaction has happened? Reactive form controls provide an Observable stream of `valueChanges` whenever + the form input changes and we can subscribe to that. Let's add h3#further-reading Further Reading :marked TODO From 2b17174e84bb0da205570ba71f0db572bdabd443 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Wed, 1 Mar 2017 22:27:47 -0600 Subject: [PATCH 11/13] Added stream integration section --- .../rxjs/ts/src/app/add-hero.component.2.html | 14 ++++ .../rxjs/ts/src/app/add-hero.component.2.ts | 40 +++++++++- .../rxjs/ts/src/app/add-hero.component.3.ts | 75 +++++++++++++++++++ .../rxjs/ts/src/app/add-hero.component.ts | 4 +- .../ts/src/app/event-aggregator.service.ts | 23 ++---- public/docs/ts/latest/guide/rxjs.jade | 54 +++++++++++-- 6 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.html create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.html b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.html new file mode 100644 index 0000000000..d70ec06052 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.html @@ -0,0 +1,14 @@ + +

ADD HERO

+ +
+

+ Name:
+ Name is required +

+

+ +

+
+ +The hero has been added diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts index b855937b99..b67ee9d8d5 100644 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts @@ -1,24 +1,37 @@ // #docplaster // #docregion +// #docregion rxjs-imports-1 import 'rxjs/add/operator/takeUntil'; import 'rxjs/add/observable/merge'; -import { Component, OnInit, OnDestroy } from '@angular/core'; +import 'rxjs/add/observable/fromEvent'; +// #enddocregion rxjs-imports-1 +// #docregion viewchild-imports +import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; +// #enddocregion viewchild-imports import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Hero } from './hero'; import { HeroService } from './hero.service'; + +// #docregion rxjs-imports-2 import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +// #enddocregion rxjs-imports-2 +// #docregion viewchild-heroName @Component({ moduleId: module.id, templateUrl: './add-hero.component.html', styles: [ '.error { color: red }' ] }) -export class AddHeroComponent implements OnInit, OnDestroy { +export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChild('heroName', { read: ElementRef }) heroName: ElementRef; +// #enddocregion viewchild-heroName + form: FormGroup; showErrors: boolean = false; success: boolean; + onDestroy$ = new Subject(); constructor( private formBuilder: FormBuilder, @@ -30,6 +43,27 @@ export class AddHeroComponent implements OnInit, OnDestroy { name: ['', [Validators.required]] }); } +// #docregion observable-event + ngAfterViewInit() { + const controlBlur$: Observable = Observable.fromEvent(this.heroName.nativeElement, 'blur'); + + Observable.merge( + controlBlur$ + ) + .takeUntil(this.onDestroy$) + .subscribe(() => this.checkForm()); + } + + checkForm() { + if (!this.form.valid) { + this.showErrors = true; + } + } +// #enddocregion observable-event + + ngOnDestroy() { + this.onDestroy$.complete(); + } save(model: Hero) { this.heroService.addHero(model.name) diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts new file mode 100644 index 0000000000..81a83145da --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts @@ -0,0 +1,75 @@ +// #docplaster +// #docregion +// #docregion rxjs-imports-1 +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/observable/fromEvent'; +// #enddocregion rxjs-imports-1 +// #docregion viewchild-imports +import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; +// #enddocregion viewchild-imports +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { Hero } from './hero'; +import { HeroService } from './hero.service'; + +// #docregion rxjs-imports-2 +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +// #enddocregion rxjs-imports-2 + +// #docregion viewchild-heroName +@Component({ + moduleId: module.id, + templateUrl: './add-hero.component.html', + styles: [ '.error { color: red }' ] +}) +export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChild('heroName', { read: ElementRef }) heroName: ElementRef; +// #enddocregion viewchild-heroName + + form: FormGroup; + showErrors: boolean = false; + success: boolean; + onDestroy$ = new Subject(); + + constructor( + private formBuilder: FormBuilder, + private heroService: HeroService + ) {} + + ngOnInit() { + this.form = this.formBuilder.group({ + name: ['', [Validators.required]] + }); + } +// #docregion value-changes + ngAfterViewInit() { + const controlBlur$: Observable = Observable.fromEvent(this.heroName.nativeElement, 'blur'); + + Observable.merge( + controlBlur$, + this.form.get('name').valueChanges + ) + .takeUntil(this.onDestroy$) + .subscribe(() => this.checkForm()); + } + + checkForm() { + if (!this.form.valid) { + this.showErrors = true; + } + } +// #enddocregion value-changes + + ngOnDestroy() { + this.onDestroy$.complete(); + } + + save(model: Hero) { + this.heroService.addHero(model.name) + .subscribe(() => { + this.success = true; + }); + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts index 4eb1d5d433..6d05eea4a5 100644 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts @@ -57,10 +57,10 @@ export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { ) .debounceTime(300) .takeUntil(this.onDestroy$) - .subscribe(() => this.checkErrors()); + .subscribe(() => this.checkForm()); } - checkErrors() { + checkForm() { if (!this.form.valid) { this.showErrors = true; } diff --git a/public/docs/_examples/rxjs/ts/src/app/event-aggregator.service.ts b/public/docs/_examples/rxjs/ts/src/app/event-aggregator.service.ts index d8cb7b0369..fa972939f1 100644 --- a/public/docs/_examples/rxjs/ts/src/app/event-aggregator.service.ts +++ b/public/docs/_examples/rxjs/ts/src/app/event-aggregator.service.ts @@ -1,6 +1,7 @@ // #docplaster // #docregion // #docregion imports +import 'rxjs/add/operator/scan'; import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; // #enddocregion imports @@ -14,25 +15,11 @@ export interface AppEvent { @Injectable() export class EventAggregatorService { - _events: AppEvent[] = []; - events$: BehaviorSubject; - - constructor() { - this._events = []; - this.events$ = new BehaviorSubject(this._events); - } + _events$: BehaviorSubject = new BehaviorSubject([]); + events$ = this._events$ + .scan((events, event) => events.concat(event), []); add(event: AppEvent) { - this._events.push(event); - this.notify(); - } - - clear() { - this._events = []; - this.notify(); - } - - notify() { - this.events$.next(this._events); + this._events$.next([event]); } } diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index 6ac90259b1..7c4f25d91f 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -314,7 +314,7 @@ h3#stream-integration Stream Integration +makeExcerpt('src/app/add-hero.component.1.ts (hero form component)', '') :marked - The hero form template is just getting started also. + And the hero form template. +makeExcerpt('src/app/add-hero.component.1.html (hero form template)', '') @@ -326,10 +326,54 @@ h3#stream-integration Stream Integration +makeExcerpt('src/app/hero.service.4.ts (add hero)', '') :marked - If you look at the template closer, you'll see the `showErrors` boolean, which hides the error messages until you're ready to display them. + If you look at the template, you'll see the `showErrors` boolean, which hides the error messages until you're ready to display them. A good form waits until the user has interacted with the fields before displaying any errors, and you'll want to follow that same rule. So - how can you display errors once an interaction has happened? Reactive form controls provide an Observable stream of `valueChanges` whenever - the form input changes and we can subscribe to that. Let's add + how can you display errors once an interaction has happened? Interaction on the input can be as simple as entering the field + and leaving the field, also known as the blur event. Observables can be created from existing events. You'll use the `fromEvent` + operator to create an Observable from the existing `blur` event on the hero name input field. + + In order to access the input field, you'll need to add a template reference to to the element. The `heroName` template reference will + give us access to the input field in the component class. The updated template is as follows: + ++makeExcerpt('src/app/add-hero.component.2.html (heroName template reference)', '') + +:marked + Now that you can access the template reference, you'll need to import the `ViewChild` decorator, the `ElementRef` type + and the `AfterViewInit` lifecycle hook. + ++makeExcerpt('src/app/add-hero.component.2.ts (ViewChild imports)', 'viewchild-imports') + +:marked + You'll use the `ViewChild` decorator to target the `heroName` template reference in the component assigned to + the `ElementRef` type. + ++makeExcerpt('src/app/add-hero.component.2.ts (ViewChild ElementRef)', 'viewchild-heroName') + +:marked + As usual, you'll need to import a few instance and static operators to create the Observable event. As + previously mentioned, you'll use the `takeUntil` operator to clean up any Observable streams once the component + is destroyed. In order to create an Observable from an element event, the `fromEvent` observable creation operator + is needed. The `fromEvent` let's you create a stream from existing events emitted by elements. An additional operator + is the `merge` creation operator, which combines multiple streams together + ++makeExcerpt('src/app/add-hero.component.2.ts (rxjs imports)', 'rxjs-imports-1') + +:marked + In order to use the `ViewChild`, you'll need to implement the `AfterViewInit` interface and the `ngAfterViewInit` + lifecycle hook. The `Observable.merge` let's you compose multiple observables and will emit when any of the source + Observables emit a value without waiting for each one. You'll subscribe to the Observable and check the validity of + the form in order to show errors. Now when the user triggers the `blur` event on the input the errors will be displayed. + ++makeExcerpt('src/app/add-hero.component.2.ts (Observable fromEvent)', '') + +:marked + Since you can compose multiple streams using `Observable.merge`, you can easily add additional streams to trigger + the validation check. Since reactive forms provide an Observable of `valueChanges`, you can listen for value changes + from the `name` field and display errors once the user inputs some data also. You can easily access this through the reactive + forms getter. Update the merged observables to include the name valueChanges. + ++makeExcerpt('src/app/add-hero.component.3.ts (Observable valueChanges)', 'value-changes') + h3#further-reading Further Reading :marked - TODO + // TODO link some resources From a1219587f95bbcd4da94dad194c7ac9945671a7a Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Mon, 6 Mar 2017 16:24:26 -0800 Subject: [PATCH 12/13] docs(rxjs): [WIP] Ward's contributions --- .../rxjs/ts/src/app/app.component.ts | 8 +- .../_examples/rxjs/ts/src/app/app.module.ts | 2 + .../rxjs/ts/src/app/observable-principles.ts | 133 +++++++ public/docs/_examples/rxjs/ts/src/heroes.json | 12 + public/docs/_examples/rxjs/ts/src/main.ts | 6 +- public/docs/ts/latest/guide/_data.json | 2 +- public/docs/ts/latest/guide/change-log.jade | 3 + public/docs/ts/latest/guide/rxjs.jade | 332 ++++++++++++++---- 8 files changed, 435 insertions(+), 63 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/src/app/observable-principles.ts create mode 100644 public/docs/_examples/rxjs/ts/src/heroes.json diff --git a/public/docs/_examples/rxjs/ts/src/app/app.component.ts b/public/docs/_examples/rxjs/ts/src/app/app.component.ts index 68b65cdd1c..16bff0416e 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app.component.ts @@ -2,6 +2,7 @@ // #docregion import { Component, OnInit } from '@angular/core'; import { EventAggregatorService } from './event-aggregator.service'; +import { ObservablePrinciples } from './observable-principles'; @Component({ selector: 'my-app', @@ -19,12 +20,17 @@ import { EventAggregatorService } from './event-aggregator.service'; ` }) export class AppComponent implements OnInit { - constructor(private eventService: EventAggregatorService) {} + constructor( + private eventService: EventAggregatorService, + private principles: ObservablePrinciples) {} ngOnInit() { this.eventService.add({ type: 'init', message: 'Application Initialized' }); + + this.principles.callFunctionalExamples(); + this.principles.callPromiseExamples(); } } diff --git a/public/docs/_examples/rxjs/ts/src/app/app.module.ts b/public/docs/_examples/rxjs/ts/src/app/app.module.ts index eb0724b3d4..d5571fa2ac 100644 --- a/public/docs/_examples/rxjs/ts/src/app/app.module.ts +++ b/public/docs/_examples/rxjs/ts/src/app/app.module.ts @@ -12,6 +12,7 @@ import { MessageLogComponent } from './message-log.component'; import { LoadingComponent } from './loading.component'; import { AddHeroComponent } from './add-hero.component'; +import { ObservablePrinciples } from './observable-principles'; import { LoadingService } from './loading.service'; import { HeroService } from './hero.service'; @@ -40,6 +41,7 @@ import { InMemoryDataService } from './in-memory-data.service'; AddHeroComponent ], providers: [ + ObservablePrinciples, HeroService, LoadingService, EventAggregatorService diff --git a/public/docs/_examples/rxjs/ts/src/app/observable-principles.ts b/public/docs/_examples/rxjs/ts/src/app/observable-principles.ts new file mode 100644 index 0000000000..606fb2140f --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/observable-principles.ts @@ -0,0 +1,133 @@ +// Demonstrate Observable principles discussed in the doc +// #docplaster +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; + +import { Observable } from 'rxjs/Observable'; + +import 'rxjs/add/observable/fromPromise'; +import 'rxjs/add/observable/interval'; + +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/take'; +import 'rxjs/add/operator/toPromise'; + +import { Hero } from './hero'; +import { InMemoryDataService } from './in-memory-data.service'; +import { EventAggregatorService } from './event-aggregator.service'; + + +@Injectable() +export class ObservablePrinciples { + private heroesUrl = 'api/heroes'; + + constructor( + private http: Http, + private eventService: EventAggregatorService) { } + + functionalArray() { + // #docregion functional-array + // double the odd numbers in the array. + const numbers = [0, 1, 2, 3, 4, 5]; + return numbers.filter(n => n % 2 === 1).map(n => n * 2); + // #enddocregion functional-array + } + + functionalEvents() { + // #docregion functional-events + // double the next odd integer every tick ... forever. + const numbers = Observable.interval(0); + return numbers.filter(n => n % 2 === 1).map(n => n * 2); + // #enddocregion functional-events + } + + /** + * Call the functional array and event example methods + * and write their results to the EventAggregatorService + * for display in AppComponent. + */ + callFunctionalExamples() { + + this.eventService.add({ + type: 'array', + message: `array of numbers: ${this.functionalArray()}`} + ); + + // Stop after 3 + this.functionalEvents().take(3).subscribe( + result => this.eventService.add({ + type: 'number stream', + message: `stream of numbers: ${result}`} + ) + ); + } + + ///////////////// + + /** + * A `fromPromise` example that converts the `Promise` result + * of the `fetch` API into an Observable of heroes. + */ + fetchHeroes(): Observable { + + // #docregion fromPromise + // JavaScript fetch returns a Promise + let promise = fetch(this.heroesUrl) + .then(resp => resp.json() as Promise) + .then(heroes => { console.log(heroes); return heroes; }); + + // return an Observable + return Observable.fromPromise(promise); + // #enddocregion fromPromise + } + + /** + * A `toPromise` example that converts the `Observable` result + * of the Angular `http` API into a Promise of heroes. + */ + getHeroes(): Promise { + + // #docregion toPromise + // Angular http.get returns an Observable + let observable = this.http.get(this.heroesUrl) + .map(resp => resp.json().data as Hero[]) + .do(heroes => console.log(heroes)); + + // return a Promise + return observable.toPromise(); + // #enddocregion toPromise + } + + /** + * Call the fromPromise and toPromise example methods + * and write their results to the EventAggregatorService + * for display in AppComponent. + */ + callPromiseExamples() { + + this.fetchHeroes() + .subscribe( + heroes => this.eventService.add({type: 'fetch', message: 'fetched heroes'}), + error => this.eventService.add({type: 'fetch', message: 'fetchHeroes failed'}) + ); + + this.getHeroes() + .then( + heroes => this.eventService.add({type: 'get', message: 'got heroes'}), + error => this.eventService.add({type: 'get', message: 'getHeroes failed'}) + ); + } +} + +// Fake the JavaScript fetch API (https://fetch.spec.whatwg.org/) because +// don't want to add another polyfill for browsers that don't support fetch +// and it's not important for this example. +function fetch(url: string) { + const heroes = new InMemoryDataService().createDb().heroes; + const resp = { json: () => Promise.resolve(heroes) as Promise}; + return new Promise(resolve => { + setTimeout(() => resolve(resp), 500); // respond after half second + }); +} diff --git a/public/docs/_examples/rxjs/ts/src/heroes.json b/public/docs/_examples/rxjs/ts/src/heroes.json new file mode 100644 index 0000000000..034d5c1856 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/heroes.json @@ -0,0 +1,12 @@ +[ + {"id": 1, "name": "Mr. Nice"}, + {"id": 2, "name": "Narco"}, + {"id": 3, "name": "Bombasto"}, + {"id": 4, "name": "Celeritas"}, + {"id": 5, "name": "Magneta"}, + {"id": 6, "name": "RubberMan"}, + {"id": 7, "name": "Dynama"}, + {"id": 8, "name": "Dr IQ"}, + {"id": 9, "name": "Magma"}, + {"id": 10, "name": "Tornado"} +] diff --git a/public/docs/_examples/rxjs/ts/src/main.ts b/public/docs/_examples/rxjs/ts/src/main.ts index f332d1d245..a46cd031b6 100644 --- a/public/docs/_examples/rxjs/ts/src/main.ts +++ b/public/docs/_examples/rxjs/ts/src/main.ts @@ -1,6 +1,8 @@ -// #docregion import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; -platformBrowserDynamic().bootstrapModule(AppModule); +// #docregion promise +platformBrowserDynamic().bootstrapModule(AppModule) + .then(() => console.log('The app was bootstrapped.')); +// #enddocregion promise diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index 74a309c2da..1a7d25dda0 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -169,7 +169,7 @@ "rxjs": { "title": "RxJS in Angular", - "intro": "Using Observables to manage application streams." + "intro": "Using Observables to manage asynchronous application events." }, "security": { diff --git a/public/docs/ts/latest/guide/change-log.jade b/public/docs/ts/latest/guide/change-log.jade index aaf5a9d533..d09ca7f3f1 100644 --- a/public/docs/ts/latest/guide/change-log.jade +++ b/public/docs/ts/latest/guide/change-log.jade @@ -5,6 +5,9 @@ block includes The Angular documentation is a living document with continuous improvements. This log calls attention to recent significant changes. + ## NEW: _RxJS in Angular_ guide (2017-03-27) + The new [_Rxjs in Angular_](rxjs.htm) guide explains why and how to use RxJS `Observables` to handle asynchronous application events. + ## All mention of moduleId removed. "Component relative paths" cookbook deleted (2017-03-13) We added a new SystemJS plugin (systemjs-angular-loader.js) to our recommended SystemJS configuration. This plugin dynamically converts "component-relative" paths in templateUrl and styleUrls to "absolute paths" for you. diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index 7c4f25d91f..eff5160be7 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -2,19 +2,29 @@ block includes include ../_util-fns :marked - **Observables** provided by the Reactive Extensions for Javascript (RxJS) library provide applications with an extensive API - for handling asynchronous and event-based values produced over time. + **Observables** are a programming technique for handling asynchronous and event-based values produced over time. + The + Reactive Extensions for Javascript (RxJS) library is a popular, third-party, open source implementation of _Observables_. - An application is made up of many different streams of information. Whether it be user input into - a form, navigating from one route another, making an HTTP request to fetch some data, updating the application view with - new data as its received, or many other examples, each of these events happen over time. Observables provide a interface to - handle the many different sources of events and help to transform these events as they flow throughout an application. + Angular makes extensive use of _observables_ internally and numerous Angular APIs return an `Observable` result. + Many Angular developers create their own _observables_ to handle application events + and facilitate communication among decoupled parts of the application. - This guide will serve as a reference for common uses cases for Observables in an Angular application - and how Observables are used and provided by the Angular framework. + This guide touches briefly on what _observables_ are and how _RxJS_ works before concentrating on common uses cases in Angular applications. +.alert.is-critical + :marked + Somewhere in here we must distinguish RxJS v.4 from RxJS v.5. + It's really confusing but if we don't, they'll never understand why some of the stuff in v.4 is not in v.5 + or behaves differently in v.5. + + Refer to + Migrating from RxJS 4 to 5. + +:marked ## Table of Contents - * [The Observable](#definition "") + * [_Observables_](#definition "") + * [Learning _Observables_](#learning-observables "") * [Observables and Promises](#observables-vs-promises "") * [Using Operators](#operators "") * [Managing Subscriptions](#managing-subscriptions "") @@ -22,58 +32,239 @@ block includes * [Error Handling](#error-handling "") * [Framework APIs](#framework-apis "") * [Stream Integration](#stream-integration "") - * [Further Reading](#further-reading "") -h3#definition The Observable: a function at its core +a#definition +:marked + ## _Observables_ + Applications process streams of events, arriving over the course of a user session. + These stream take many forms. + They include user keystrokes and mouse actions, + navigating from one page to another, + responses from HTTP requests, + and messages exchanged between parts of the application. + + The observable _pattern_ is a functional approach to processing event streams. + It is similar to the functional approach to arrays. + Instead of writing `for...` statements you chain array operators like this. ++makeExcerpt('src/app/observable-principles.ts', 'functional-array') :marked - An Observable, simply put, is a specific type of function with a specific purpose. It’s a function that accepts an `Observer` to produce values and - returns a function for cancellation. It represents an action that can be performed. This action may be performed right now, or at some point - in the future. An action can be anything, from simply "return a constant", "make an HTTP request" or "navigate to another page". + The `Observable` is a functional approach to _streams of events in time_ rather than _arrays of items in space_. - Angular makes extensive use of Observables internally and externally through its APIs to provide you - with built-in streams to use in your Angular application. These APIs also manage their provided Observables - efficiently. This means you don't need to unsubscribe from these provided Observables as their subscriptions - are managed locally and will be destroyed when your components are destroyed. Observable streams are made available through the HTTP client, - reactive forms, the router and view querying APIs. ++makeExcerpt('src/app/observable-principles.ts', 'functional-events') +:marked + The `Observable` _type_ is an _RxJS_ implementation of _observables in JavaScript_. + It conforms to a proposed observable extension to JavaScript and adds many nifty features + including static helper methods like `interval` and a large number of operators such as `filter` and `map`. - Using Observables starts with an understanding of the basic principles, in which there are a wealth of resources that cover these in-depth. You should - become more familiar with these principles, as it will help you with how to use these concepts in your Angular application. Below are a few resources - to guide you in the concepts of an Observable and reactivity in general. +:marked + ### _Observable_ is just a function - * [Learning Observable By Building Observable](https://medium.com/@benlesh/learning-observable-by-building-observable-d5da57405d87#.3lun8dyt7) - * [Thinking Reactively](https://www.youtube.com/watch?v=3LKMwkuK0ZE) - * [RxJS Official Documentation](http://reactivex.io/rxjs/) + At its core, an `Observable` is just a function representing an action that returns one or more events. + An action can be anything: "return a number", "make an HTTP request", "listen for keystrokes", or "navigate to another page". + + The results of an action may be available immediately ("here's the number") + or at some point in the future ("the server responded", "the user hit a key"). -h3#observables-vs-promises Observables and Promises: More different than alike + A new `Observable` takes an `Observer` argument. + The `Observer` is an object with three (optional) notification methods: `next`, `error`, and `complete`. +.alert.is-critical. + Code snippet needed. :marked - RxJS and Observables have been around for a long time, and they aren't the first concept of handling asynchronous events. Before Observables became more prevalent, - the `Promise` was the primary way of handling asynchronous events. Promises and Observables share some similarities as they both handle asynchronous events, - both implement a function to handle execution and error handling, but they are more different then alike. - - ***Promises*** - * Always eagerly evaluated - * Produce a value/error once - * Cannot be composed - * Are always resolved/rejected asynchronously - * Are always multicast to multiple receivers - - ***Observables*** - * Can be evaluated lazily - * Produce multiple values/errors - * Can be composed - * Resolve synchronously/asynchronously - * Multicast when needed using a Subject + When an action produces a value, the `Observable` tells the _observer_ about it by "emitting" the value, a fancy way of saying that it passes the value to the _observer's_ `next` method. - One of the strengths of Observables is producing and handling values over time, which is something a Promise wasn't designed to do. Observables also provide mechanisms - for easy cancellation, retrying upon failure and transformations. Observables include a rich library of operators, along with the extensibility to provide a more powerful - tool to handle the various streams of events in an application. So does this mean Promises are no longer needed? Absolutely not. Promises will continue to serve a purpose as - the right tool for the job in some situations. +.alert.is-critical. + Code snippet needed. +:marked + The `Observable` can tell the _observer_ when things go wrong or the action stops by + calling the _observer_'s `error` and `complete` methods. -h3#operators Operators: Import them and use them + We often say that the `Observer` _subscribes_ to the `Observable` or that the `Observer` is a `Subscriber`. + In fact, an `Observable` has a `subscribe` method that accepts an observer/subscriber object with these three methods. -:marked +.alert.is-critical. + Code snippet needed. +:marked + The `Observable` _function_ returns a _cancellation_ function. + You can call this function to tell the `Observer` to stop producing events and notifications. + Calling this function is also known as "unsubscribing". + +.alert.is-critical. + Code snippet needed. +:marked + The `Observable` is fundamentally that simple. It's fundamentally that wide open. + You can observe any source of events with an `Observable` function and consume those event with this API. + + The real power of `Observable` comes from chaining them together with _**operators**_. + An _operator_ takes a source `Observable`, observes its emitted values, transforms them, and returns a new `Observable` of those transformed values. + + The _RxJS_ library ships with a large number of _operators_ for standard tasks. + The `map` operator, for example, turns an input value into an output value. +.alert.is-critical. + Code snippet needed. +:marked + The `take` operator passes along a specified number of results (it may have to wait for them) before + signaling to the `Observer` that the sequence is complete. +.alert.is-critical. + Code snippet needed. +:marked + That's just two of the many operators you learn as you become acquainted with `Observables`. + +a#learn-observables +:marked + ### Learning about _Observables_ + + There are numererous ways to learn the concepts and details of _Observables_. + Here are a few external resources to get you started: + + * Learning Observable By Building Observable. + * + Practical Guide to Observables in Angular with Rob Wormald (video). + * Thinking Reactively with Ben Lesh (video). + * RxJS Official Documentation. + * + RxJS Operators By Example. + + These links will lead you to many more presentations and videos to expand your knowledge. + + This guide is more narrowly focused on using `Observable` in Angular applications. + +a#observables-vs-promises +:marked + ### _Observables_ and _Promises_ are different + + JavaScript has many asynchronous APIs, including mouse moves, keystrokes, and timers. + You don't block the UI and wait for these events. + You attach a callback function to the event and let the event call your handler + whenever something happens. + Developers quickly understand that an `Observable` is a superior way to manage the flow of events coming from these high-volume sources. + + But some asynchronous sources return at most _one value_. + When you make an HTTP request to the server to fetch or save data, you expect a single response. + + Developers rely on an HTTP client to make such requests and, these days, most HTTP client methods return a `Promise` with a `then` method. + You pass your callback to the `then` method and the `Promise` invokes your callback when the HTTP response arrives. + + The Angular `http` client returns an `Observable` instead. + You consume the `Observable` in a way that looks _superficially_ like a `Promise`. + You supply a callback to the `Observable.subscribe` method rather than to a `then` method. + + The `Observable` and the `Promise` are both techniques for coping with asynchronous processes. + You can use an `Observable` where you'd use a `Promise`. + + The similarity ends there. + An `Observable` is not a `Promise`, + it doesn't want to be a `Promise`, + and you'll be confused and disappointed if you expect an `Observable` to behave like a `Promise`. + + The `Promise` and the `Observable` are more different then alike: + +style. + td, th {vertical-align: top;} + +table(width="100%") + col(width="50%") + col(width="50%") + tr + th Promise + th Observable + tr + td + :marked + A `Promise` resolves to a single result (or error). + td + :marked + An `Observable` can emit any number of events. It may never stop emitting values. + tr + td + :marked + The source of the `Promise` executes immediately. + td + :marked + The `Observable` may emit events immediately ("hot") or wait until the first subscription ("cold"). + tr + td + :marked + The `then` method always executes its callback _asynchronously_. + td + :marked + `Observable` methods and operators may execute _synchronously_ or _asynchronously_. + tr + td + :marked + You cannot _cancel_ or _retry_ the action. + td + :marked + You can _cancel_ or _retry_ the action. + tr + td + :marked + You chain a sequence of promises with the `then` method. + td + :marked + You chain observables with a variety of **operators**. + tr + td + :marked + A `Promise` returns the same result (or error) every time. + + Calling `then` a second time returns the same object as the first time. + It does _not_ re-execute the source of the promised value. + It does _not_ re-execute a `then` callback, + not the last one nor any in a chain of `then` calls. + + In the language of _observables_ this is called "multicasting". + td + :marked + An `Observable` re-executes each time you subscribe to it. + + If the `Observable` initiates the action, as `http.get` does, a second + subscription performs that action again. + Every operator in a chain of _observables_ re-executes its callback. + This is called "single casting". + + You can choose to share the same values with all subscribers ("multicasting") instead + with the help of a `Subject` or a "multicasting" operator such as + `share`, `publish,` or `toPromise`. These operators use a `Subject` internally. + tr + td + :marked + `Promise` is native to JavaScript. + You don't need to import a library although you may need a shim for older browsers. + td + :marked + `Observable` is _not_ part of JavaScript and may never become a part of JavaScript. + Today it requires a third party library such as RxJS and `import` statements for every _observable_ class and operator. +:marked + An `Observable` has a wider range of capabilities and uses than a `Promise`. + It can handle a stream of events; a `Promise` can't. + You can retry the `Observable` action if it fails simply by appending a `retry` operator. + You can't retry a `Promise`. + You can send a cancellation signal to the event producer simply by unsubscribing from the `Observable`. + You can't do that with a `Promise`; you cannot communicate with the event producer through a `Promise`. + + On the other hand, the `Promise` is much simpler. It has a `then` method and that's it. It's always "hot", asynchronous, multicast, and resolves to a single value. + There is no way to _unsubscribe_ and, therefore, no danger in failing to unsubscribe. + + `Promises` aren't bad. They aren't inferior. They're just different. + Angular has APIs that return a `Promise` such as the application bootstrap method: + ++makeExcerpt('src/main.ts', 'promise') + +:marked + The simplicity of a `Promise` is perfectly suited to this use case. + The asynchronous bootstrap action must start immediately, it can't be cancelled, and it has a single outcome. + + You decide, on a case basis, whether and when to use a `Promise` instead of an `Observable`. + It's easy to convert an `Observable` to a `Promise` or from a `Promise` to an `Observable`. + ++makeExcerpt('src/app/observable-principles.ts', 'toPromise') ++makeExcerpt('src/app/observable-principles.ts', 'fromPromise') + +a#operators +:marked + ### Operators: Import them and use them Operators are pure functions that extend the Observable interface, allow you to perform an action against the Observable and return a new Observable. An Observable comes with very few built-in operators and the rest of the operators are added to the Observable on demand. There are multiple approaches to make these operators available for use. @@ -124,9 +315,27 @@ h3#operators Operators: Import them and use them certain feature areas may only make use of certain operators. Importing the operators this way ensures the operators are available regardless of where and when you use them. -h3#managing-subscriptions Managing Subscriptions +a#operator-info +:marked + ### Finding the right operator + + There are several web resources that can help you find the right operator. + * + Operator decision tree to chose operator by use case. + + * "Which Operator do I use?"" (RxJS v4. specific). + + These references describe the operators in RxJS v.4. + Some of the operators have been dropped, renamed, or changed in v.5. + You may need to refer to "Migrating from RxJS 4 to 5". + See + RxJS 5 Operators By Example to understand what an operator does. + +a#managing-subscriptions :marked + ### Managing Subscriptions + Observables like any other instance use resources and those resources add to the overall weight of your application over time. Observables provide a `Subscription` for each `Subscriber` of the Observable that comes with a way to _unsubscribe_ or clean up any resources used while listening for values produced by the Observable. We'll look at a simple example of how to unsubscribe from and Observable once @@ -174,8 +383,10 @@ h3#managing-subscriptions Managing Subscriptions +makeExcerpt('src/app/hero-counter.component.ts', '') -h3#async-pipe Async Pipe: Declarative Subscription Management +a#async-pipe :marked + ### Async Pipe: declarative subscription management + You can manage Observables imperatively through manually subscribing and unsubscribing when needed but you can also manage them declaratively in our templates using the `Async Pipe`. The async pipe can also take care of our Subscription management, as it can take an Observable or a Promise, listen for its emitted values and will destroy its subscriptions @@ -205,8 +416,10 @@ h3#async-pipe Async Pipe: Declarative Subscription Management are produced it will bind those values to the same `ngFor` directive. If you were to initiate another sequence of heroes the pipe would handle updated the retrieved values along with destroying the Observable subscription once the component is destroyed. -h3#sharing-data Sharing data with a stream +a#sharing-data :marked + ### Sharing data with a stream + As you build out your Angular application, you will start sharing data between multiple components. These components may span across multiple routes or application views in your application hierarchy. This allows you to centralize where that data comes from and allow multiple recipients of that data to handle it according to their needs. With Observables, you can push changes to this data and notify all of the subscribers so they can react @@ -251,8 +464,9 @@ h3#sharing-data Sharing data with a stream +makeExcerpt('src/app/app.component.ts (message log)', '') -h3#error-handling Error Handling +a#error-handling :marked + ### Error handling As often as you strive for perfect conditions, errors will happen. Servers go down, invalid data is sent and other issues cause errors to happen when processing data. While you can do your best to prevent these errors, its also wise to be ready for them when they do happen. The scenario this is most likely to happen in is when you're making data requests to an external API. This is a common task done with the Angular HTTP client. @@ -279,8 +493,10 @@ h3#error-handling Error Handling Now we have a path of recovery. When the `getHeroes` request is made and fails, an error notification is produced, which will be handled in the `catch` operation. This error handling is simplified, so returning an Observable with an empty array will suffice. -h3#retry Retry Failed Observable +a#retry :marked + ### Retry Failed Observable + This is a simple path of recovery, but we can go further. What if you also wanted to _retry_ a failed request? With Observables, this is as easy as adding a new operator, aptly named `retry`. If you've ever done this with a Promise, its definitely not a painless operation. @@ -299,8 +515,10 @@ h3#retry Retry Failed Observable // TODO Diagram for retry sequence -h3#stream-integration Stream Integration +a#stream-integration :marked + ### Stream integration + Knowing Angular provides multiple Observables through different APIs is good, but putting those streams together in a valuable way is what you will be striving for. With a consistent interface provided by Observables, its easy to combine streams together. Let's look at building @@ -373,7 +591,3 @@ h3#stream-integration Stream Integration forms getter. Update the merged observables to include the name valueChanges. +makeExcerpt('src/app/add-hero.component.3.ts (Observable valueChanges)', 'value-changes') - -h3#further-reading Further Reading -:marked - // TODO link some resources From 1e3dfad5da42530787b9e1a5c3d9a45e7079764b Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Wed, 22 Mar 2017 21:07:10 -0500 Subject: [PATCH 13/13] Added examples for observable and operator basics --- .../rxjs/ts/src/app/add-hero.component.3.ts | 75 ------------ .../rxjs/ts/src/app/add-hero.component.ts | 64 +++++------ .../rxjs/ts/src/app/observable-basics.ts | 39 +++++++ .../rxjs/ts/src/app/operator-basics.ts | 37 ++++++ public/docs/ts/latest/guide/rxjs.jade | 108 +++++++++--------- 5 files changed, 161 insertions(+), 162 deletions(-) delete mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/observable-basics.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/operator-basics.ts diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts deleted file mode 100644 index 81a83145da..0000000000 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.3.ts +++ /dev/null @@ -1,75 +0,0 @@ -// #docplaster -// #docregion -// #docregion rxjs-imports-1 -import 'rxjs/add/operator/takeUntil'; -import 'rxjs/add/observable/merge'; -import 'rxjs/add/observable/fromEvent'; -// #enddocregion rxjs-imports-1 -// #docregion viewchild-imports -import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; -// #enddocregion viewchild-imports -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; - -import { Hero } from './hero'; -import { HeroService } from './hero.service'; - -// #docregion rxjs-imports-2 -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -// #enddocregion rxjs-imports-2 - -// #docregion viewchild-heroName -@Component({ - moduleId: module.id, - templateUrl: './add-hero.component.html', - styles: [ '.error { color: red }' ] -}) -export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { - @ViewChild('heroName', { read: ElementRef }) heroName: ElementRef; -// #enddocregion viewchild-heroName - - form: FormGroup; - showErrors: boolean = false; - success: boolean; - onDestroy$ = new Subject(); - - constructor( - private formBuilder: FormBuilder, - private heroService: HeroService - ) {} - - ngOnInit() { - this.form = this.formBuilder.group({ - name: ['', [Validators.required]] - }); - } -// #docregion value-changes - ngAfterViewInit() { - const controlBlur$: Observable = Observable.fromEvent(this.heroName.nativeElement, 'blur'); - - Observable.merge( - controlBlur$, - this.form.get('name').valueChanges - ) - .takeUntil(this.onDestroy$) - .subscribe(() => this.checkForm()); - } - - checkForm() { - if (!this.form.valid) { - this.showErrors = true; - } - } -// #enddocregion value-changes - - ngOnDestroy() { - this.onDestroy$.complete(); - } - - save(model: Hero) { - this.heroService.addHero(model.name) - .subscribe(() => { - this.success = true; - }); - } -} diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts index 6d05eea4a5..81a83145da 100644 --- a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts @@ -1,27 +1,32 @@ // #docplaster // #docregion -import 'rxjs/add/observable/of'; -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/observable/merge'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/switchMap'; +// #docregion rxjs-imports-1 import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/observable/fromEvent'; +// #enddocregion rxjs-imports-1 +// #docregion viewchild-imports import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; -import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; +// #enddocregion viewchild-imports +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { EventAggregatorService } from './event-aggregator.service'; +import { Hero } from './hero'; import { HeroService } from './hero.service'; +// #docregion rxjs-imports-2 +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +// #enddocregion rxjs-imports-2 + +// #docregion viewchild-heroName @Component({ moduleId: module.id, - templateUrl: 'add-hero.component.html', + templateUrl: './add-hero.component.html', styles: [ '.error { color: red }' ] }) export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { @ViewChild('heroName', { read: ElementRef }) heroName: ElementRef; +// #enddocregion viewchild-heroName form: FormGroup; showErrors: boolean = false; @@ -30,32 +35,22 @@ export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { constructor( private formBuilder: FormBuilder, - private heroService: HeroService, - private eventService: EventAggregatorService + private heroService: HeroService ) {} ngOnInit() { this.form = this.formBuilder.group({ - name: ['', [Validators.required], [(control: FormControl) => { - return this.checkHeroName(control.value); - }]] + name: ['', [Validators.required]] }); } - - checkHeroName(name: string) { - return Observable.of(name) - .switchMap(heroName => this.heroService.isNameAvailable(heroName)) - .map(available => available ? null : { taken: true }); - } - +// #docregion value-changes ngAfterViewInit() { - const controlBlur$ = Observable.fromEvent(this.heroName.nativeElement, 'blur'); + const controlBlur$: Observable = Observable.fromEvent(this.heroName.nativeElement, 'blur'); Observable.merge( - this.form.valueChanges, - controlBlur$ + controlBlur$, + this.form.get('name').valueChanges ) - .debounceTime(300) .takeUntil(this.onDestroy$) .subscribe(() => this.checkForm()); } @@ -65,19 +60,16 @@ export class AddHeroComponent implements OnInit, OnDestroy, AfterViewInit { this.showErrors = true; } } +// #enddocregion value-changes + + ngOnDestroy() { + this.onDestroy$.complete(); + } - save(model: any) { + save(model: Hero) { this.heroService.addHero(model.name) .subscribe(() => { this.success = true; - this.eventService.add({ - type: 'hero', - message: 'Hero Added' - }); }); } - - ngOnDestroy() { - this.onDestroy$.complete(); - } } diff --git a/public/docs/_examples/rxjs/ts/src/app/observable-basics.ts b/public/docs/_examples/rxjs/ts/src/app/observable-basics.ts new file mode 100644 index 0000000000..a0548abbba --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/observable-basics.ts @@ -0,0 +1,39 @@ +// #docregion +/* +// #docregion basic-1 +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; + +const heroObservable = Observable.create((observer: Observer) => { + // notify observer of values + observer.next('Mr. Nice'); + + // notify observer of an error + observer.error(new Error('I failed the mission')); + + // notify observer of completion + observer.complete(); +}); +// #enddocregion basic-1 +// #docregion basic-2 +const heroObservable = Observable.create((observer: Observer) => { + // notify observer of values + observer.next('Mr. Nice'); + observer.next('Narco'); +}); +// #enddocregion basic-2 +// #docregion basic-3 +import { Subscription } from 'rxjs/Subscription'; + +const observer: Observer = { + next: (hero) => { console.log(`Hero: ${hero}`); }, + error: (error) => { console.log(`Something went wrong: ${error}`); }, + complete: () => { console.log('All done here'); } +}; + +const subscription = heroObservable.subscribe(observer); +// #enddocregion basic-3 +// #docregion basic-4 +subscription.unsubscribe(); +// #enddocregion basic-4 +*/ diff --git a/public/docs/_examples/rxjs/ts/src/app/operator-basics.ts b/public/docs/_examples/rxjs/ts/src/app/operator-basics.ts new file mode 100644 index 0000000000..799aae1a5c --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/operator-basics.ts @@ -0,0 +1,37 @@ +// #docregion +/* +// #docregion basic-1 +import 'rxjs/add/operator/map'; +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; +import { Subscription } from 'rxjs/Subscription'; + +const heroObservable = Observable.create((observer: Observer) => { + // notify observer of values + observer.next('Mr. Nice'); + observer.next('Narco'); + observer.complete(); +}); + +// map each hero value to new value +const subscription = heroObservable + .map(hero => `(( ${hero} ))` ) + .subscribe( + // next + (heroName) => { console.log(`Mapped hero: ${heroName}`); }, + // error + () => {}, + // complete + () => { console.log('Finished'); } + ); +// #enddocregion basic-1 +// #docregion basic-2 +import 'rxjs/add/observable/interval'; +import 'rxjs/add/operator/interval'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; + +const intervalObservable = Observable.interval(1000); + +const subscription: Subscription = intervalObservable.take(5).subscribe(); +// #enddocregion basic-2 diff --git a/public/docs/ts/latest/guide/rxjs.jade b/public/docs/ts/latest/guide/rxjs.jade index eff5160be7..29abb8afc5 100644 --- a/public/docs/ts/latest/guide/rxjs.jade +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -33,18 +33,18 @@ block includes * [Framework APIs](#framework-apis "") * [Stream Integration](#stream-integration "") -a#definition +a#definition :marked ## _Observables_ - Applications process streams of events, arriving over the course of a user session. - These stream take many forms. - They include user keystrokes and mouse actions, - navigating from one page to another, - responses from HTTP requests, + Applications process streams of events, arriving over the course of a user session. + These stream take many forms. + They include user keystrokes and mouse actions, + navigating from one page to another, + responses from HTTP requests, and messages exchanged between parts of the application. - + The observable _pattern_ is a functional approach to processing event streams. - It is similar to the functional approach to arrays. + It is similar to the functional approach to arrays. Instead of writing `for...` statements you chain array operators like this. +makeExcerpt('src/app/observable-principles.ts', 'functional-array') @@ -62,20 +62,20 @@ a#definition At its core, an `Observable` is just a function representing an action that returns one or more events. An action can be anything: "return a number", "make an HTTP request", "listen for keystrokes", or "navigate to another page". - - The results of an action may be available immediately ("here's the number") - or at some point in the future ("the server responded", "the user hit a key"). + + The results of an action may be available immediately ("here's the number") + or at some point in the future ("the server responded", "the user hit a key"). A new `Observable` takes an `Observer` argument. The `Observer` is an object with three (optional) notification methods: `next`, `error`, and `complete`. -.alert.is-critical. - Code snippet needed. ++makeExcerpt('src/app/observable-basics.ts (observer)', 'basic-1') + :marked When an action produces a value, the `Observable` tells the _observer_ about it by "emitting" the value, a fancy way of saying that it passes the value to the _observer's_ `next` method. -.alert.is-critical. - Code snippet needed. ++makeExcerpt('src/app/observable-basics.ts (next)', 'basic-2') + :marked The `Observable` can tell the _observer_ when things go wrong or the action stops by calling the _observer_'s `error` and `complete` methods. @@ -83,35 +83,37 @@ a#definition We often say that the `Observer` _subscribes_ to the `Observable` or that the `Observer` is a `Subscriber`. In fact, an `Observable` has a `subscribe` method that accepts an observer/subscriber object with these three methods. -.alert.is-critical. - Code snippet needed. ++makeExcerpt('src/app/observable-basics.ts (subscribe)', 'basic-3') + :marked - The `Observable` _function_ returns a _cancellation_ function. + The `Observable` _function_ returns a _cancellation_ function. You can call this function to tell the `Observer` to stop producing events and notifications. Calling this function is also known as "unsubscribing". -.alert.is-critical. - Code snippet needed. ++makeExcerpt('src/app/observable-basics.ts (unsubscribe)', 'basic-4') + :marked The `Observable` is fundamentally that simple. It's fundamentally that wide open. You can observe any source of events with an `Observable` function and consume those event with this API. The real power of `Observable` comes from chaining them together with _**operators**_. An _operator_ takes a source `Observable`, observes its emitted values, transforms them, and returns a new `Observable` of those transformed values. - + The _RxJS_ library ships with a large number of _operators_ for standard tasks. - The `map` operator, for example, turns an input value into an output value. -.alert.is-critical. - Code snippet needed. + The `map` operator, for example, turns an input value into an output value. + ++makeExcerpt('src/app/operator-basics.ts (map)', 'basic-1') + :marked The `take` operator passes along a specified number of results (it may have to wait for them) before signaling to the `Observer` that the sequence is complete. -.alert.is-critical. - Code snippet needed. + ++makeExcerpt('src/app/operator-basics.ts (take)', 'basic-2') + :marked That's just two of the many operators you learn as you become acquainted with `Observables`. -a#learn-observables +a#learn-observables :marked ### Learning about _Observables_ @@ -128,14 +130,14 @@ a#learn-observables These links will lead you to many more presentations and videos to expand your knowledge. - This guide is more narrowly focused on using `Observable` in Angular applications. + This guide is more narrowly focused on using `Observable` in Angular applications. -a#observables-vs-promises +a#observables-vs-promises :marked ### _Observables_ and _Promises_ are different JavaScript has many asynchronous APIs, including mouse moves, keystrokes, and timers. - You don't block the UI and wait for these events. + You don't block the UI and wait for these events. You attach a callback function to the event and let the event call your handler whenever something happens. Developers quickly understand that an `Observable` is a superior way to manage the flow of events coming from these high-volume sources. @@ -149,13 +151,13 @@ a#observables-vs-promises The Angular `http` client returns an `Observable` instead. You consume the `Observable` in a way that looks _superficially_ like a `Promise`. You supply a callback to the `Observable.subscribe` method rather than to a `then` method. - + The `Observable` and the `Promise` are both techniques for coping with asynchronous processes. You can use an `Observable` where you'd use a `Promise`. - - The similarity ends there. - An `Observable` is not a `Promise`, - it doesn't want to be a `Promise`, + + The similarity ends there. + An `Observable` is not a `Promise`, + it doesn't want to be a `Promise`, and you'll be confused and disappointed if you expect an `Observable` to behave like a `Promise`. The `Promise` and the `Observable` are more different then alike: @@ -208,7 +210,7 @@ table(width="100%") td :marked A `Promise` returns the same result (or error) every time. - + Calling `then` a second time returns the same object as the first time. It does _not_ re-execute the source of the promised value. It does _not_ re-execute a `then` callback, @@ -247,13 +249,13 @@ table(width="100%") On the other hand, the `Promise` is much simpler. It has a `then` method and that's it. It's always "hot", asynchronous, multicast, and resolves to a single value. There is no way to _unsubscribe_ and, therefore, no danger in failing to unsubscribe. - `Promises` aren't bad. They aren't inferior. They're just different. + `Promises` aren't bad. They aren't inferior. They're just different. Angular has APIs that return a `Promise` such as the application bootstrap method: +makeExcerpt('src/main.ts', 'promise') :marked - The simplicity of a `Promise` is perfectly suited to this use case. + The simplicity of a `Promise` is perfectly suited to this use case. The asynchronous bootstrap action must start immediately, it can't be cancelled, and it has a single outcome. You decide, on a case basis, whether and when to use a `Promise` instead of an `Observable`. @@ -262,7 +264,7 @@ table(width="100%") +makeExcerpt('src/app/observable-principles.ts', 'toPromise') +makeExcerpt('src/app/observable-principles.ts', 'fromPromise') -a#operators +a#operators :marked ### Operators: Import them and use them Operators are pure functions that extend the Observable interface, allow you to perform an action against the Observable @@ -324,7 +326,7 @@ a#operator-info Operator decision tree to chose operator by use case. * "Which Operator do I use?"" (RxJS v4. specific). - + These references describe the operators in RxJS v.4. Some of the operators have been dropped, renamed, or changed in v.5. You may need to refer to "Migrating from RxJS 4 to 5". @@ -332,7 +334,7 @@ a#operator-info See RxJS 5 Operators By Example to understand what an operator does. -a#managing-subscriptions +a#managing-subscriptions :marked ### Managing Subscriptions @@ -383,7 +385,7 @@ a#managing-subscriptions +makeExcerpt('src/app/hero-counter.component.ts', '') -a#async-pipe +a#async-pipe :marked ### Async Pipe: declarative subscription management @@ -416,7 +418,7 @@ a#async-pipe are produced it will bind those values to the same `ngFor` directive. If you were to initiate another sequence of heroes the pipe would handle updated the retrieved values along with destroying the Observable subscription once the component is destroyed. -a#sharing-data +a#sharing-data :marked ### Sharing data with a stream @@ -442,8 +444,8 @@ a#sharing-data :marked Next, you'll create your service. Since a `BehaviorSubject` keeps the latest value for subscribers, you'll need to provide it with an initial value also. - There is the `add` method for adding additional events to the log and `clear` method for clearing the message. You'll notice that the `notify` method - calls the `events$.next` method to notify the subscribers of a new value pushed to the stream. + There is the `add` method for adding additional events to the log. Each time a new event is added, the subscribers + will be notified of the newest value pushed to the stream. +makeExcerpt('src/app/event-aggregator.service.ts', '') @@ -464,7 +466,7 @@ a#sharing-data +makeExcerpt('src/app/app.component.ts (message log)', '') -a#error-handling +a#error-handling :marked ### Error handling As often as you strive for perfect conditions, errors will happen. Servers go down, invalid data is sent and other issues cause errors to happen @@ -493,7 +495,7 @@ a#error-handling Now we have a path of recovery. When the `getHeroes` request is made and fails, an error notification is produced, which will be handled in the `catch` operation. This error handling is simplified, so returning an Observable with an empty array will suffice. -a#retry +a#retry :marked ### Retry Failed Observable @@ -515,7 +517,7 @@ a#retry // TODO Diagram for retry sequence -a#stream-integration +a#stream-integration :marked ### Stream integration @@ -585,9 +587,13 @@ a#stream-integration +makeExcerpt('src/app/add-hero.component.2.ts (Observable fromEvent)', '') :marked - Since you can compose multiple streams using `Observable.merge`, you can easily add additional streams to trigger - the validation check. Since reactive forms provide an Observable of `valueChanges`, you can listen for value changes + Since reactive forms provide an Observable of `valueChanges`, you can listen for value changes from the `name` field and display errors once the user inputs some data also. You can easily access this through the reactive forms getter. Update the merged observables to include the name valueChanges. -+makeExcerpt('src/app/add-hero.component.3.ts (Observable valueChanges)', 'value-changes') ++makeExcerpt('src/app/add-hero.component.ts (Observable valueChanges)', '') + +:marked + Since you can compose multiple streams using `Observable.merge`, you can easily add additional streams to trigger + the validation check. `Observable.merge` will emit a notification whenever any of its source observables emit an + event without waiting for the others to emit.