Angular之响应式表单 ( Reactive Forms )
项目结构
一 首页 ( index.html )
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Angular4ReactiveForm</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-hero-list></app-hero-list> </body> </html>
二 根模块 ( app.module.ts )
import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { NgModule } from '@angular/core'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ declarations: [ HeroListComponent, HeroDetailComponent ], imports: [ BrowserModule, ReactiveFormsModule ], providers: [HeroService], bootstrap: [HeroListComponent] }) export class AppModule { }
三 列表脚本 ( hero-list.component.ts )
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { finalize } from 'rxjs/operators'; import { Hero } from '../model/model'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-hero-list', templateUrl: './hero-list.component.html', styleUrls: ['./hero-list.component.css'] }) export class HeroListComponent implements OnInit { isLoading = false; heroes: Observable<Hero[]>; selectedHero: Hero; constructor(public heroService: HeroService) { } ngOnInit() { } /** * 获取Hero列表 * * @memberof HeroListComponent */ getHeroes() { this.isLoading = true; this.heroes = this.heroService.getHeroes() .pipe(finalize(() => this.isLoading = false)); this.selectedHero = null; } /** * 选择Hero * * @param {Hero} hero * @memberof HeroListComponent */ select(hero: Hero) { this.selectedHero = hero; } }
四 列表模版 ( hero-list.component.html )
<h3 *ngIf="isLoading"> <i>Loading heroes ... </i> </h3> <h3 *ngIf="!isLoading"> <i>Select a hero</i> </h3> <nav> <button (click)="getHeroes();" class="btn btn-primary">Refresh</button> <a *ngFor="let hero of heroes | async" (click)="select(hero);">{{hero.name}}</a> </nav> <div *ngIf="selectedHero"> <hr/> <h2>Hero Detail</h2> <h3>Editing:{{selectedHero.name}}</h3> <app-hero-detail [hero]="selectedHero"></app-hero-detail> </div>
五 详情脚本 ( hero-detail.component.ts )
import { Component, OnInit, Input, OnChanges, OnDestroy } from '@angular/core'; import { Hero, Address } from '../model/model'; import { FormBuilder, FormGroup, FormArray, AbstractControl, FormControl } from '@angular/forms'; import { HeroService } from '../hero.service'; import { provinces } from '../model/model'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit, OnChanges, OnDestroy { @Input() hero: Hero; heroForm: FormGroup; provinces: string[] = provinces; nameChangeLog: string[] = []; constructor(private fb: FormBuilder, private heroService: HeroService) { this.createForm(); this.logNameChanges(); } /** * * getter方法:从而可以直接访问secretLairs * @readonly * @type {FormArray} * @memberof HeroDetailComponent */ get secretLairs(): FormArray { return <FormArray>this.heroForm.get('secretLairs'); } ngOnInit() { // 单击Hero按钮,选择Hero时执行 console.log('详情页面初始化'); } ngOnDestroy(): void { // 单击Refresh按钮,重新获取Hero列表时执行 console.log('详情页面销毁'); } ngOnChanges() { this.rebuildForm(); } createForm() { this.heroForm = this.fb.group({ name: '', secretLairs: this.fb.array([]), power: '', sidekick: '' }); } /** * * 选择英雄、还原表单时重置表单 * @memberof HeroDetailComponent */ rebuildForm() { this.heroForm.reset({ // 将字段标记为pristine、untouched name: this.hero.name }); this.setAddress(this.hero.addresses); } /** * * 设置表单的地址 * @param {Address[]} addresses * @memberof HeroDetailComponent */ setAddress(addresses: Address[]) { const addressFormGroups = addresses.map(address => this.fb.group(address)); const addressForArray = this.fb.array(addressFormGroups); this.heroForm.setControl('secretLairs', addressForArray); } /** * 新增一个地址 * * @memberof HeroDetailComponent */ addLair() { this.secretLairs.push(this.fb.group(new Address())); } /** * 保存表单 * * @memberof HeroDetailComponent */ save() { this.hero = this.prepareCopyHero(); this.heroService.updateHero(this.hero).subscribe( (val) => { // 成功 }, (err) => { // 出错 }); this.rebuildForm(); } /** * 深度复制Hero对象 * * @returns {Hero} * @memberof HeroDetailComponent */ prepareCopyHero(): Hero { const formModel: any = this.heroForm.value; // AbstractControl是FormGroup、FormArray、FormControl的基类 const secrectLairDeepCopy: Address[] = formModel.secretLairs.map( (address: Address) => Object.assign({}, address) ); const savedHero: Hero = { id: this.hero.id, name: formModel.name, addresses: secrectLairDeepCopy }; return savedHero; } /** * 还原表单 * * @memberof HeroDetailComponent */ revert() { this.rebuildForm(); } /** * 订阅valueChanges属性( Observale对象 ),监控详情页面名称的变化,选择英雄、输入名称时执行 * * @memberof HeroDetailComponent */ logNameChanges() { const nameControl: FormControl = <FormControl>this.heroForm.get('name'); nameControl.valueChanges.forEach((val: string) => this.nameChangeLog.push(val)); } }
六 详情模版 ( hero-detail.component.html )
<form [formGroup]='heroForm'> <!-- 按钮 --> <div style="margin-bottom: 1em;"> <button type="button" (click)="save();" [disabled]="heroForm.pristine" class="btn btn-success">Save</button> <button type="button" (click)="revert();" [disabled]="heroForm.pristine" class="btn btn-success">Revert</button> </div> <!-- 名称 --> <div class="form-group"> <label class="center-block">Name: <input class="form-control" formControlName="name" /> </label> </div> <!-- 地址循环开始 --> <div formArrayName="secretLairs" class="well well-lg"> <div *ngFor="let address of secretLairs.controls;let i = index;" [formGroupName]="i"> <h4>Address #{{i+1}}</h4> <div style="margin-left: 1em;"> <div class="form-group"> <label class="center-block">Street: <input class="form-control" formControlName="street" /> </label> </div> <div class="form-group"> <label class="center-block">City: <input class="form-control" formControlName="city" /> </label> </div> <div class="form-group"> <label class="center-block">Province: <select class="form-control" formControlName="province"> <option *ngFor="let province of provinces" [value]="province">{{province}}</option> </select> </label> </div> <div class="form-group"> <label class="center-block">Zip Code: <input class="form-control" formControlName="zip" /> </label> </div> </div> </div> <button (click)="addLair();" type="button">Add a Secret Lair</button> </div> <!-- 地址循环结束 --> </form> <p>heroForm value: {{heroForm.value | json}}</p> <h4>Name change log</h4> <ul> <li *ngFor="let name of nameChangeLog">{{name}}</li> </ul>
七 服务脚本 ( hero.service.ts )
import { Injectable } from '@angular/core'; import { of } from 'rxjs/observable/of'; import { delay } from 'rxjs/operators'; import { Hero, heroes } from './model/model'; import { Observable } from 'rxjs/Observable'; @Injectable() export class HeroService { delayMs = 500; constructor() { } /** * 获取Hero对象列表 * * @returns {Observable<Hero[]>} * @memberof HeroService */ getHeroes(): Observable<Hero[]> { return of(heroes).pipe(delay(this.delayMs)); } /** * 更新Hero对象 * * @param {Hero} hero * @returns {Observable<Hero>} * @memberof HeroService */ updateHero(hero: Hero): Observable<Hero> { const oldHero = heroes.find(h => h.id === hero.id); const newHero = Object.assign(oldHero, hero); // 潜复制 return of(newHero).pipe(delay(this.delayMs)); } }
八 数据模型 ( model.ts )
export class Hero { constructor(public id: number, public name: string, public addresses: Address[]) { } } export class Address { constructor(public province?: string, public city?: string, public street?: string, public zip?: number) { } } export const heroes: Hero[] = [ new Hero(1, 'Whirlwind', [ new Address('山东', '青岛', '东海路', 266000), new Address('江苏', '苏州', '干将路', 215000) ]), new Hero(2, 'Bombastic', [ new Address('福建', '厦门', '环岛路', 361000) ]), new Hero(3, 'Magneta', []) ]; export const provinces: string[] = ['山东', '江苏', '福建', '四川'];