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/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/plnkr.json b/public/docs/_examples/rxjs/ts/plnkr.json new file mode 100644 index 0000000000..63de2208e6 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/plnkr.json @@ -0,0 +1,10 @@ +{ + "description": "RxJS", + "basePath": "src/", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[0-9].*" + ], + "tags": ["rxjs", "observable"] +} 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.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 new file mode 100644 index 0000000000..b67ee9d8d5 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.2.ts @@ -0,0 +1,74 @@ +// #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 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) + .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 new file mode 100644 index 0000000000..d148361e8a --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html @@ -0,0 +1,16 @@ + +

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..81a83145da --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.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/app-routing.module.ts b/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts new file mode 100644 index 0000000000..5c72b75b88 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/app-routing.module.ts @@ -0,0 +1,20 @@ +// #docplaster +// #docregion +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' }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(appRoutes)], + exports: [RouterModule] +}) +export class AppRoutingModule {} diff --git a/public/docs/_examples/rxjs/ts/src/app/app.component.ts b/public/docs/_examples/rxjs/ts/src/app/app.component.ts new file mode 100644 index 0000000000..16bff0416e --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/app.component.ts @@ -0,0 +1,36 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { EventAggregatorService } from './event-aggregator.service'; +import { ObservablePrinciples } from './observable-principles'; + +@Component({ + selector: 'my-app', + template: ` +

RxJS in Angular

+ + Add Hero
+ Heroes
+ Hero Counter
+ + + + + + ` +}) +export class AppComponent implements OnInit { + 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 new file mode 100644 index 0000000000..d5571fa2ac --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/app.module.ts @@ -0,0 +1,53 @@ +// #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 { 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 { ObservablePrinciples } from './observable-principles'; +import { LoadingService } from './loading.service'; +import { HeroService } from './hero.service'; + +// #docregion event-aggregator-import +import { EventAggregatorService } from './event-aggregator.service'; +// #enddocregion event-aggregator-import + +// Imports for loading & configuring the in-memory web api +import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; + +@NgModule({ + imports: [ + BrowserModule, + HttpModule, + AppRoutingModule, + ReactiveFormsModule, + InMemoryWebApiModule.forRoot(InMemoryDataService) + ], + declarations: [ + AppComponent, + HeroCounterComponent, + HeroListComponent, + MessageLogComponent, + LoadingComponent, + AddHeroComponent + ], + providers: [ + ObservablePrinciples, + HeroService, + LoadingService, + EventAggregatorService + ], + bootstrap: [ AppComponent ] +}) +export class AppModule { +} +// #enddocregion 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 new file mode 100644 index 0000000000..fa972939f1 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/event-aggregator.service.ts @@ -0,0 +1,25 @@ +// #docplaster +// #docregion +// #docregion imports +import 'rxjs/add/operator/scan'; +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$: BehaviorSubject = new BehaviorSubject([]); + events$ = this._events$ + .scan((events, event) => events.concat(event), []); + + add(event: AppEvent) { + this._events$.next([event]); + } +} 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 new file mode 100644 index 0000000000..5761dacbed --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.1.ts @@ -0,0 +1,40 @@ +// #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 new file mode 100644 index 0000000000..6ecd9edb2c --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero-counter.component.ts @@ -0,0 +1,57 @@ +// #docplaster +// #docregion +// #docregion takeUntil-operator +import 'rxjs/add/operator/takeUntil'; +// #enddocregion takeUntil-operator +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; + +// #docregion import-subject +import { Subject } from 'rxjs/Subject'; +// #enddocregion import-subject + +@Component({ + selector: 'hero-counter', + template: ` +

HERO COUNTER

+

+ Heroes {{ count }} +

+ ` +}) +export class HeroCounterComponent implements OnInit, OnDestroy { + count: number = 0; + counter$: Observable; + +// #docregion onDestroy-subject + onDestroy$ = new Subject(); +// #enddocregion onDestroy-subject + + ngOnInit() { + this.counter$ = Observable.create((observer: Observer) => { + setInterval(() => { + observer.next(this.count++); + }, 1000); + }); + + this.counter$ + .takeUntil(this.onDestroy$) + .subscribe(); + + this.counter$ + .takeUntil(this.onDestroy$) + .subscribe(); + + this.counter$ + .takeUntil(this.onDestroy$) + .subscribe(); + } + +// #docregion ngOnDestroy-complete + ngOnDestroy() { + this.onDestroy$.complete(); + } +// #enddocregion ngOnDestroy-complete +} +// #enddocregion diff --git a/public/docs/_examples/rxjs/ts/src/app/hero-list.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.1.ts new file mode 100644 index 0000000000..967b98a8e4 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/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..1e8f22b775 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.2.ts @@ -0,0 +1,33 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ +// #docregion async-pipe + template: ` +

HEROES

+
    +
  • + {{ hero.id }} {{ hero.name }} +
  • +
+ ` +// #enddocregion async-pipe +}) +// #docregion observable-heroes +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/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..45eb8679d8 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero-list.component.3.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(true); // Simulate a failed request + } +} +// #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 new file mode 100644 index 0000000000..7d3740c5bb --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/src/app/hero.service.1.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts new file mode 100644 index 0000000000..977a266ed9 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.1.ts @@ -0,0 +1,22 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/map'; +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroService { + private heroesUrl = 'api/heroes'; + + constructor( + private http: Http + ) {} + + getHeroes(): Observable { + return this.http.get(this.heroesUrl) + .map(response => response.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..6157d3c428 --- /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 } 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..28714c0e7e --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.3.ts @@ -0,0 +1,37 @@ +// #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 } 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' : ''}`) + // #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.4.ts b/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts new file mode 100644 index 0000000000..3e5c6e9d5d --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts @@ -0,0 +1,43 @@ +// #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, { 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 new file mode 100644 index 0000000000..c04ce1ab86 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.service.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, { name }, {headers: this.headers}); + } + + isNameAvailable(name: string): Observable { + return this.http + .get(`${this.heroesUrl}/?name=${name}`) + .map(response => response.json().data) + .map(heroes => !heroes.find((hero: Hero) => hero.name === name)); + } +} diff --git a/public/docs/_examples/rxjs/ts/src/app/hero.ts b/public/docs/_examples/rxjs/ts/src/app/hero.ts new file mode 100644 index 0000000000..e3eac516da --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} diff --git a/public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.1.ts b/public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.1.ts new file mode 100644 index 0000000000..1ac5eb2b71 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/src/app/heroes-filtered.component.2.ts b/public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.2.ts new file mode 100644 index 0000000000..edf88e48a0 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/src/app/heroes-filtered.component.ts b/public/docs/_examples/rxjs/ts/src/app/heroes-filtered.component.ts new file mode 100644 index 0000000000..40f6b3196d --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/_examples/rxjs/ts/src/app/in-memory-data.service.ts b/public/docs/_examples/rxjs/ts/src/app/in-memory-data.service.ts new file mode 100644 index 0000000000..9b227bf2d3 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/src/app/loading.component.css b/public/docs/_examples/rxjs/ts/src/app/loading.component.css new file mode 100644 index 0000000000..522401dae0 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/src/app/loading.component.ts b/public/docs/_examples/rxjs/ts/src/app/loading.component.ts new file mode 100644 index 0000000000..35e6646a62 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/loading.component.ts @@ -0,0 +1,29 @@ +// #docplaster +// #docregion +import { Component, OnInit, OnDestroy } 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, OnDestroy { + 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/src/app/loading.service.ts b/public/docs/_examples/rxjs/ts/src/app/loading.service.ts new file mode 100644 index 0000000000..fa56fbb256 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/loading.service.ts @@ -0,0 +1,24 @@ +// #docplaster +// #docregion +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/src/app/message-log.component.ts b/public/docs/_examples/rxjs/ts/src/app/message-log.component.ts new file mode 100644 index 0000000000..0588793360 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/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/_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/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/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/_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/index.html b/public/docs/_examples/rxjs/ts/src/index.html new file mode 100644 index 0000000000..94f44300e2 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/index.html @@ -0,0 +1,32 @@ + + + + + + + + + RxJS in Angular + + + + + + + + + + + + + + + + loading... + + + + diff --git a/public/docs/_examples/rxjs/ts/src/main.ts b/public/docs/_examples/rxjs/ts/src/main.ts new file mode 100644 index 0000000000..a46cd031b6 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/main.ts @@ -0,0 +1,8 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; + +// #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 7a0c4aab83..1a7d25dda0 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 asynchronous application events." + }, + "security": { "title": "Security", "intro": "Developing for content security in Angular applications." 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 new file mode 100644 index 0000000000..29abb8afc5 --- /dev/null +++ b/public/docs/ts/latest/guide/rxjs.jade @@ -0,0 +1,599 @@ +block includes + include ../_util-fns + +:marked + **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_. + + 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 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 + * [_Observables_](#definition "") + * [Learning _Observables_](#learning-observables "") + * [Observables and Promises](#observables-vs-promises "") + * [Using Operators](#operators "") + * [Managing Subscriptions](#managing-subscriptions "") + * [Sharing Data](#sharing-data "") + * [Error Handling](#error-handling "") + * [Framework APIs](#framework-apis "") + * [Stream Integration](#stream-integration "") + +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 + The `Observable` is a functional approach to _streams of events in time_ rather than _arrays of items in space_. + ++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`. + +:marked + ### _Observable_ is just a function + + 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"). + + A new `Observable` takes an `Observer` argument. + The `Observer` is an object with three (optional) notification methods: `next`, `error`, and `complete`. + ++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. + ++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. + + 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. + ++makeExcerpt('src/app/observable-basics.ts (subscribe)', 'basic-3') + +: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". + ++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. + ++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. + ++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 +: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. + One approach is to import the entire RxJS library. + ++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, + 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('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`, + 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 + 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('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. + +.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 + 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. + +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 + 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 in the view. Once the component is destroyed, we no longer + want to listen for any changes coming from the Observable 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. + ++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. + 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. + + 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. + ++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. + ++makeExcerpt('src/app/hero-counter.component.ts', 'import-subject') + +:marked + You'll need to create an `onDestroy$` observable using the Subject. + ++makeExcerpt('src/app/hero-counter.component.ts', 'onDestroy-subject') + +: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', '') + +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 + 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. + ++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 + 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. + ++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 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 +: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 + 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('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('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. 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', '') + +: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('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. + +: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('src/app/app.component.ts (message log)', '') + +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. + 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.2.ts (failed heroes)', 'getHeroes-failed') + +:marked + This is what the `HeroListComponent` currently looks like with no error handling and the simulated error. + ++makeExcerpt('src/app/hero-list.component.3.ts (failed heroes)', '') + +:marked + 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.service.2.ts (catch and return)', '') + +:marked + 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 +: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. + + Of course you'll need to import the operator first. + ++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.service.3.ts', '') + +:marked + 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. + +// TODO Diagram for retry sequence + +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 + 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 + And the hero form template. + ++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, 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? 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 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.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.