From d6c01867bded31db283c2387685830e38ee62e4d Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 19 Feb 2017 21:10:59 -0600 Subject: [PATCH] Added async form validation example for framework apis --- .../rxjs/ts/src/app/add-hero.component.html | 14 ++++ .../rxjs/ts/src/app/add-hero.component.ts | 83 +++++++++++++++++++ .../rxjs/ts/src/app/app-routing.module.ts | 2 + .../rxjs/ts/src/app/app.component.ts | 1 + .../_examples/rxjs/ts/src/app/app.module.ts | 4 +- .../rxjs/ts/src/app/hero-counter.component.ts | 8 +- .../rxjs/ts/src/app/hero.service.1.ts | 7 +- .../rxjs/ts/src/app/hero.service.2.ts | 2 +- .../rxjs/ts/src/app/hero.service.3.ts | 7 +- .../rxjs/ts/src/app/hero.service.4.ts | 50 +++++++++++ .../_examples/rxjs/ts/src/app/hero.service.ts | 12 +++ public/docs/ts/latest/guide/rxjs.jade | 9 +- 12 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.html create mode 100644 public/docs/_examples/rxjs/ts/src/app/add-hero.component.ts create mode 100644 public/docs/_examples/rxjs/ts/src/app/hero.service.4.ts diff --git a/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html new file mode 100644 index 0000000000..77cd440258 --- /dev/null +++ b/public/docs/_examples/rxjs/ts/src/app/add-hero.component.html @@ -0,0 +1,14 @@ +

ADD HERO

+
+

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

+

+ +

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

RxJS in Angular

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