用表单处理用户输入是许多常见应用的基础功能。 应用通过表单来让用户登录、修改个人档案、输入敏感信息以及执行各种数据输入任务。

Angular表单的优点

  1. 收集用户输入的表单数据,在 Angular 中通过 ngModel 语法糖实现双向绑定非常方便;
    2. 通过各种验证器验证表单元素输入的数据是否合法,Angular 内置了常用的验证器(required、pattern、email,min,max,minlength,maxlength);

Angular 提供了两种不同的方法来通过表单处理用户输入:响应式表单和模板驱动表单。

响应式表单

首先在app.module.ts文件引入FormsModule,ReactiveFormsModule这两个模块

  1. // app.module.ts
  2. import { FormsModule, ReactiveFormsModule } from '@angular/forms';
  3. @NgModule({
  4. imports: [
  5. FormsModule,
  6. ReactiveFormsModule
  7. ]
  8. })

在需要加载表单的页面内引入:

  1. import { FormControl, FormGroup, Validators } from '@angular/forms';
  2. // FormControl 用于追踪/监控单个表单控件的值和验证状态。(例如:监控一个input框等)
  3. // FormGroup 把每个子 FormControl 的值聚合进一个对象,它的 key 是每个控件的名字。
  4. 用于追踪一个表单控件组的值和状态。(例如:监控一个form里面的所有input
  5. 它通过归集其子控件的状态值来计算出自己的状态。如果组中的任何一个控件是无效的,那么整个组就是无效的。

创建输入框

  1. <!-- .html -->
  2. <input type="text" [formControl]="job">
  1. // .ts
  2. job: any;
  3. ngOnInit() {
  4. this.job = new FormControl('');
  5. }

添加校验规则

  1. <!-- .html -->
  2. <input type="text" [formControl]="job">
  3. <span *ngIf="job.errors?.required">输入不能为空</span>
  4. <span *ngIf="job.errors?.maxlength">最多10个字符</span>
  1. // .ts
  2. job: any;
  3. ngOnInit() {
  4. this.job = new FormControl('', [Validators.required, Validators.maxLength(10)]);
  5. }

创建表单

响应式表单提供了 FormGroup 用于跟踪一组 FormControl 实例的值和有效性状态。FormGroup 的 key 就是每个子控件的名称,通过 [formControlName] 指令与视图中的表单元素关联。

  1. <!-- .html -->
  2. <div [formGroup]="myForm">
  3. <input type="text" formControlName="name">
  4. <div>
  5. <span *ngIf="myForm.controls.name.errors?.required">输入不能为空</span>
  6. <span *ngIf="myForm.controls.name.errors?.maxlength">最多10个字符</span>
  7. </div>
  8. </div>
  1. // .ts
  2. myForm: any;
  3. ngOnInit() {
  4. this.myForm = new FormGroup({
  5. name:new FormControl('', [Validators.required, Validators.maxLength(10)])
  6. })
  7. }

对表单分组

表单组可以同时接受单个表单控件实例和其它表单组实例作为其子控件。FormGroup 实例中可以嵌套 FormGroup 实例,形成嵌套的表单组。

  1. <!-- .html -->
  2. <div [formGroup]="myForm">
  3. <!-- <input type="text" formControlName="name">
  4. <div>
  5. <span *ngIf="myForm.controls.name.errors?.required">输入不能为空</span>
  6. <span *ngIf="myForm.controls.name.errors?.maxlength">最多10个字符</span>
  7. </div> -->
  8. <div formGroupName="username" class="form-group">
  9. <input type="text" placeholder="First Name" formControlName="first">
  10. <div>
  11. <span *ngIf="myForm.controls?.username.controls.first.errors?.required">输入不能为空</span>
  12. <span *ngIf="myForm.controls?.username.controls.first.errors?.maxlength">最多10个字符</span>
  13. </div>
  14. <input type="text" placeholder="Last Name" formControlName="last">
  15. <div>
  16. <span *ngIf="myForm.controls?.username.controls.last.errors?.required">输入不能为空</span>
  17. <span *ngIf="myForm.controls?.username.controls.last.errors?.maxlength">最多10个字符</span>
  18. </div>
  19. </div>
  20. <input type="password" formControlName="password">
  21. <div>
  22. <span *ngIf="myForm.controls.password.errors?.required">输入不能为空</span>
  23. <span *ngIf="myForm.controls.password.errors?.pattern">请输入6位数字</span>
  24. </div>
  25. </div>
  1. // .ts
  2. myForm: any;
  3. ngOnInit() {
  4. this.myForm = new FormGroup({
  5. name: new FormControl('', [Validators.required, Validators.maxLength(10)]),
  6. username: new FormGroup({
  7. first: new FormControl('', [Validators.required, Validators.maxLength(10)]),
  8. last: new FormControl('', [Validators.required, Validators.maxLength(10)]),
  9. }),
  10. password: new FormControl('', [Validators.required, Validators.pattern(/^\d{6}$/)])
  11. })
  12. }

使用 FormBuilder 服务生成控件

当我们需要创建多个表单控件实例时,代码会显得会比较臃肿。FormBuilder 提供了一个语法糖,以简化 FormControl、FormGroup 或 FormArray 实例的创建过程。下面我们用它来重构一下上面的表单创建

  1. // 引入FormBuilder
  2. import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
  3. constructor(private fb: FormBuilder) {
  4. this.myForm = fb.group({
  5. name: ['', [Validators.required, Validators.maxLength(10)]],
  6. username: fb.group({
  7. first: ['', [Validators.required, Validators.maxLength(10)]],
  8. last: ['', [Validators.required, Validators.maxLength(10)]],
  9. }),
  10. password: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]]
  11. })
  12. }

模板驱动表单

模板驱动表单和 AngularJS 中的表单相似,通过 NgModel 指令来实现双向绑定。
模板驱动方法,FormControl 对象会被 NgModel 指令隐式创建

创建表单

在使用 form 标签后,其中通过 NgModel 指令双向绑定的表单控件必须添加 name 属性。通过把 ngForm 导出成局部模板变量来查看表单的状态。

  1. <!-- .html -->
  2. <form #userInfo="ngForm">
  3. <input type="text" placeholder="Username" [(ngModel)]="username" required maxlength="10" name="username">
  4. <div>
  5. <span *ngIf="userInfo.controls.username?.errors?.required">输入不能为空</span>
  6. </div>
  7. <input type="password" placeholder="Password" [(ngModel)]="password" required pattern="^\d{6}$" name="password">
  8. <div>
  9. <span *ngIf="userInfo.controls.password?.errors?.required">输入不能为空</span>
  10. <span *ngIf="userInfo.controls.password?.errors?.pattern">请输入6位数字</span>
  11. </div>
  12. </form>
  13. <pre>Form Value: {{ userInfo.value | json }}</pre>
  14. <pre>Form Valid: {{ userInfo.valid }}</pre>
  1. // .ts
  2. username = '';
  3. password = '';


对表单分组

在模板驱动表单中对表单控件进行分组需要使用 NgModelGroup 指令。该指令只能用作 NgForm 的子级(在
标签内),相当于响应式表单中的 FormGroupName 指令

  1. <!-- .html -->
  2. <form #userInfo="ngForm">
  3. <div ngModelGroup="username" class="form-group">
  4. <input type="text" placeholder="First Name" [(ngModel)]="username.first" required maxlength="10" name="first">
  5. <div>
  6. <span *ngIf="firstEmptyShow(userInfo)">输入不能为空</span>
  7. </div>
  8. <input type="text" placeholder="Last Name" [(ngModel)]="username.last" required maxlength="10" name="last">
  9. <div>
  10. <span *ngIf="lastEmptyShow(userInfo)">输入不能为空</span>
  11. </div>
  12. </div>
  13. <input type="password" placeholder="Password" [(ngModel)]="password" required pattern="^\d{6}$" name="password">
  14. <div>
  15. <span *ngIf="userInfo.controls.password?.errors?.required">输入不能为空</span>
  16. <span *ngIf="userInfo.controls.password?.errors?.pattern">请输入6位数字</span>
  17. </div>
  18. </form>
  19. <pre>Form Value: {{ test(userInfo) }}</pre>
  20. <pre>Form Valid: {{ userInfo.valid }}</pre>
  1. // .ts
  2. username = { first: '', last: '' };
  3. password = '';
  4. firstEmptyShow(userInfo:any){
  5. return userInfo.controls.username?.controls.first.errors?.required
  6. }
  7. lastEmptyShow(userInfo:any) {
  8. return userInfo.controls.username?.controls.last.errors?.required
  9. }


表单验证

响应式表单验证

  1. constructor(private fb: FormBuilder) {
  2. this.myForm = fb.group({
  3. name: ['', [
  4. Validators.required,
  5. Validators.maxLength(10),
  6. customValidator // <-- Here's how you pass in the custom validator.]],
  7. })
  8. }

自定义验证器

  1. forbiddenAll(): ValidatorFn {
  2. return (control: AbstractControl): ValidationErrors | null => {
  3. console.log(control.value ? {forbiddenAll: {value: control.value}} : null)
  4. return control.value ? {forbiddenAll: {value: control.value}} : null;
  5. };
  6. }

模板驱动表单验证

通过把 ngModel 导出成局部模板变量来查看该控件的状态。 比如下面这个例子就把 NgModel 导出成了一个名叫 name 的变量:

  1. <input type="text" id="name" name="name" class="form-control"
  2. required minlength="4" appForbiddenName="bob"
  3. [(ngModel)]="hero.name" #name="ngModel">
  4. <div *ngIf="name.invalid && (name.dirty || name.touched)"
  5. class="alert">
  6. <div *ngIf="name.errors?.required">
  7. Name is required.
  8. </div>
  9. <div *ngIf="name.errors?.minlength">
  10. Name must be at least 4 characters long.
  11. </div>
  12. <div *ngIf="name.errors?.forbiddenName">
  13. Name cannot be Bob.
  14. </div>
  15. </div>

自定义验证器

创建一个自定义验证器

  1. ng generate directive forbidden-name

创建validator.ts文件,提供验证器算法

  1. // validator.ts
  2. import { ValidatorFn, AbstractControl } from '@angular/forms';
  3. export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  4. // 返回一个具有验证性函数
  5. return (control: AbstractControl): { [key: string]: any } | null => {
  6. console.log("control.value=",control.value)
  7. // control.value控件的值
  8. const forbidden = nameRe.test(control.value);
  9. console.log("forbidden=",forbidden)
  10. // 下面是三目运算符,当forbidden为false的时候,返回null,当forbidden为true的时候,返回{'forbiddenNAme':{value:control.value}}
  11. return forbidden ? { 'forbiddenName': { value: control.value } } : null;
  12. };
  13. }

在自定义验证器中继承该算法并实现一个接口

  1. // forbidden-name.directive.ts
  2. import { Directive, Input } from '@angular/core';
  3. import { NG_VALIDATORS, Validator, AbstractControl} from '@angular/forms';
  4. import { forbiddenNameValidator } from './main/validator';
  5. @Directive({
  6. selector: '[appForbiddenName]', // 指令作为属性使用
  7. // 实现Validator接口之后,要能够给全局的模板都能够实现,要注册。注册的密钥是angular固定提供的一种NG_VALIDATORS。注册的具体语句:
  8. providers:[{provide:NG_VALIDATORS,useExisting:ForbiddenNameDirective,multi:true}]
  9. })
  10. export class ForbiddenNameDirective implements Validator {
  11. @Input('appForbiddenName') forbiddenName: string = ''; // 获取指令值
  12. // 指令实现接口,也就是说,在调用这条指令的时候,就已经能够启动这个函数
  13. validate(control: AbstractControl): {[key: string]: any} | null {
  14. // 键值对:map[key:string]any {[key: string]: any} | null接收返回值的类型有键值对或者是NULL
  15. console.log("control:",control)
  16. console.log("forbiddenName=",this.forbiddenName)
  17. // 还是三目运算符,this.forbidden为空的时候,不进行匹配
  18. return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control): null;
  19. }
  20. constructor() { }
  21. }

使用模板自定义的方法

  1. <form #userInfo="ngForm">
  2. <input type="text" placeholder="Username" [(ngModel)]="username" required maxlength="10" name="username" appForbiddenName="bob">
  3. <div>
  4. <span *ngIf="userInfo.controls.username?.errors?.required">输入不能为空</span>
  5. <!-- <span *ngIf="userInfo.controls.username?.errors?.required">输入不能为空</span> -->
  6. <span *ngIf="userInfo.controls.username?.errors?.forbiddenName">no bob</span>
  7. </div>
  8. <input type="password" placeholder="Password" [(ngModel)]="password" required pattern="^\d{6}$" name="password">
  9. <div>
  10. <span *ngIf="userInfo.controls.password?.errors?.required">输入不能为空</span>
  11. <span *ngIf="userInfo.controls.password?.errors?.pattern">请输入6位数字</span>
  12. </div>
  13. </form>

总结

响应式表单和模板驱动表单以不同的方式处理和管理表单数据。每种方法都有各自的优点。

响应式表单提供对底层表单对象模型直接、显式的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。如果表单是你的应用程序的关键部分,或者你已经在使用响应式表单来构建应用,那就使用响应式表单。

模板驱动表单依赖模板中的指令来创建和操作底层的对象模型。它们对于向应用添加一个简单的表单非常有用,比如电子邮件列表注册表单。它们很容易添加到应用中,但在扩展性方面不如响应式表单。如果你有可以只在模板中管理的非常基本的表单需求和逻辑,那么模板驱动表单就很合适。

动态表单

工作中可能会存在以下场景:
在确定表单使用的组件的情况下,根据后端返回的数据,自由排列组合生成一个新表单:

比如后端返回了以下数据,规定了每种数据需要使用的控件类型:

  1. // mockData
  2. response = [
  3. {
  4. key: 'username',
  5. controlType: 'input',
  6. label: '用户名',
  7. value: ''
  8. },
  9. {
  10. key: 'area',
  11. controlType: 'select',
  12. label: '地区',
  13. option: [{
  14. label: '国内',
  15. value: 'china'
  16. },{
  17. label: '国外',
  18. value: 'foreign'
  19. }],
  20. value: ''
  21. },
  22. {
  23. key: 'isAdult',
  24. controlType: 'radio',
  25. label: '是否成年',
  26. checkList: [{
  27. label: 'Yes',
  28. value: 'yes'
  29. },{
  30. label: 'No',
  31. value: 'no'
  32. }],
  33. value: ''
  34. }
  35. ]

我们只需要根据这个数据生成一个formgroup:

  1. //循环控件加到formgroup上
  2. toFormGroup(){
  3. let group: any = {};
  4. this.response.forEach(item => {
  5. group[item.key] = new FormControl("");
  6. });
  7. this.dynamicForm = new FormGroup(group);
  8. }

在form上绑定这个formGroup,并通过ngSwitch根据条件按需加载即可:

  1. <form (ngSubmit)="onSubmit()" [formGroup]="dynamicForm">
  2. <div *ngFor="let item of response">
  3. <label>{{item.label}}:</label>
  4. <div [ngSwitch]="item.controlType">
  5. <!--通过判断类型来显示哪种表单-->
  6. <input *ngSwitchCase="'input'" type="text" [(ngModel)]="item.value" [formControlName]="item.key">
  7. <select *ngSwitchCase="'select'" [formControlName]="item.key">
  8. <option *ngFor="let option of item.option" [ngValue]="option">
  9. {{ option.label }}
  10. </option>
  11. </select>
  12. <div *ngSwitchCase="'radio'">
  13. <div *ngFor="let child of item.checkList">
  14. <input type="radio" [value]="child.value" [formControlName]="item.key">{{ child.label }}
  15. </div>
  16. </div>
  17. </div>
  18. </div>
  19. <button type="submit">保存</button>
  20. </form>