Angular模版驱动表单 & 响应式表单

杨旭 bio photo By 杨旭

Angular模版驱动表单 & 响应式表单

表单的作用:

  • 快速收集用户输入信息
  • 提供submit、reset等行为支持
  • 校验、提交等状态的收集

Angular的表单分类:模版驱动表单、响应式表单(模型驱动表单)

模版驱动表单

https://angular.io/docs/ts/latest/guide/forms.html

直接在组件模版中,通过内置的表单指令绑定到对象属性上。

优点:

  • 简单
  • 快速
  • 不需要太多的编码

缺点:

  • 控制能力差
  • 不方便添加自定义校验
  • 逻辑容易积累在模版中
export class Hero {
  constructor(
    public id: number,
    public name: string,
    public power: string,
    public alterEgo?: string
  ) {  }
}

@Component({
  selector: 'hero-form',
  templateUrl: './hero-form.component.html'
})
export class HeroFormComponent {

  powers = ['Really Smart', 'Super Flexible',
            'Super Hot', 'Weather Changer'];

  model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');

  onSubmit() { 
    // 调用后台服务,保存数据
  }
}
<form (ngSubmit)="onSubmit()" #heroForm="ngForm"> <!-- 绑定表单的submit事件 -->
  <div class="form-group">
    <label for="name">Name</label>
    <input type="text" class="form-control" id="name"
           required <!-- 自定义校验规则 -->
           [(ngModel)]="model.name" name="name" <!-- 双向绑定到组件属性 -->
           #name="ngModel"> <!-- 获取FormControl的引用 -->
    <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> <!-- 根据输入状态控制提示信息 -->
      Name is required
    </div>
  </div>
  <div class="form-group">
    <label for="power">Hero Power</label>
    <select class="form-control" id="power"
            required
            [(ngModel)]="model.power" name="power"
            #power="ngModel">
      <option *ngFor="let pow of powers" [value]="pow"></option> <!-- 显示下拉选项 -->
    </select>
    <div [hidden]="power.valid || power.pristine" class="alert alert-danger">
      Power is required
    </div>
  </div>
  <!-- type=submit出发submit事件; 获取整体表单的校验状态 -->
  <button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button> 
  <!-- 使用form对象的内置方法 -->
  <button type="button" class="btn btn-default" (click)="heroForm.reset()">Reset</button>
</form>
</div>

使用模型驱动表单,需要在AppModule中引入FormsModule

@NgModule({
  imports: [
    BrowserModule,
    FormsModule
  ],
  declarations: [
    AppComponent,
    HeroFormComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

响应式表单的使用:

  • 引入FormsModule
  • 使用ngModule绑定到组件的属性上
  • 使用ngForm和ngModel内置指令获取控制对象的引用
  • 使用默认的校验规则
  • 使用Angular管理的表单、输入状态来控制提示信息显示

内置校验规则

    /**
     * Validator that requires controls to have a non-empty value.
     */
    static required(control: AbstractControl): ValidationErrors | null;
    /**
     * Validator that requires control value to be true.
     */
    static requiredTrue(control: AbstractControl): ValidationErrors | null;
    /**
     * Validator that performs email validation.
     */
    static email(control: AbstractControl): ValidationErrors | null;
    /**
     * Validator that requires controls to have a value of a minimum length.
     */
    static minLength(minLength: number): ValidatorFn;
    /**
     * Validator that requires controls to have a value of a maximum length.
     */
    static maxLength(maxLength: number): ValidatorFn;
    /**
     * Validator that requires a control to match a regex to its value.
     */
    static pattern(pattern: string | RegExp): ValidatorFn;
    /**
     * No-op validator.
     */
    static nullValidator(c: AbstractControl): ValidationErrors | null;

内置的表单状态

响应式表单(模型驱动表单)

在组件中创建和控制表单的控制对象,可以直接向模版中推送数据和监听变化。

相对于模版驱动表单,具有如下优点:

  • 使用响应式编程范式
  • 灵活、可控
  • 状态的推送、获取是同步的
  • 便于添加自定义校验规则
  • 便于测试

缺点:

  • 一定的学习成本
  • 增加编码量
    this.heroForm = this.fb.group({
      name: ['', Validators.required],
      address: this.fb.group({
        street: '',
        city: '',
      })
    });
  <form [formGroup]="heroForm" novalidate>
    <div class="form-group">
      <label class="center-block">Name:</label>
      <input class="form-control" formControlName="name">
    </div>
    <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>
  </form>

模块中需要导入ReactiveFormsModule:

  imports: [
    CommonModule,
    ReactiveFormsModule
  ],

使用响应式表单:

  • 组件中通过FormBuilder服务,手动创建FormGroup和FormControl对象,并且指定校验规则
  • 模板中通过formGroup和formControlName指令绑定到组件的控件上

可以通过FormControl的API来控制表单的输入控件

    /**
     * Set the value of the form control to `value`.
     *
     * If `onlySelf` is `true`, this change will only affect the validation of this `FormControl`
     * and not its parent component. This defaults to false.
     *
     * If `emitEvent` is `true`, this
     * change will cause a `valueChanges` event on the `FormControl` to be emitted. This defaults
     * to true (as it falls through to `updateValueAndValidity`).
     *
     * If `emitModelToViewChange` is `true`, the view will be notified about the new value
     * via an `onChange` event. This is the default behavior if `emitModelToViewChange` is not
     * specified.
     *
     * If `emitViewToModelChange` is `true`, an ngModelChange event will be fired to update the
     * model.  This is the default behavior if `emitViewToModelChange` is not specified.
     */
    setValue(value: any, options?: {
        onlySelf?: boolean;
        emitEvent?: boolean;
        emitModelToViewChange?: boolean;
        emitViewToModelChange?: boolean;
    }): void;
    /**
     * Patches the value of a control.
     *
     * This function is functionally the same as {@link FormControl#setValue} at this level.
     * It exists for symmetry with {@link FormGroup#patchValue} on `FormGroups` and `FormArrays`,
     * where it does behave differently.
     */
    patchValue(value: any, options?: {
        onlySelf?: boolean;
        emitEvent?: boolean;
        emitModelToViewChange?: boolean;
        emitViewToModelChange?: boolean;
    }): void;
    /**
     * Resets the form control. This means by default:
     *
     * * it is marked as `pristine`
     * * it is marked as `untouched`
     * * value is set to null
     *
     * You can also reset to a specific form state by passing through a standalone
     * value or a form state object that contains both a value and a disabled state
     * (these are the only two properties that cannot be calculated).
     *
     * Ex:
     *
     * ```ts
     * this.control.reset('Nancy');
     *
     * console.log(this.control.value);  // 'Nancy'
     * ```
     *
     * OR
     *
     * ```
     * this.control.reset({value: 'Nancy', disabled: true});
     *
     * console.log(this.control.value);  // 'Nancy'
     * console.log(this.control.status);  // 'DISABLED'
     * ```
     */
    reset(formState?: any, options?: {
        onlySelf?: boolean;
        emitEvent?: boolean;
    }): void;
    /**
     * Register a listener for change events.
     */
    registerOnChange(fn: Function): void;
    /**
     * Register a listener for disabled events.
     */
    registerOnDisabledChange(fn: (isDisabled: boolean) => void): void;

监听页面输入变化

  const nameControl = this.heroForm.get('name');
  nameControl.valueChanges.forEach(
    (value: string) => this.nameChangeLog.push(value)
  );

自定义校验规则

创建校验函数

/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): {[key: string]: any} => {
    const name = control.value;
    const no = nameRe.test(name);
    return no ? {'forbiddenName': {name}} : null;
  };
}

响应式表单如何使用?

'name': [this.hero.name, [
    Validators.required,
    Validators.minLength(4),
    Validators.maxLength(24),
    forbiddenNameValidator(/bob/i)
  ]
],

模版驱动表单如何使用?

@Directive({
  selector: '[forbiddenName]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator, OnChanges {
  @Input() forbiddenName: string;
  private valFn = Validators.nullValidator;
  ngOnChanges(changes: SimpleChanges): void {
    const change = changes['forbiddenName'];
    if (change) {
      const val: string | RegExp = change.currentValue;
      const re = val instanceof RegExp ? val : new RegExp(val, 'i');
      this.valFn = forbiddenNameValidator(re);
    } else {
      this.valFn = Validators.nullValidator;
    }
  }
  validate(control: AbstractControl): {[key: string]: any} {
    return this.valFn(control);
  }
}
<input type="text" id="name" class="form-control"
       required minlength="4" maxlength="24" forbiddenName="bob"
       name="name" [(ngModel)]="hero.name" >