From bd0bad4286cd7c7d314a12632a63bfba23564fa4 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 27 Apr 2017 07:14:29 -0500 Subject: [PATCH] docs(aio): Added developer guide on RxJS --- aio/content/examples/.DS_Store | Bin 0 -> 6148 bytes aio/content/examples/.gitignore | 4 + aio/content/examples/rxjs/e2e-spec.ts | 62 ++ aio/content/examples/rxjs/example-config.json | 0 aio/content/examples/rxjs/plnkr.json | 10 + .../rxjs/src/app/app-routing.module.ts | 18 + .../examples/rxjs/src/app/app.component.1.ts | 36 + .../examples/rxjs/src/app/app.component.ts | 29 + .../examples/rxjs/src/app/app.module.ts | 47 + .../src/app/event-aggregator.service.spec.ts | 44 + .../rxjs/src/app/event-aggregator.service.ts | 25 + .../rxjs/src/app/hero-counter.component.1.ts | 40 + .../rxjs/src/app/hero-counter.component.2.ts | 57 ++ .../rxjs/src/app/hero-counter.component.ts | 72 ++ .../rxjs/src/app/hero-detail.component.1.ts | 38 + .../src/app/hero-detail.component.spec.ts | 71 ++ .../rxjs/src/app/hero-detail.component.ts | 38 + .../rxjs/src/app/hero-list.component.1.ts | 30 + .../rxjs/src/app/hero-list.component.2.ts | 33 + .../rxjs/src/app/hero-list.component.3.ts | 30 + .../rxjs/src/app/hero-list.component.ts | 30 + .../examples/rxjs/src/app/hero.service.1.ts | 22 + .../examples/rxjs/src/app/hero.service.2.ts | 32 + .../examples/rxjs/src/app/hero.service.3.ts | 35 + .../examples/rxjs/src/app/hero.service.4.ts | 34 + .../rxjs/src/app/hero.service.spec.ts | 110 +++ .../examples/rxjs/src/app/hero.service.ts | 40 + aio/content/examples/rxjs/src/app/hero.ts | 3 + .../src/app/heroes-filtered.component.1.ts | 42 + .../src/app/heroes-filtered.component.2.ts | 36 + .../rxjs/src/app/heroes-filtered.component.ts | 35 + .../rxjs/src/app/in-memory-data.service.ts | 19 + .../rxjs/src/app/loading.component.css | 8 + .../rxjs/src/app/loading.component.ts | 29 + .../examples/rxjs/src/app/loading.service.ts | 24 + .../rxjs/src/app/message-log.component.ts | 25 + .../rxjs/src/app/observable-basics.ts | 39 + .../rxjs/src/app/observable-principles.ts | 133 +++ .../examples/rxjs/src/app/operator-basics.ts | 38 + .../examples/rxjs/src/browser-test-shim.js | 87 ++ aio/content/examples/rxjs/src/heroes.json | 12 + aio/content/examples/rxjs/src/index.html | 32 + aio/content/examples/rxjs/src/main.ts | 8 + .../rxjs/src/systemjs.config.extras.js | 11 + aio/content/examples/rxjs/src/tests.html | 41 + aio/content/examples/rxjs/tests.plnkr.json | 21 + aio/content/guide/aot-compiler.md | 1 - aio/content/guide/quickstart.md | 2 +- aio/content/guide/rxjs.md | 904 ++++++++++++++++++ aio/content/guide/webpack.md | 1 - aio/content/navigation.json | 12 + aio/tools/examples/shared/package.json | 1 + aio/tools/examples/shared/yarn.lock | 6 + 53 files changed, 2554 insertions(+), 3 deletions(-) create mode 100644 aio/content/examples/.DS_Store create mode 100644 aio/content/examples/rxjs/e2e-spec.ts create mode 100644 aio/content/examples/rxjs/example-config.json create mode 100644 aio/content/examples/rxjs/plnkr.json create mode 100644 aio/content/examples/rxjs/src/app/app-routing.module.ts create mode 100644 aio/content/examples/rxjs/src/app/app.component.1.ts create mode 100644 aio/content/examples/rxjs/src/app/app.component.ts create mode 100644 aio/content/examples/rxjs/src/app/app.module.ts create mode 100644 aio/content/examples/rxjs/src/app/event-aggregator.service.spec.ts create mode 100644 aio/content/examples/rxjs/src/app/event-aggregator.service.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-counter.component.1.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-counter.component.2.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-counter.component.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-detail.component.1.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-detail.component.spec.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-detail.component.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-list.component.1.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-list.component.2.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-list.component.3.ts create mode 100644 aio/content/examples/rxjs/src/app/hero-list.component.ts create mode 100644 aio/content/examples/rxjs/src/app/hero.service.1.ts create mode 100644 aio/content/examples/rxjs/src/app/hero.service.2.ts create mode 100644 aio/content/examples/rxjs/src/app/hero.service.3.ts create mode 100644 aio/content/examples/rxjs/src/app/hero.service.4.ts create mode 100644 aio/content/examples/rxjs/src/app/hero.service.spec.ts create mode 100644 aio/content/examples/rxjs/src/app/hero.service.ts create mode 100644 aio/content/examples/rxjs/src/app/hero.ts create mode 100644 aio/content/examples/rxjs/src/app/heroes-filtered.component.1.ts create mode 100644 aio/content/examples/rxjs/src/app/heroes-filtered.component.2.ts create mode 100644 aio/content/examples/rxjs/src/app/heroes-filtered.component.ts create mode 100644 aio/content/examples/rxjs/src/app/in-memory-data.service.ts create mode 100644 aio/content/examples/rxjs/src/app/loading.component.css create mode 100644 aio/content/examples/rxjs/src/app/loading.component.ts create mode 100644 aio/content/examples/rxjs/src/app/loading.service.ts create mode 100644 aio/content/examples/rxjs/src/app/message-log.component.ts create mode 100644 aio/content/examples/rxjs/src/app/observable-basics.ts create mode 100644 aio/content/examples/rxjs/src/app/observable-principles.ts create mode 100644 aio/content/examples/rxjs/src/app/operator-basics.ts create mode 100644 aio/content/examples/rxjs/src/browser-test-shim.js create mode 100644 aio/content/examples/rxjs/src/heroes.json create mode 100644 aio/content/examples/rxjs/src/index.html create mode 100644 aio/content/examples/rxjs/src/main.ts create mode 100644 aio/content/examples/rxjs/src/systemjs.config.extras.js create mode 100644 aio/content/examples/rxjs/src/tests.html create mode 100644 aio/content/examples/rxjs/tests.plnkr.json create mode 100644 aio/content/guide/rxjs.md diff --git a/aio/content/examples/.DS_Store b/aio/content/examples/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..677396a4efe0fe1a48f461040a6c1d52372ab771 GIT binary patch literal 6148 zcmeHK%}T^D5Kh)ryLzdpAb9Cj@UV)%x4P<4Sg0tXvbMCikQ`-tsDL=>>`E`exiv^C}mp#_AiR6v!= z^%aAwbnsg`&eoVKROyV%k)a7*JIc&;QM@@Bbf@ zs74GB1OJKvUfggtDljE|w)Ra9&sq!g2owe5a)rGVFyv7Tv3L|ufl2|tg$AIlF;@s4 P5c(02G*Ced{3ru&U7lnk literal 0 HcmV?d00001 diff --git a/aio/content/examples/.gitignore b/aio/content/examples/.gitignore index 17584c4f09e9..1ef3d0c182eb 100644 --- a/aio/content/examples/.gitignore +++ b/aio/content/examples/.gitignore @@ -83,3 +83,7 @@ aot-compiler/**/*.factory.d.ts # ngUpgrade testing !upgrade-phonecat-*/**/karma.conf.js !upgrade-phonecat-*/**/karma-test-shim.js + +# rxjs +!rxjs/src/browser-test-shim.js +!rxjs/src/jasmine-marbles.umd.js diff --git a/aio/content/examples/rxjs/e2e-spec.ts b/aio/content/examples/rxjs/e2e-spec.ts new file mode 100644 index 000000000000..0e1e74b1cd56 --- /dev/null +++ b/aio/content/examples/rxjs/e2e-spec.ts @@ -0,0 +1,62 @@ +'use strict'; // necessary for es6 output in node + +import { browser, element, by, ElementFinder, ElementArrayFinder } from 'protractor'; + +describe('RxJS', function () { + let page: any; + + function getPage() { + return { + findHrefs: () => element.all(by.css('my-app a')), + findHeroes: () => element(by.linkText('Heroes')), + findHeroCounter: () => element(by.linkText('Hero Counter')), + + findMessageLog: () => element(by.css('message-log')), + findHeroList: () => element(by.css('ul.items')), + findHeroListItems: () => element.all(by.css('ul.items li')), + + findHeroDetailDivs: () => element.all(by.css('ng-component div div')) + }; + } + + beforeAll(function () { + browser.get(''); + }); + + beforeEach(() => { + page = getPage(); + }); + + it('should have 10 heroes', async() => { + const heroes: ElementArrayFinder = page.findHeroListItems(); + + expect(await heroes.count()).toBe(10); + }); + + it('should have 1 initial event log items', async() => { + const log: ElementFinder = page.findMessageLog(); + const logItems: ElementArrayFinder = log.all(by.css('ul li')); + + expect(await logItems.count()).toBe(1); + }); + + xit('should add log entries after leaving hero counter page', async() => { + const heroCounter: ElementFinder = page.findHeroCounter(); + const heroes: ElementFinder = page.findHeroes(); + const log: ElementFinder = page.findMessageLog(); + const logItems: ElementArrayFinder = log.all(by.css('ul li')); + await heroCounter.click(); + await heroes.click(); + + expect(await logItems.count()).toBe(9); + }); + + it('should display hero details', async () => { + const hero: ElementFinder = page.findHeroListItems().first().element(by.css('a')); + const heroDetailDivs = page.findHeroDetailDivs(); + await hero.click(); + + expect(await heroDetailDivs.first().getText()).toContain('ID: 1'); + expect(await heroDetailDivs.last().getText()).toContain('Name: Mr. Nice'); + }); +}); diff --git a/aio/content/examples/rxjs/example-config.json b/aio/content/examples/rxjs/example-config.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/aio/content/examples/rxjs/plnkr.json b/aio/content/examples/rxjs/plnkr.json new file mode 100644 index 000000000000..63de2208e6aa --- /dev/null +++ b/aio/content/examples/rxjs/plnkr.json @@ -0,0 +1,10 @@ +{ + "description": "RxJS", + "basePath": "src/", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[0-9].*" + ], + "tags": ["rxjs", "observable"] +} diff --git a/aio/content/examples/rxjs/src/app/app-routing.module.ts b/aio/content/examples/rxjs/src/app/app-routing.module.ts new file mode 100644 index 000000000000..1c032c385927 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/app-routing.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { HeroListComponent } from './hero-list.component'; +import { HeroCounterComponent } from './hero-counter.component'; +import { HeroDetailComponent } from './hero-detail.component'; + +const appRoutes: Routes = [ + { path: 'hero/counter', component: HeroCounterComponent }, + { path: 'hero/:id', component: HeroDetailComponent }, + { path: 'heroes', component: HeroListComponent }, + { path: '', redirectTo: '/heroes', pathMatch: 'full' }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(appRoutes)], + exports: [RouterModule] +}) +export class AppRoutingModule {} diff --git a/aio/content/examples/rxjs/src/app/app.component.1.ts b/aio/content/examples/rxjs/src/app/app.component.1.ts new file mode 100644 index 000000000000..936f2036a3b8 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/app.component.1.ts @@ -0,0 +1,36 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { EventAggregatorService } from './event-aggregator.service'; +import { ObservablePrinciples } from './observable-principles'; + +// #docregion message-log +@Component({ + selector: 'my-app', + template: ` +

RxJS in Angular

+ + Heroes
+ Hero Counter
+ + + + + ` +}) +// #enddocregion message-log +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/aio/content/examples/rxjs/src/app/app.component.ts b/aio/content/examples/rxjs/src/app/app.component.ts new file mode 100644 index 000000000000..aa387423da41 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/app.component.ts @@ -0,0 +1,29 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { EventAggregatorService } from './event-aggregator.service'; + +@Component({ + selector: 'my-app', + template: ` +

RxJS in Angular

+ + Heroes
+ Hero Counter
+ + + + + ` +}) +export class AppComponent implements OnInit { + constructor( + private eventService: EventAggregatorService) {} + + ngOnInit() { + this.eventService.add({ + type: 'init', + message: 'Application Initialized' + }); + } +} diff --git a/aio/content/examples/rxjs/src/app/app.module.ts b/aio/content/examples/rxjs/src/app/app.module.ts new file mode 100644 index 000000000000..9ae1e3757f96 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/app.module.ts @@ -0,0 +1,47 @@ +// #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 { HeroDetailComponent } from './hero-detail.component'; + +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, + HeroDetailComponent + ], + providers: [ + HeroService, + EventAggregatorService + ], + bootstrap: [ AppComponent ] +}) +export class AppModule { +} +// #enddocregion diff --git a/aio/content/examples/rxjs/src/app/event-aggregator.service.spec.ts b/aio/content/examples/rxjs/src/app/event-aggregator.service.spec.ts new file mode 100644 index 000000000000..cab09675f79a --- /dev/null +++ b/aio/content/examples/rxjs/src/app/event-aggregator.service.spec.ts @@ -0,0 +1,44 @@ +// #docregion +// #docplaster +// #docregion testing-1 +import { TestBed } from '@angular/core/testing'; +import { EventAggregatorService, AppEvent } from './event-aggregator.service'; + +describe('Event Aggregator Service', () => { + let eventService: EventAggregatorService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + EventAggregatorService + ] + }); + + eventService = TestBed.get(EventAggregatorService); + }); +// #enddocregion testing-1 +// #docregion testing-2 + it('should start with an empty array', () => { + eventService.events$.subscribe(events => { + expect(events.length).toBe(0); + }); + }); +// #enddocregion testing-2 +// #docregion testing-3 + it('should append new events to the array when add() is called', () => { + const event: AppEvent = { + type: 'Event', + message: 'An event occurred' + }; + + eventService.add(event); + + eventService.events$.subscribe(events => { + expect(events.length).toBe(1); + expect(events[0]).toEqual(event); + }); + }); +// #enddocregion testing-3 +// #docregion testing-1 +}); +// #enddocregion testing-1 diff --git a/aio/content/examples/rxjs/src/app/event-aggregator.service.ts b/aio/content/examples/rxjs/src/app/event-aggregator.service.ts new file mode 100644 index 000000000000..fa972939f1d8 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/hero-counter.component.1.ts b/aio/content/examples/rxjs/src/app/hero-counter.component.1.ts new file mode 100644 index 000000000000..90b214d69dec --- /dev/null +++ b/aio/content/examples/rxjs/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 = 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/aio/content/examples/rxjs/src/app/hero-counter.component.2.ts b/aio/content/examples/rxjs/src/app/hero-counter.component.2.ts new file mode 100644 index 000000000000..25d6ccbfe70a --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero-counter.component.2.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 = 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/aio/content/examples/rxjs/src/app/hero-counter.component.ts b/aio/content/examples/rxjs/src/app/hero-counter.component.ts new file mode 100644 index 000000000000..8722653b41b1 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero-counter.component.ts @@ -0,0 +1,72 @@ + +// #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'; + +import { EventAggregatorService } from './event-aggregator.service'; + +// #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 = 0; + counter$: Observable; + + constructor(private eventService: EventAggregatorService) {} + +// #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.getObserver(1)); + + this.counter$ + .takeUntil(this.onDestroy$) + .subscribe(this.getObserver(2)); + + this.counter$ + .takeUntil(this.onDestroy$) + .subscribe(this.getObserver(3)); + } + + getObserver(num: number): Observer { + return { + next: () => {}, + error: () => {}, + complete: () => { + this.eventService.add({ type: 'status', message: `Counter ${num} complete`}); + } + }; + } + +// #docregion ngOnDestroy-complete + ngOnDestroy() { + this.onDestroy$.next(); + } +// #enddocregion ngOnDestroy-complete +} +// #enddocregion diff --git a/aio/content/examples/rxjs/src/app/hero-detail.component.1.ts b/aio/content/examples/rxjs/src/app/hero-detail.component.1.ts new file mode 100644 index 000000000000..107bf499ca2b --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero-detail.component.1.ts @@ -0,0 +1,38 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/mergeMap'; +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute, Params } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +@Component({ + template: ` +

HEROES

+ +
+
+ ID: {{ (hero$ | async)?.id }} +
+
+ + + ID: {{ (hero$ | async)?.name }} +
+
+ ` +}) +export class HeroDetailComponent implements OnInit { + hero$: Observable; + + constructor( + private route: ActivatedRoute, + private service: HeroService + ) {} + + ngOnInit() { + this.hero$ = this.route.params + .mergeMap((params: Params) => this.service.getHero(+params['id'])); + } +} diff --git a/aio/content/examples/rxjs/src/app/hero-detail.component.spec.ts b/aio/content/examples/rxjs/src/app/hero-detail.component.spec.ts new file mode 100644 index 000000000000..b7e0c21e43b7 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero-detail.component.spec.ts @@ -0,0 +1,71 @@ +// #docregion +// #docplaster +// #docregion testing-setup +import 'rxjs/add/observable/of'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +import { HeroDetailComponent } from './hero-detail.component'; + +export class MockActivatedRoute { + params = new BehaviorSubject({}); +} + +export class MockHeroService { + getHero() {} +} + +describe('Hero Detail Component', () => { + let component: HeroDetailComponent; + let fixture: ComponentFixture; + let heroService: HeroService; + let route: MockActivatedRoute; + let hero: Hero = { id: 1, name: 'Test' }; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ HeroDetailComponent ], + providers: [ + { provide: HeroService, useClass: MockHeroService }, + { provide: ActivatedRoute, useClass: MockActivatedRoute } + ] + }); + + heroService = TestBed.get(HeroService); + route = TestBed.get(ActivatedRoute); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HeroDetailComponent); + component = fixture.componentInstance; + }); +// #enddocregion testing-setup +// #docregion testing-service-call + it('should call the hero service with the provided id', () => { + spyOn(heroService, 'getHero').and.returnValue(Observable.of(hero)); + + route.params.next({ id: hero.id }); + fixture.detectChanges(); + + expect(heroService.getHero).toHaveBeenCalledWith(hero.id); + }); +// #enddocregion testing-service-call + +// #docregion testing-component-template + it('should display the provided hero', () => { + spyOn(heroService, 'getHero').and.returnValue(Observable.of(hero)); + + route.params.next({ id: hero.id }); + fixture.detectChanges(); + + const componentElement = fixture.debugElement.nativeElement; + expect(componentElement.textContent).toContain(`ID: ${hero.id}`); + }); +// #enddocregion testing-component-template +// #docregion testing-setup +}); +// #enddocregion testing-setup diff --git a/aio/content/examples/rxjs/src/app/hero-detail.component.ts b/aio/content/examples/rxjs/src/app/hero-detail.component.ts new file mode 100644 index 000000000000..f269e64d3d81 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero-detail.component.ts @@ -0,0 +1,38 @@ +// #docplaster +// #docregion +import 'rxjs/add/operator/mergeMap'; +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute, Params } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + +// #docregion ngIfAs +@Component({ + template: ` +

HEROES

+ +
+
+ ID: {{ hero.id }} +
+
+ Name: {{ hero.name }} +
+
+ ` +}) +// #enddocregion ngIfAs +export class HeroDetailComponent implements OnInit { + hero$: Observable; + + constructor( + private route: ActivatedRoute, + private service: HeroService + ) {} + + ngOnInit() { + this.hero$ = this.route.params + .mergeMap((params: Params) => this.service.getHero(+params['id'])); + } +} diff --git a/aio/content/examples/rxjs/src/app/hero-list.component.1.ts b/aio/content/examples/rxjs/src/app/hero-list.component.1.ts new file mode 100644 index 000000000000..967b98a8e423 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/hero-list.component.2.ts b/aio/content/examples/rxjs/src/app/hero-list.component.2.ts new file mode 100644 index 000000000000..24f1f62603e7 --- /dev/null +++ b/aio/content/examples/rxjs/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

+ + ` +// #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/aio/content/examples/rxjs/src/app/hero-list.component.3.ts b/aio/content/examples/rxjs/src/app/hero-list.component.3.ts new file mode 100644 index 000000000000..d78453c55fc9 --- /dev/null +++ b/aio/content/examples/rxjs/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

+ + ` +}) +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/aio/content/examples/rxjs/src/app/hero-list.component.ts b/aio/content/examples/rxjs/src/app/hero-list.component.ts new file mode 100644 index 000000000000..f6dc5e6a7f3b --- /dev/null +++ b/aio/content/examples/rxjs/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

+ + ` +}) +export class HeroListComponent implements OnInit { + heroes$: Observable; + + constructor( + private service: HeroService + ) {} + + ngOnInit() { + this.heroes$ = this.service.getHeroes(); + } +} +// #enddocregion diff --git a/aio/content/examples/rxjs/src/app/hero.service.1.ts b/aio/content/examples/rxjs/src/app/hero.service.1.ts new file mode 100644 index 000000000000..977a266ed94f --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/hero.service.2.ts b/aio/content/examples/rxjs/src/app/hero.service.2.ts new file mode 100644 index 000000000000..6157d3c42819 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/hero.service.3.ts b/aio/content/examples/rxjs/src/app/hero.service.3.ts new file mode 100644 index 000000000000..06a034419ccf --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero.service.3.ts @@ -0,0 +1,35 @@ +// #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' : ''}`) + .retry(3) + .map(response => response.json().data as Hero[]) + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + // #enddocregion getHeroes-failed + } +} diff --git a/aio/content/examples/rxjs/src/app/hero.service.4.ts b/aio/content/examples/rxjs/src/app/hero.service.4.ts new file mode 100644 index 000000000000..afcfa9b5f36b --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero.service.4.ts @@ -0,0 +1,34 @@ +// #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 + ) {} + + getHeroes(fail?: boolean): Observable { + return this.http.get(`${this.heroesUrl}${fail ? '/failed' : ''}`) + .retry(3) + .map(response => response.json().data as Hero[]) + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + } +} diff --git a/aio/content/examples/rxjs/src/app/hero.service.spec.ts b/aio/content/examples/rxjs/src/app/hero.service.spec.ts new file mode 100644 index 000000000000..4f3a80592a27 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero.service.spec.ts @@ -0,0 +1,110 @@ +// #docregion +// #docplaster +// #docregion marble-testing-setup +import { TestBed } from '@angular/core/testing'; +import { Http } from '@angular/http'; +import { HeroService } from './hero.service'; +import { Hero } from './hero'; + + +import { cold } from 'jasmine-marbles'; + +// #enddocregion marble-testing-setup + +import { initTestScheduler, resetTestScheduler, addMatchers } from 'jasmine-marbles'; +import { defer } from 'rxjs/observable/defer'; + +// #docregion marble-testing-setup +describe('Hero Service', () => { + let heroService: HeroService; + let hero: Hero = new Hero(1, 'Test'); + let http: any; +// #enddocregion marble-testing-setup + /** + * TestScheduler Setup is ONLY FOR DEMO PURPOSES!! + */ + beforeAll(() => addMatchers()); + beforeEach(() => initTestScheduler()); + afterEach(() => resetTestScheduler()); +// #docregion marble-testing-setup + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + HeroService, + { + provide: Http, + useFactory: () => jasmine.createSpyObj('Http', ['get']) + } + ] + }); + + heroService = TestBed.get(HeroService); + http = TestBed.get(Http); + }); +// #enddocregion marble-testing-setup + +// #docregion retrieve-hero-test + it('should retrieve a hero by id', () => { + const data = { + json: () => ({ data: hero }) + }; + + const response$ = cold('--a|', { a: data }); + + http.get.and.returnValue(response$); + + const expected$ = cold('--b|', { b: hero }); + + expect(heroService.getHero(1)).toBeObservable(expected$); + }); +// #enddocregion retrieve-hero-test + +// #docregion retry-heroes-success + it('should return heroes on a successful retry after a failed attempt', () => { + const error = 'Error!'; + const data = { + json: () => ({ data: [hero] }) + }; + const error$ = cold('---#', { }, error); + const success$ = cold('---a|', { a: data }); + let calls = 0; + const response$ = defer(() => { + ++calls; + if (calls < 2) { + return error$; + } + + return success$; + }); + + http.get.and.returnValue(response$); + + const expected$ = cold('------b|', { b: [hero] }); + + expect(heroService.getHeroes()).toBeObservable(expected$); + }); +// #enddocregion retry-heroes-success + +// #docregion retry-heroes-failure + it('should return an empty array after retrying 3 times to retrieve heroes', () => { + const error = 'Error!'; + const error$ = cold('---#', { }, error); + let calls = 0; + const response$ = defer(() => { + calls++; + + return error$; + }); + + http.get.and.returnValue(response$); + + const expected$ = cold('------------(b|)', { b: [] }); + + expect(heroService.getHeroes()).toBeObservable(expected$); + expect(calls).toBe(4); + }); +// #enddocregion retry-heroes-failure +// #docregion marble-testing-setup +}); +// #enddocregion diff --git a/aio/content/examples/rxjs/src/app/hero.service.ts b/aio/content/examples/rxjs/src/app/hero.service.ts new file mode 100644 index 000000000000..e17beb853b75 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero.service.ts @@ -0,0 +1,40 @@ +// #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 + ) {} + + + getHeroes(fail?: boolean): Observable { + return this.http.get(`${this.heroesUrl}${fail ? '/failed' : ''}`) + .retry(3) + .map(response => response.json().data as Hero[]) + .catch((error: any) => { + console.log(`An error occurred: ${error}`); + + return Observable.of([]); + }); + } + + getHero(id: number): Observable { + return this.http.get(`${this.heroesUrl}/${id}`) + .map(response => response.json().data as Hero); + } +} diff --git a/aio/content/examples/rxjs/src/app/hero.ts b/aio/content/examples/rxjs/src/app/hero.ts new file mode 100644 index 000000000000..7a44583aaa99 --- /dev/null +++ b/aio/content/examples/rxjs/src/app/hero.ts @@ -0,0 +1,3 @@ +export class Hero { + constructor(public id: number, public name: string) {} +} diff --git a/aio/content/examples/rxjs/src/app/heroes-filtered.component.1.ts b/aio/content/examples/rxjs/src/app/heroes-filtered.component.1.ts new file mode 100644 index 000000000000..1ac5eb2b71a4 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/heroes-filtered.component.2.ts b/aio/content/examples/rxjs/src/app/heroes-filtered.component.2.ts new file mode 100644 index 000000000000..edf88e48a020 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/heroes-filtered.component.ts b/aio/content/examples/rxjs/src/app/heroes-filtered.component.ts new file mode 100644 index 000000000000..40f6b3196d91 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/in-memory-data.service.ts b/aio/content/examples/rxjs/src/app/in-memory-data.service.ts new file mode 100644 index 000000000000..9b227bf2d3b7 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/loading.component.css b/aio/content/examples/rxjs/src/app/loading.component.css new file mode 100644 index 000000000000..522401dae02e --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/loading.component.ts b/aio/content/examples/rxjs/src/app/loading.component.ts new file mode 100644 index 000000000000..945cb79e6221 --- /dev/null +++ b/aio/content/examples/rxjs/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 = 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/aio/content/examples/rxjs/src/app/loading.service.ts b/aio/content/examples/rxjs/src/app/loading.service.ts new file mode 100644 index 000000000000..fa56fbb256e9 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/message-log.component.ts b/aio/content/examples/rxjs/src/app/message-log.component.ts new file mode 100644 index 000000000000..058879336095 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/observable-basics.ts b/aio/content/examples/rxjs/src/app/observable-basics.ts new file mode 100644 index 000000000000..a0548abbbab1 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/observable-principles.ts b/aio/content/examples/rxjs/src/app/observable-principles.ts new file mode 100644 index 000000000000..606fb2140f81 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/app/operator-basics.ts b/aio/content/examples/rxjs/src/app/operator-basics.ts new file mode 100644 index 000000000000..2c1870cf310a --- /dev/null +++ b/aio/content/examples/rxjs/src/app/operator-basics.ts @@ -0,0 +1,38 @@ +// #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/aio/content/examples/rxjs/src/browser-test-shim.js b/aio/content/examples/rxjs/src/browser-test-shim.js new file mode 100644 index 000000000000..ee21831e2267 --- /dev/null +++ b/aio/content/examples/rxjs/src/browser-test-shim.js @@ -0,0 +1,87 @@ +// BROWSER TESTING SHIM +// Keep it in-sync with what karma-test-shim does +// #docregion +/*global jasmine, __karma__, window*/ +(function () { + +Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing. + +// Uncomment to get full stacktrace output. Sometimes helpful, usually not. +// Error.stackTraceLimit = Infinity; // + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; + +var baseURL = document.baseURI; +baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/'; + +System.config({ + baseURL: baseURL, + // Extend usual application package list with test folder + packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, + + // Assume npm: is set in `paths` in systemjs.config + // Map the angular testing umd bundles + map: { + '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', + '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', + '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', + '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', + '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', + }, +}); + +System.import('systemjs.config.js') + .then(importSystemJsExtras) + .then(initTestBed) + .then(initTesting); + +/** Optional SystemJS configuration extras. Keep going w/o it */ +function importSystemJsExtras(){ + return System.import('systemjs.config.extras.js') + .catch(function(reason) { + console.log( + 'Note: System.import could not load "systemjs.config.extras.js" where you might have added more configuration. It is an optional file so we will continue without it.' + ); + console.log(reason); + }); +} + +function initTestBed(){ + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]) + + .then(function (providers) { + var coreTesting = providers[0]; + var browserTesting = providers[1]; + + coreTesting.TestBed.initTestEnvironment( + browserTesting.BrowserDynamicTestingModule, + browserTesting.platformBrowserDynamicTesting()); + }) +} + +// Import all spec files defined in the html (__spec_files__) +// and start Jasmine testrunner +function initTesting () { + console.log('loading spec files: '+__spec_files__.join(', ')); + return Promise.all( + __spec_files__.map(function(spec) { + return System.import(spec); + }) + ) + // After all imports load, re-execute `window.onload` which + // triggers the Jasmine test-runner start or explain what went wrong + .then(success, console.error.bind(console)); + + function success () { + console.log('Spec files loaded; starting Jasmine testrunner'); + window.onload(); + } +} + +})(); diff --git a/aio/content/examples/rxjs/src/heroes.json b/aio/content/examples/rxjs/src/heroes.json new file mode 100644 index 000000000000..034d5c185628 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/index.html b/aio/content/examples/rxjs/src/index.html new file mode 100644 index 000000000000..94f44300e276 --- /dev/null +++ b/aio/content/examples/rxjs/src/index.html @@ -0,0 +1,32 @@ + + + + + + + + + RxJS in Angular + + + + + + + + + + + + + + + + loading... + + + + diff --git a/aio/content/examples/rxjs/src/main.ts b/aio/content/examples/rxjs/src/main.ts new file mode 100644 index 000000000000..a46cd031b6b7 --- /dev/null +++ b/aio/content/examples/rxjs/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/aio/content/examples/rxjs/src/systemjs.config.extras.js b/aio/content/examples/rxjs/src/systemjs.config.extras.js new file mode 100644 index 000000000000..b3ee4d7d1fff --- /dev/null +++ b/aio/content/examples/rxjs/src/systemjs.config.extras.js @@ -0,0 +1,11 @@ +// #docregion +/** App specific SystemJS configuration */ +System.config({ + packages: { + // barrels + }, + map: { + 'lodash': 'npm:lodash@4.17.4', + 'jasmine-marbles': 'https://rawgit.com/brandonroberts/jasmine-marbles-builds/master/bundles/jasmine-marbles.umd.js' + } +}); diff --git a/aio/content/examples/rxjs/src/tests.html b/aio/content/examples/rxjs/src/tests.html new file mode 100644 index 000000000000..03f9f0ad459f --- /dev/null +++ b/aio/content/examples/rxjs/src/tests.html @@ -0,0 +1,41 @@ + + + + + + + Sample App Specs + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aio/content/examples/rxjs/tests.plnkr.json b/aio/content/examples/rxjs/tests.plnkr.json new file mode 100644 index 000000000000..0099d3ec7f1d --- /dev/null +++ b/aio/content/examples/rxjs/tests.plnkr.json @@ -0,0 +1,21 @@ +{ + "description": "Testing - app.specs", + "basePath": "src/", + "files":[ + "browser-test-shim.js", + "systemjs.config.extras.js", + + "app/event-aggregator.service.ts", + "app/event-aggregator.service.spec.ts", + "app/hero-detail.component.ts", + "app/hero-detail.component.spec.ts", + "app/hero.service.ts", + "app/hero.service.spec.ts", + "app/hero.ts", + + "tests.html" + ], + "main": "tests.html", + "open": "app/hero-detail.component.spec.ts", + "tags": ["testing"] +} diff --git a/aio/content/guide/aot-compiler.md b/aio/content/guide/aot-compiler.md index 9a3205e77708..4bfbc24f9e40 100644 --- a/aio/content/guide/aot-compiler.md +++ b/aio/content/guide/aot-compiler.md @@ -510,7 +510,6 @@ Rollup does the tree shaking as before. The general audience instructions for running the AOT build of the Tour of Heroes app are not ready. The following instructions presuppose that you have downloaded the - Tour of Heroes' zip and run `npm install` on it. diff --git a/aio/content/guide/quickstart.md b/aio/content/guide/quickstart.md index 167449700e22..09d576055e08 100644 --- a/aio/content/guide/quickstart.md +++ b/aio/content/guide/quickstart.md @@ -26,7 +26,7 @@ You'll pursue these ends in the following high-level steps: --> -And you can also download the example. + diff --git a/aio/content/guide/rxjs.md b/aio/content/guide/rxjs.md new file mode 100644 index 000000000000..3d5f5ebfe432 --- /dev/null +++ b/aio/content/guide/rxjs.md @@ -0,0 +1,904 @@ +@title +RxJS in Angular + +@intro +Using Observables to manage asynchronous application events. + +@description + + + +**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 +including an example that you can run live in the browser. + + +{@a definition} + + +### _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"). + +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. +{@a learn-observables} + + +### Learning about _Observables_ + +There are numerous 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} + + +### _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. + +The `Observable` and the `Promise` are both techniques for coping with asynchronous processes. + +The similarity ends there, as the `Promise` and the `Observable` are more different then alike: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Promise + + Observable +
+ + + A `Promise` resolves to a single result (or error). + + + + An `Observable` can emit any number of events. It may never stop emitting values. +
+ + + The source of the `Promise` executes immediately. + + + + The `Observable` may emit events immediately ("hot") or wait until the first subscription ("cold"). +
+ + + The `then` method always executes its callback _asynchronously_. + + + + `Observable` methods and operators may execute _synchronously_ or _asynchronously_. +
+ + + You cannot _cancel_ or _retry_ the action. + + + + You can _cancel_ or _retry_ the action. +
+ + + You chain a sequence of promises with the `then` method. + + + + You chain observables with a variety of **operators**. +
+ + + 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". + + + + 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. +
+ + + `Promise` is native to JavaScript. + You don't need to import a library although you may need a shim for older browsers. + + + + `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. +
+ + +{@a operators} + + +### 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. + + + + + + + + +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. With this method you 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. This operator is commonly +used for flow control of Observable events that only match certain criteria. + +
+ +
+ +The `do` operator provides the Observable value to perform a side-effect. The `do` operator does not require a return value, as it provides the source +Observable to the next operator in the chain. This operator is useful for debugging, including console logging the current value in the chain and running arbitrary +actions on the observable. + +
+ +Had you not imported these operators before using them with the Observable returned by `getHeroes`, the Observable would throw an error. It would fail to perform these actions as the functions don't exist on the Observable prototype yet. + +
+ +Its important to avoid incomplete operator imports. A common example is having multiple files that use Observable operators but not all files imports the operators they need. Depending on how and when the files are imported, some Observable operators that are needed may not be available on the Observable prototype. Be diligent in making sure *each* file that +needs a set of operators imports them all. + +
+ +Another approach is to import the Observable operators directly and call them individually on the Observables. Let's +update the filtered heroes component to use direct imports. + + + + + + + + +This approach has no side-effects as you're not patching the Observable prototype. It also is +more conducive to [tree-shaking](https://en.wikipedia.org/wiki/Dead_code_elimination) versus patching the Observable prototype, where tree-shaking measures +cannot be applied. 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 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. + +
+ + + +The recommended approach is to import all 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} + + +### 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} + + +### 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. + + + + + + + + +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. + + + + + + + + +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. There is a better approach to managing subscriptions. Observables have `operators` +that complete observable streams. One such operator is the the `takeUntil` operator, that listens for one or more supplied Observables +to complete, then notifies the source Observable to complete also. + +Let's update our hero counter example to use the `takeUntil` operator. We import the operator to add it to the observable prototype. + + + + + + +Since we need an Observable that emits a value, we use a `Subject`. + + + + + + +
+ +A `Subject` is a special type of Observable that allows `multicasting`. Subjects use `multiscasting`, by keeping a list of registered observers and notifying +all observers each time a new value is emitted. This is different from a standard `Observable` which creates a new independent execution for each subscribed observer. + +
+ + + +You'll need to create an `onDestroy$` observable using the Subject. + + + + + + + + +Now we apply 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 a single observable triggers completion across multiple subscriptions. + + + + + + + + +{@a async-pipe} + + +### Async Pipe + +You manage Observables imperatively through manually subscribing and unsubscribing when ready to clean up their used resource. Observable subscriptions +are also managed declaratively in component templates using the `Async Pipe`. This lets us use Observables with less boilerplate and that's a good thing. +The `Async Pipe` creates a subscription to the observable each time it is updated, tracks reference changes to the Observable's emitted value and cleans up subscriptions +when the component is destroyed, protecting against memory leaks. + +Let's 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 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. + + + + + + + +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. + + + + + + + +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-unwrapped-observable} + + +### Sharing observable reference + +As stated previously, the `Async Pipe` creates a **new** subscription each time it is provided an Observable. The pipe unwraps the Observable and provides its +value to the template. This has a potential pitfall we want to avoid, which is creating multiple subscriptions for one resource. Let's look at a `HeroDetailComponent` +that uses the `Async Pipe` to retrieve a hero's details: + + + + + +
+ +The `mergeMap` operator maps each value to a new Observable, and merges all the inner Observables. Once merged, the resulting value is emitted as an Observable result. + +
+ +Looking at the template, you'll see the `Async Pipe` used 3 times. We also are using the `safe-navigation operator` on each hero attributes since its resolved asynchronously. This is not ideal as we are making 3 **separate** network requests to fetch the hero's details. Imagine if the hero had 10 different attributes we wanted to display? We don't want 10 requests. We only want to make one request to fetch the hero, and pass that data to the template. Using the **ngIfAs** syntax, you only use the async pipe once and assign its result to a template variable. Let's update the template using __ngIfAs__ statement to assign the result of the `hero$` Observable to the `hero` variable and update the hero name and hero id references. + + + + + +We still use the async pipe, but use it cleanly and more efficiently. + +{@a sharing-data} + + +### 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. It also lets you specify and initial or `seed` value that will be the first value produced when the Observable is subscribed to. + +
+ + +You'll import the `Injectable` decorator from `@angular/core` and the `BehaviorSubject` from the RxJS library to use it in the service. + + + + + + + + +The `scan` operator uses an accumulator function to collect the current value of the Observable and join it with the newly provided value. You use this +operator to provide an Observable of the accumulated values over time. + +You'll need an interface to provide consumers with to add messages to the event log. + + + + + + + + +Next, you'll create your service. Since a `BehaviorSubject` keeps the latest value for subscribers, you'll 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. The `scan` operator is collecting each newly pushed events and accumulating it with the current set of events in the array. + + + + + + + + +Now that you have a central place to collect events, you 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 use the `Async Pipe` here also to wire up the +stream to the template. + + + + + + + + +As with other services, you'll import the `EventAggregatorService` and `MessageLogComponent` and add it to the `AppModule` providers and declarations +arrays respectively. + + +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. + + + + + + + + +{@a 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 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`. + + + + + + + + +This is what the `HeroListComponent` currently looks like with no error handling and the simulated error. + + + + + + + + +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, and you use this to return an empty array or even a new Observable HTTP request. + +You'll also import the `of` operator, which you use to create an Observable sequence from a list of arguments. In this case, you're returning an empty array +of `Heroes` when an error occurs. + + + + + + + +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} + + +### 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. + + + + + + + +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. + + + + + + +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. + +
+ +As a general rule, you don't use the `retry` operator on all types of requests. Requests such as authentication wouldn't be retried as those requests should +be initiated by user action. We don't want to lock out user accounts with repeated login requests uninitiated by our users. + +
+ +## Testing + +So you've learned about using Observables, sharing data, error handling and more, but there is another piece to this picture, which is testing. Testing Observables +in Angular is similar to many other testing methods. You arrange the data or stream you need, you act on the intended target, and assert against the expected result. Let's look at how you would test the examples provided earlier in the chapter. + +You can run a live example of the tests provided below. + +{@a testing-stream-data} + +### Testing Observable Services + +First, let's look at the [sharing data with a stream](#sharing-data-with-a-stream) example. You want to verify the functionality of the service including its initial state, and that new entries are added into the stream each time the `add` method is used. If you're familiar with Angular's `TestBed`, this is a normal setup. You use the `TestBed` to setup the configure a testing module include the necessary providers, and use the `TestBed.get()` method to get a new instance of the `EventAggregatorService` before each test run. + + + + + +Now let's test the initial state of the Observable provided by your service through the `events$` property. You already know that retrieving data from an Observable involves _subscribing_ to it, and that's what you'll do in the test. Since you're using a `BehaviorSubject`, an Observable with a memory of its last value, you can subscribe to it. When the service is initially created, the `events$` stream is set to an empty array and that's what you'll validate against. + + + + + +Since each test gets a new instance of the `EventAggregatorService`, you get a new instance of the `events$` BehaviorSubject and just as expected, the initial value is an empty array. You also want to validate that items can be pushed into the stream, so next you'll test the `add()` method and check its results. + + + + + +This test requires a bit more setup, as you need to provide the `AppEvent` you want to push into the stream, calling the `add()` method to add a new item into the aggregated event stream, and _subscribing_ to the stream. The first argument in the `subscribe` callback will provide you with the latest data from your `BehaviorSubject`, which you assert against to verify the stream is producing what it should. + +### Testing Component Streams + +Along with testing services, you'll need to test data provided through Observables to your components. The [hero detail](#sharing-unwrapped-observable) component uses 2 Observables, the first retrieving route parameters from the `Router`, and then returning hero details through an Observable from the `Hero Service`. Since you're only testing the component itself, mocking out the `ActivatedRoute` and `HeroService` services will be sufficient. For the test setup, you'll create mocks of the mentioned services to provide replacement observables for the test. + + + + + + + +Looking at the test setup, the `MockActivateRoute` looks similar to the `ActivatedRoute` service used to provide the route parameters. The biggest difference is that the +`params` property provides a new instance of a `BehaviorSubject`. Since the `Router` provides the route parameters as an Observable, you want to mimic that behavior +without bringing in all the dependencies of the `ActivatedRoute`. The same goes for the `MockHeroService` implementation. The `HeroService` returns and Observable of the `Http` request made, but you are keeping the test shallow intentionally in this case. Now that the component is setup for testing, you can use the mock services to provide values during testing. + + + + + + +First, you want to test that `HeroService.getHero()` method is called when receiving route parameters. You don't need to call the actual `getHero()` method, so you'll use a [spy](https://jasmine.github.io/2.0/introduction.html#section-Spies) as replacement for its implementation. For the spy you return an `Observable.of(hero)`, that will immediately resolve the to the mock `Hero` defined in the setup. When you call `route.params.next()`, it sends a new value of `{ id: 1 }` to the Observable, and the `mergeMap` operator will map the result of the route parameters to your stub `Hero` Observable. Since the component template is using the `AsyncPipe`, when you use the `fixture.detectChanges()` method to trigger change detection, the `AsyncPipe` will subscribe to the `Observables` provided during the test, which in turn calls the `HeroService.getHero()` function. Lastly, we can assert that the method was successfully called. + +You also want to verify that the component is rendering the Observable correctly. + + + + + +The test setup is similar to the previous test, but now you're inspecting the rendered component's contents to verify that the `Observable`s provided are display correctly. +You want to verify that the `Hero` with and `ID` of `1`, is subscribed to by the component using the `AsyncPipe` and displays the hero details. + +### Marble Testing + + +RxJS also has a special [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) for testing Observables using "Marble tests". Marble testing allows you to test Observable sequences visually using [marble notation](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md#marble-syntax) to represent events happening over time. Marble testing is very powerful in that you compose events using the string-based marble syntax, it creates the Observables, whether they be `Hot` or `Cold` and can assert those Observables against the expected results in a given sequence. Let's look at the syntax for marble diagrams. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Notation + + Description +
+ + `-` + + + + Each dash is a _frame_ used to simulate a passage of _time_. Each dash represents 10 "frames" of time. Since `Observable`s can be asynchronous, + when testing time-based sequences, frames are similar to using the `tick` function in Angular's testing framework. + +
+ + `|` + + + + The pipe represents the `complete` notification from an Observable. This is the same as the Observable producer calling the `.complete()` method. + +
+ + `#` + + + + The pipe represents an `error` notification from an Observable. This is the same as the Observable producer calling the `.error()` method. + +
+ + `a` or any character + + + + All other characters represents a value being produced by an Observable. This is the same as the Observable producer calling the `.next()` method with a provided value. + +
+ + `()` or grouping + + + + Parenthesis are used to group events together in the _same frame_. Values and completion or error are grouped together using parenthesis. + +
+ + `^` or subscription point + + + + Only used when testing *Hot* Observables, the caret represents the point at which the observable was subscribed to. This also represents the "zero frame", where all frames before the `^` will be negative. + +
+ + `!` or unsubscription point + + + + Only used when testing *Subscriptions*, the exclamation represents the point at which the observable was unsubscribed from. + +
+ +You can learn more about marble tests below: + +* [Writing Marble Tests](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md) +* [Intro to RxJS Marble Testing](https://egghead.io/lessons/rxjs-introduction-to-rxjs-marble-testing) +* [Testing Race Condition Using RxJS Marbles](https://blog.nrwl.io/rxjs-advanced-techniques-testing-race-conditions-using-rxjs-marbles-53e7e789fba5) + +Underneath, marble tests use a `TestScheduler` to enable testing of asynchronous events in a synchronous way. RxJS provides marble testing as part of its internal testing framework, and those tools can be used external to the RxJS library. For the purposes of this guide, you can take advantage of an existing library called that packages up the marble testing framework and integrates it with the `Jasmine` test runner, aptly named [jasmine-marbles](https://github.com/synapse-wireless-labs/jasmine-marbles). + +You can run a live example of the tests provided below. + +Let's test the `HeroService` using marble testing. + + + + + +The testing setup is similar to a normal testing setup. Since you're unit testing and isolating the `HeroService` from outside dependencies, you mock out the `Http` service with the `get()` method using a jasmine spy. Since the `HeroService` only has one dependency, you could have skipped using the `TestBed` to inject dependencies, but as a convenience you'll use it to setup the providers and get instances of them. + +
+ +The `jasmine-marbles` package initializes the `TestScheduler` before each test, adds a custom Jasmine matcher `toBeObservable` for test assertions and resets the `TestScheduler` after each test, all of which are transparent when running tests. + +
+ +The `http.get()` method returns a _cold_ Observable, which does make the network request and produce a value until its subscribed to. You want to simulate this in a test, and the `cold` function is used to return a _cold_ Observable. Using the marble syntax, you can write a test to verify this behavior. + + + + + +Looking at the test, you'll notice a few things. The `data` variable is used to provide the return value, which is a mock object with a `json()` method to return a mock `Hero`. The `response$` set up the `cold` Observable using the marble diagram, which waits two frames, delivers the value `a` and then completes with the `|` character. You also substitute actual values for the characters in the marble diagram, using an object as the second argument in the `cold()` method. The object contains the `a` property, and its value being the `data` variable. Now you can see how marble diagrams shine. You can visually see and represent the sequence of events under test. The `expected$` is also a marble diagram of how you expect the Observable sequence to play out. + +The `toBeObservable` assertion method uses the `TestScheduler` to unwrap both marble diagram Observables and convert them to strings. The strings are then compared the for equality. + +
+ +There is also a `hot` method for testing _Hot_ Observables, which are Observables that produce events even before they are subscribed to. The signature is the same as the `cold()` method. + +
+ +Earlier in the chapter, you learned about the `retry` operator and how it retries a failed Observable a given number of times before returning an error. You can also verify the behavior of a sequence of retries. Let's simulate a failed request using the `HeroService.getHeroes()` method and a successful response after a retry using a marble diagram. + + + + + +To simulate an error response, you still use the same syntax, but use the `#` character to denote an error being returned. The third parameter in the `cold` method is the replacement value for the error in the marble diagram. Since the `retry` operator re-subscribes to the _source_ Observable, you need to provide a new Observable for each retry. The `expected$` marble diagram reflects that even though you received an error during the sequence, it was never propagated up and after the second set of frames, the heroes were returned. + +
+ +The `defer` Observable is an `Observable` factory, waiting until the `Observable` is subscribed to, calling the provided function and returning a _new_ instance of the `Observable` each for each subscriber. + +
+ +Testing that an error is returned after exhausting the number retries is also a strength of testing with marble diagrams. + + + + + +Instead of returning a successful response, the `response$` returns an error until all retry attempts have been made, and then the error is passed up the Observable chain. The `expected$` marble diagram, shows a set of frames for each retry attempt and ends with a grouped error and completion event. You can also assert the number of calls made in total. diff --git a/aio/content/guide/webpack.md b/aio/content/guide/webpack.md index 7ce543717648..223d6be084d2 100644 --- a/aio/content/guide/webpack.md +++ b/aio/content/guide/webpack.md @@ -54,7 +54,6 @@ This guide offers a taste of Webpack and explains how to use it with Angular app --> -You can also download the final result. {@a what-is-webpack} diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 0f18d7635ef6..5bc00310f1b5 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -261,6 +261,18 @@ "title": "HttpClient", "tooltip": "Use HTTP to talk to a remote server." }, + { + "url": "guide/rxjs", + "title": "Observables", + "tooltip": "Using Observables to manage event streams." + }, + + { + "url": "guide/pipes", + "title": "Pipes", + "tooltip": "Pipes transform displayed values within a template." + }, + { "url": "guide/router", "title": "Routing & Navigation", diff --git a/aio/tools/examples/shared/package.json b/aio/tools/examples/shared/package.json index 9498026223be..181c62ae1829 100644 --- a/aio/tools/examples/shared/package.json +++ b/aio/tools/examples/shared/package.json @@ -58,6 +58,7 @@ "http-server": "^0.9.0", "jasmine": "~2.4.1", "jasmine-core": "~2.4.1", + "jasmine-marbles": "^0.2.0", "karma": "^1.3.0", "karma-chrome-launcher": "^2.0.0", "karma-cli": "^1.0.1", diff --git a/aio/tools/examples/shared/yarn.lock b/aio/tools/examples/shared/yarn.lock index 8b7d2f063464..ccdaeb5ff553 100644 --- a/aio/tools/examples/shared/yarn.lock +++ b/aio/tools/examples/shared/yarn.lock @@ -3259,6 +3259,12 @@ jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" +jasmine-marbles@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.2.0.tgz#b893d8508b75790b634876d3a1bea1345d65c156" + dependencies: + lodash "^4.5.0" + jasmine@^2.5.3: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e"