Angular 表单

407 阅读12分钟

在 Angular 中,表单有两种类型,分别为模板驱动和模型驱动。本文作为 Angular 表单处理的入门文章,带您了解 Angular 中提供的强大的表单处理机制。

1.1 模板驱动

1.1.1 概述

表单的控制逻辑写在组件模板中,适合简单的表单类型。

1.1.2 快速上手

  1. 引入依赖模块 FormsModule

    import { FormsModule } from "@angular/forms"
    
    @NgModule({
      imports: [FormsModule],
    })
    export class AppModule {}
    
  2. 将 DOM 表单转换为 ngForm

    <form #f="ngForm" (submit)="onSubmit(f)"></form>
    
  3. 声明表单字段为 ngModel

    <form #f="ngForm" (submit)="onSubmit(f)">
      <input type="text" name="username" ngModel />
      <button type="submit">提交</button>
    </form>
    

    input 这里必须加上 ngModel 这个指令,否则 username 这个 field 不会出现在 formref 的变量 f 中。

  4. 获取表单字段值

    import { NgForm } from "@angular/forms"
    
    export class AppComponent {
      onSubmit(form: NgForm) {
        console.log(form.value)
      }
    }
    

    注意 formRef 的类型定义为 NgForm.

  5. 表单分组

      <form #f="ngForm" (submit)="onSubmit(f)">
        <div ngModelGroup="usernameG">
          <input type="text" name="username" ngModel />
        </div>
        <div ngModelGroup="passwordG">
          <input type="password" name="password" ngModel />
        </div>
        <button type="submit">提交</button>
      </form>
    

    使用 ngModelGroup 之后 formRef.value 的数据结构也发生了相应的改变。

    image.png

1.1.3 表单验证

  • required 必填字段; 但是不会自动添加*,这一点要分清楚。
  • minlength 字段最小长度。
  • maxlength 字段最大长度。
  • pattern 验证正则 例如:pattern="\d" 匹配一个数值。
<form #f="ngForm" (submit)="onSubmit(f)">
  <input type="text" name="username" ngModel required pattern="\d" />
  <button type="submit">提交</button>
</form>
export class AppComponent {
  onSubmit(form: NgForm) {
    // 查看表单整体是否验证通过
    console.log(form.valid)
  }
}

下面这个功能在 react 中实现起来是相当困难的,但是在 angular 中就很容易[disabled]="f.invalid"。非常好用!

<!-- 表单整体未通过验证时禁用提交表单 -->
<button type="submit" [disabled]="f.invalid">提交</button>

在组件模板中显示表单项未通过时的错误信息。

  <form #f="ngForm" (submit)="onSubmit(f)">
    <div ngModelGroup="usernameG">
      <input
        type="text"
        name="username"
        #username="ngModel"
        ngModel
        required
        pattern="\d+"
      />
      <div *ngIf="username.touched && !username.valid && username.errors">
        <span *ngIf="username.errors['required']"> 请输入用户名 </span>
        <span *ngIf="username.errors['pattern']"> 用户名不合法 </span>
      </div>
    </div>
    <div ngModelGroup="passwordG">
      <input type="password" name="password" ngModel />
    </div>
    <button type="submit" [disabled]="f.invalid">提交</button>
  </form>

这里我们指定了 input 的 ref 值为 username. 注意区分其与 name="username" 的不同;紧接着,我们通过这个 ref 就可以拿到 input 的状态,是不是被修改过,是不是没有通过验证,是不是发生了错误?

只有被修改过,没有通过测试并且发生了错误的 input 才会在它的下面展示出错误信息。 上面的代码中用到的 touched errors valid errors['require'] 都不是随便写的。 注意指定 ref 的方式为 #username="ngModel"。

指定表单项未通过验证时的样式。

input.ng-touched.ng-invalid {
  border: 2px solid red;
}

总结上面的知识点:

  1. 如何指定整个表单的 ref
  2. 如何提交表单并调用 onSubmit 函数,传递表单的 ref
  3. 如何给 input 指定 ref
  4. 如何给 input 做双向绑定
  5. 如何给 input 指定 name
  6. 如何给表单元素设置校验规则
  7. 如何通过 input 的 ref 获取其状态,校验状态
  8. 如何动态显示错误提示
  9. 如何将提交按钮动态置灰
  10. 如何将表单元素分组及对应的表单数据结构
  11. 如何设置没有通过检测的 input 的提示样式
  12. 使用模板表单需要的支持 Module 有哪些

1.2 模型驱动

在使用模板表单的时候,我们是在 html 文件中构建好表单的基本结构,然后在模板中通过设定 ref 的方式获取特定表单元素或者表单信息或者给它们添加交互的。实际上在这个过程中,在看不到的地方会自动生成相对应 html 中 form 的一个数据结构,称为:表单模型。在模板驱动中,我们创建了模板,自动生成了模型。但是 html 的模板是比较固定的,不容易发生改变,这样,在需要动态变化的地方这种创建表单的方式就显得力不从心了,这个时候,我们不妨直接操作表单模型对象,以配置的方式生成表单,这样会灵活的的多。同时,我们也必须使用完全不同的 Angular 指令和操作表单的方式。不仅如此,在使用模板表单的时候我们用的是 FormsModule, 而在使用模型表单的时候提供支持的是 ReactiveFormsModule 这个模型

1.2.1 概述

表单的控制逻辑写在组件类中,对验证逻辑拥有更多的控制权,适合复杂的表单的类型

在模型驱动表单中,表单字段需要是 FormControl 类的实例,实例对象可以验证表单字段中的值,值是否被修改过等等。

<img src="./images/6.jpg" alt="转存失败,建议直接上传图片文件">

一组表单字段构成整个表单,整个表单需要是 FormGroup 类的实例,它可以对表单进行整体验证。

<img src="./images/7.jpg" alt="转存失败,建议直接上传图片文件">

使用模型表单有三大要点,分别是:FormControl FormGroup FormArray

  1. FormControl:表单组中的一个表单项。
  2. FormGroup:表单组,表单至少是一个 FormGroup。
  3. FormArray:用于复杂表单,可以动态添加表单项或表单组,在表单验证时,FormArray 中有一项没通过,整体没通过。

1.2.2 快速上手

使用模型创建表单刚好和模板的过程是相反的,使用模型创建表单的过程是从配置项到表单结构的过程。所以从代码上来看就是从 class 到 html 的过程。这个过程给了开发者极大的操作表单的自由度!

  1. 引入 ReactiveFormsModule

    import { ReactiveFormsModule } from "@angular/forms"
    
    @NgModule({
      imports: [ReactiveFormsModule]
    })
    export class AppModule {}
    
  2. 在组件类中创建 FormGroup 表单控制对象

    import { FormControl, FormGroup } from "@angular/forms"
    
    export class AppComponent {
      contactForm: FormGroup = new FormGroup({
        name: new FormControl(),
        phone: new FormControl()
      })
    }
    
  3. 关联组件模板中的表单

    <form [formGroup]="contactForm" (submit)="onSubmit()">
      <input type="text" formControlName="name" />
      <input type="text" formControlName="phone" />
      <button type="submit">提交</button>
    </form>
    
  4. 获取表单值

    export class AppComponent {
      onSubmit() {
        console.log(this.contactForm.value)
      }
    }
    
  5. 设置表单默认值

    contactForm: FormGroup = new FormGroup({
      name: new FormControl("默认值"),
      phone: new FormControl(15888888888)
    })
    
  6. 表单分组

    contactForm: FormGroup = new FormGroup({
      fullName: new FormGroup({
        firstName: new FormControl(),
        lastName: new FormControl()
      }),
      phone: new FormControl()
    })
    
    <form [formGroup]="contactForm" (submit)="onSubmit()">
      <div formGroupName="fullName">
        <input type="text" formControlName="firstName" />
        <input type="text" formControlName="lastName" />
      </div>
      <input type="text" formControlName="phone" />
      <button>提交</button>
    </form>
    
    onSubmit() {
      console.log(this.contactForm.value.name.username)
      console.log(this.contactForm.get(["name", "username"])?.value)
    }
    

如果使用的是模型表单,那么实现之前的模板表单功能的代码如下所示:

  <form [formGroup]="concatForm" (submit)="onSubmit()">
    <div formGroupName="usernameG">
      <input
        type="text"
        formControlName="username"
        required
        pattern="\d+"
      />
      <div
        *ngIf="username && username.touched && !username.valid && username.errors"
      >
        <span *ngIf="username.errors['required']"> 请输入用户名 </span>
        <span *ngIf="username.errors['pattern']"> 用户名不合法 </span>
      </div>
    </div>
    <div formGroupName="passwordG">
      <input formControlName="password" type="password" />
    </div>
    <button type="submit" [disabled]="concatForm.invalid">提交</button>
  </form>
  usernameControl: FormControl = new FormControl();
  concatForm: FormGroup = new FormGroup({
    usernameG: new FormGroup({
      username: this.usernameControl
    }),
    passwordG: new FormGroup({
      password: new FormControl('123456'),
    })
  });

  get username() {
    return this.concatForm.get('username');
  }

  onSubmit() {
    console.log('Form: ', this.concatForm)
    console.log('Value: ', this.concatForm.value)
  }

注意这里我们使用的不再是 ref 或者 ngModel, 取而代之的是 [formGroup]="concatForm" formGroupName="usernameG" formControlName="username" 这些新的指令。同时还需要注意的是 html 中用到的 username 不再是与 input 绑定的 ref, 而是 class 中提供的 getter 方法:

  get username() {
    return this.concatForm.get('username');
  }

整个表单模型对应的是一个 FormGroup 实例对象;而每一个表单元素对应的是一个 FormControl 对象;并且 FormGroup 是可以相互嵌套的,FormGroup 作为 FormControl 的组织结构,而 FormControl 作为每个表单元素的引用。因此,上述 getter 还可以写成如下形式:

  get username() {
    return this.usernameControl;
    // return this.concatForm.get('username');
  }

如何获取 FormGroup 所包裹的所有的 FormControl 呢?假设 group 是某个 FormGroup 的 ref,那么通过 group.controls 就可以得到此 group 管理下的所有的 controls.

1.2.3 内置表单验证器

使用模型表单,不仅可以方便的设置默认值,还可以方便的指定数据校验规则。而在模板表单中,我们需要使用大量的浏览器内置校验规则,这严重限制了校验时候的灵活性。

  1. 使用内置验证器提供的验证规则验证表单字段

    import { FormControl, FormGroup, Validators } from "@angular/forms"
    
    contactForm: FormGroup = new FormGroup({
      name: new FormControl("默认值", [
        Validators.required,
        Validators.minLength(2)
      ])
    })
    
  2. 获取整体表单是否验证通过

    onSubmit() {
      console.log(this.contactForm.valid)
    }
    
    <!-- 表单整体未验证通过时禁用表单按钮 -->
    <button [disabled]="contactForm.invalid" type="submit">提交</button>
    
  3. 在组件模板中显示为验证通过时的错误信息

    get name() {
      return this.contactForm.get("name")!
    }
    
    <form [formGroup]="contactForm" (submit)="onSubmit()">
      <input type="text" formControlName="name" />
      <div *ngIf="name.touched && name.invalid && name.errors">
        <div *ngIf="name.errors.required">请填写姓名</div>
        <div *ngIf="name.errors.maxlength">
          姓名长度不能大于
          {{ name.errors.maxlength.requiredLength }} 实际填写长度为
          {{ name.errors.maxlength.actualLength }}
        </div>
      </div>
    </form>
    

1.2.4 自定义同步表单验证器

  1. 自定义验证器的类型是 TypeScript 类
  2. 类中包含具体的验证方法,验证方法必须为静态方法
  3. 验证方法有一个参数 control,类型为 AbstractControl。其实就是 FormControl 类的实例对象的类型
  4. 如果验证成功,返回 null
  5. 如果验证失败,返回对象,对象中的属性即为验证标识,值为 true,标识该项验证失败
  6. 验证方法的返回值为 ValidationErrors | null
import { AbstractControl, ValidationErrors } from "@angular/forms"

export class NameValidators {
  // 字段值中不能包含空格
  static cannotContainSpace(control: AbstractControl): ValidationErrors | null {
    // 验证未通过
    if (/\s/.test(control.value)) return { cannotContainSpace: true }
    // 验证通过
    return null
  }
}
import { NameValidators } from "./Name.validators"

contactForm: FormGroup = new FormGroup({
  name: new FormControl("", [
    Validators.required,
    NameValidators.cannotContainSpace
  ])
})
<div *ngIf="name.touched && name.invalid && name.errors">
	<div *ngIf="name.errors.cannotContainSpace">姓名中不能包含空格</div>
</div>

这样一来,可以继续简化代码,实现和模板一样的功能的代码就继续可以简化成如下所示的模样:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Form, FormControl, FormGroup, FormsModule, NgForm, ReactiveFormsModule, Validators } from "@angular/forms"
import { CommonModule } from '@angular/common';

import { AbstractControl, ValidationErrors } from "@angular/forms"

export class CustomValidators {
  // 字段值中不能包含空格
  static number(control: AbstractControl): ValidationErrors | null {
    // 验证未通过
    if (control.value && !/^\d+$/.test(control.value)) return { number: true }
    // 验证通过
    return null
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
    FormsModule,
    ReactiveFormsModule,
    CommonModule,
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'myForm';
  usernameControl: FormControl = new FormControl('', [
    Validators.required,
    CustomValidators.number,
  ]);
  concatForm: FormGroup = new FormGroup({
    usernameG: new FormGroup({
      username: this.usernameControl
    }),
    passwordG: new FormGroup({
      password: new FormControl('123456'),
    })
  });

  get username() {
    return this.usernameControl;
    // return this.concatForm.get('username');
  }

  onSubmit() {
    console.log('Form: ', this.concatForm)
    console.log('Value: ', this.concatForm.value)
  }
}

  <form [formGroup]="concatForm" (submit)="onSubmit()">
    <div formGroupName="usernameG">
      <input type="text" formControlName="username" />
      <div
        *ngIf="username && username.touched && !username.valid && username.errors"
      >
        <span *ngIf="username.errors['required']"> 请输入用户名 </span>
        <span *ngIf="username.errors['number']"> 用户名不合法 </span>
      </div>
    </div>
    <div formGroupName="passwordG">
      <input formControlName="password" type="password" />
    </div>
    <button type="submit" [disabled]="concatForm.invalid">提交</button>
  </form>

我们可以从 username.errors 这个对象中读取很多其他信息,比如自定义的表单验证项提供的错误信息等,通过 *.errors 可以将表单做的更加的用户友好。

1.2.5 自定义异步表单验证器

import { AbstractControl, ValidationErrors } from "@angular/forms"
import { Observable } from "rxjs"

export class NameValidators {
  static shouldBeUnique(control: AbstractControl): Promise<ValidationErrors | null> {
    return new Promise(resolve => {
      if (control.value == "admin") {
         resolve({ shouldBeUnique: true })
       } else {
         resolve(null)
       }
    })
  }
}
contactForm: FormGroup = new FormGroup({
    name: new FormControl(
      "",
      [
        Validators.required
      ],
      NameValidators.shouldBeUnique
    )
  })
<div *ngIf="name.touched && name.invalid && name.errors">
  <div *ngIf="name.errors.shouldBeUnique">用户名重复</div>
</div>
<div *ngIf="name.pending">正在检测姓名是否重复</div>

使用异步表单数据验证器可以实现防重复在内的一系列有用的表单验证功能,结合自定义 Validator 返回值的 message 字段,可以对验证失败信息进行更加简洁的展示:

  get errorMsg() {
    if (!this.username?.errors) return '';
    return this.username.errors["message"] ?? "请输入用户名"
  }
<div *ngIf="errorMsg">{{ errorMsg }}</div>

1.2.6 FormArray

FormArray 是使用模型表单动态性的良好体现。使用 FormArray 我们可以实现模板表单不具备的动态添加表单项的高级功能,用在复杂交互场景下,提供更好的用户体验。

需求:在页面中默认显示一组联系方式,通过点击按钮可以添加更多联系方式组。

import { Component, OnInit } from "@angular/core"
import { FormArray, FormControl, FormGroup } from "@angular/forms"
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styles: []
})
export class AppComponent implements OnInit {
  // 表单
  contactForm: FormGroup = new FormGroup({
    contacts: new FormArray([])
  })

  get contacts() {
    return this.contactForm.get("contacts") as FormArray
  }

  // 添加联系方式
  addContact() {
    // 联系方式
    const myContact: FormGroup = new FormGroup({
      name: new FormControl(),
      address: new FormControl(),
      phone: new FormControl()
    })
    // 向联系方式数组中添加联系方式
    this.contacts.push(myContact)
  }

  // 删除联系方式
  removeContact(i: number) {
    this.contacts.removeAt(i)
  }

  ngOnInit() {
    // 添加默认的联系方式
    this.addContact()
  }

  onSubmit() {
    console.log(this.contactForm.value)
  }
}
<form [formGroup]="contactForm" (submit)="onSubmit()">
  <div formArrayName="contacts">
    <div
      *ngFor="let contact of contacts.controls; let i = index"
      [formGroupName]="i"
    >
      <input type="text" formControlName="name" />
      <input type="text" formControlName="address" />
      <input type="text" formControlName="phone" />
      <button (click)="removeContact(i)">删除联系方式</button>
    </div>
  </div>
  <button (click)="addContact()">添加联系方式</button>
  <button>提交</button>
</form>

从上面的代码不难看出来,FormArray 可能是比 FormGroup 更加高级别的元素组织结构,但它一定比 FormControl 更加高级。FormArray 对应的 html 结构的接头指令是 formArrayName

给之前的模板表单加上动态效果如下代码所示:

  bestFriends: any = new FormArray([]);
  concatForm: FormGroup = new FormGroup({
    usernameG: new FormGroup({
      username: this.usernameControl
    }),
    passwordG: new FormGroup({
      password: new FormControl('123456'),
    }),
    friends: new FormGroup({
      bestFriends: this.bestFriends,
    })
  });
  
  addBestFriends(): void {
    this.bestFriends.push(new FormGroup({
      name: new FormControl(),
      sex: new FormControl(),
      address: new FormControl(),
    }))
  }
  
  ngOnInit(): void {
    this.addBestFriends();
  }
<div formGroupName="friends">
  <div formArrayName="bestFriends">
    <div
      *ngFor="let item of bestFriends.controls; let i = index"
      [formGroupName]="i"
    >
      <input formControlName="name" type="text" />
      <input formControlName="sex" type="text" />
      <input formControlName="address" type="text" />
    </div>
  </div>
</div>
<button type="button" (click)="addBestFriends()">增加新的朋友</button>

从上面的代码中不难看出来, FormArray 中的元素由 FormGroup 包裹在外面,并且渲染的时候使用到了 *ngFor 指令,并从中拿到了序列号作为外层 FormGroup 的绑定值,这一点非常重要。

1.2.7 FormBuilder

创建表单的快捷方式。

  1. this.fb.control:表单项
  2. this.fb.group:表单组,表单至少是一个 FormGroup
  3. this.fb.array:用于复杂表单,可以动态添加表单项或表单组,在表单验证时,FormArray 中有一项没通过,整体没通过
import { FormBuilder, FormGroup, Validators } from "@angular/forms"

export class AppComponent {
  contactForm: FormGroup
  constructor(private fb: FormBuilder) {
    this.contactForm = this.fb.group({
      fullName: this.fb.group({
        firstName: ["😝", [Validators.required]],
        lastName: [""]
      }),
      phone: []
    })
  }
}

现在使用 FormBuilder 服务重写刚才的所有代码:

  // usernameControl: FormControl = new FormControl('', [
  //   Validators.required,
  //   CustomValidators.number,
  // ], CustomValidators.nameUnique);
  // bestFriends: any = new FormArray([]);
  // concatForm: FormGroup = new FormGroup({
  //   usernameG: new FormGroup({
  //     username: this.usernameControl
  //   }),
  //   passwordG: new FormGroup({
  //     password: new FormControl('123456'),
  //   }),
  //   friends: new FormGroup({
  //     bestFriends: this.bestFriends,
  //   })
  // });

  usernameControl: any;
  bestFriends = [] as unknown as any;
  concatForm = null as any;
  constructor(private fb: FormBuilder) {
    this.usernameControl = this.fb.control('', [
      Validators.required,
      CustomValidators.number,
    ], CustomValidators.nameUnique);

    this.bestFriends = this.fb.array([]);
    this.concatForm = this.fb.group({
      usernameG: this.fb.group({
        username: this.username,
      }),
      passwordG: this.fb.group({
        password: this.fb.control('123456'),
      }),
      friends: this.fb.group({
        bestFriends: this.bestFriends,
      })
    });
  }

其实就是用服务上面的方法简单的替换一下,这样更加符合依赖注入的原理。

1.2.8 练习

  1. 获取一组复选框中选中的值

    <form [formGroup]="form" (submit)="onSubmit()">
      <label *ngFor="let item of Data">
        <input type="checkbox" [value]="item.value" (change)="onChange($event)" />
        {{ item.name }}
      </label>
      <button>提交</button>
    </form>
    
    import { Component } from "@angular/core"
    import { FormArray, FormBuilder, FormGroup } from "@angular/forms"
    interface Data {
      name: string
      value: string
    }
    @Component({
      selector: "app-checkbox",
      templateUrl: "./checkbox.component.html",
      styles: []
    })
    export class CheckboxComponent {
      Data: Array<Data> = [
        { name: "Pear", value: "pear" },
        { name: "Plum", value: "plum" },
        { name: "Kiwi", value: "kiwi" },
        { name: "Apple", value: "apple" },
        { name: "Lime", value: "lime" }
      ]
      form: FormGroup
    
      constructor(private fb: FormBuilder) {
        this.form = this.fb.group({
          checkArray: this.fb.array([])
        })
      }
    
      onChange(event: Event) {
        const target = event.target as HTMLInputElement
        const checked = target.checked
        const value = target.value
        const checkArray = this.form.get("checkArray") as FormArray
    
        if (checked) {
          checkArray.push(this.fb.control(value))
        } else {
          const index = checkArray.controls.findIndex(
            control => control.value === value
          )
          checkArray.removeAt(index)
        }
      }
    
      onSubmit() {
        console.log(this.form.value)
      }
    }
    
    
  2. 获取单选框中选中的值

    export class AppComponent {
      form: FormGroup
    
      constructor(public fb: FormBuilder) {
        this.form = this.fb.group({ gender: "" })
      }
    
      onSubmit() {
        console.log(this.form.value)
      }
    }
    
    <form [formGroup]="form" (submit)="onSubmit()">
      <input type="radio" value="male" formControlName="gender" /> Male
      <input type="radio" value="female" formControlName="gender" /> Female
      <button type="submit">Submit</button>
    </form>
    

1.2.9 其他

  1. patchValue:设置表单控件的值(可以设置全部,也可以设置其中某一个,其他不受影响)
  2. setValue:设置表单控件的值 (设置全部,不能排除任何一个)
  3. valueChanges:当表单控件的值发生变化时被触发的事件
  4. reset:表单内容置空

1. 使用patchValue设置表单控件的值

patchValue方法允许你设置表单中的一个或多个控件的值,而不会影响其他控件。

// 假设你有一个FormGroup
this.form = new FormGroup({
  'firstName': new FormControl(),
  'lastName': new FormControl()
});

// 使用patchValue设置firstName控件的值
this.form.patchValue({
  firstName: 'John'
});

// 使用patchValue同时设置firstName和lastName控件的值
this.form.patchValue({
  firstName: 'John',
  lastName: 'Doe'
});

2. 使用setValue设置表单控件的值

setValue方法用于设置整个表单的值,不能单独排除任何一个控件。

// 假设form是上面定义的FormGroup
this.form.setValue({
  firstName: 'Jane',
  lastName: 'Smith'
});

3. 使用valueChanges监听表单控件值的变化

valueChanges是一个Observable,它会在表单控件的值发生变化时发出新的值。

// 监听firstName控件的值变化
this.form.get('firstName').valueChanges.subscribe(value => {
  console.log('First Name changed to:', value);
});

// 监听整个表单的值变化
this.form.valueChanges.subscribe(value => {
  console.log('Form values changed to:', value);
});

4. 使用reset方法置空表单内容

reset方法可以将表单控件的值重置为空,或者你可以提供一个值对象来重置表单到特定的值。

// 重置整个表单,清空所有控件的值
this.form.reset();

// 或者提供特定的值来重置表单
this.form.reset({
  firstName: 'Default Name',
  lastName: 'Default Surname'
});

组件模板示例

假设你有一个组件模板,其中包含两个输入控件和一个按钮来重置表单:

<form [formGroup]="form">
  <input formControlName="firstName" placeholder="First Name">
  <input formControlName="lastName" placeholder="Last Name">
  <button type="button" (click)="resetForm()">Reset Form</button>
</form>

组件类中的处理

在组件类中,你将定义FormGroup,并添加事件处理器来处理表单的值变化和重置操作。

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-form-component',
  templateUrl: './form-component.html'
})
export class FormComponent {
  form: FormGroup;

  constructor() {
    this.form = new FormGroup({
      'firstName': new FormControl(''),
      'lastName': new FormControl('')
    });

    // 监听firstName控件的值变化
    this.form.get('firstName').valueChanges.subscribe(newValue => {
      console.log('First Name value:', newValue);
    });
  }

  resetForm() {
    this.form.reset();
  }
}

在这个例子中,我们创建了一个包含firstNamelastName的表单,并展示了如何使用patchValuesetValuevalueChangesreset方法。这些方法提供了对响应式表单控件的灵活操作。

2. 实践与练习

2.1 ReactiveFormModule 的方式实现 Select

踩了很多坑之后的整理代码如下:

  <mat-select formControlName="deviceStatus">
    <mat-option
      *ngFor="let status of deviceStatusOptions$ | async as deviceStatusOptions"
      [value]="status.value"
    >
      {{status.label}}
    </mat-option>
  </mat-select>

其中,deviceStatusOptions$ 来自:

this.deviceStatusOptions$ = this.alarmDataService.statusOptions;

而服务本身为:

    getStatusOptions(): Observable<SelectOption[]> {
        return of([
            { value: '', label: 'None', },
            { value: 'Running', label: 'Running', },
            { value: 'Offline', label: 'Offline', },
        ]);
    }

    get statusOptions() {
        return this.getStatusOptions();
    }

以上代码为远程加载 select 的 option 的标准流程!

2.2 ReactiveFormModule 实践 -- 实现一个复杂的 Reactive 表单

模板文件为:

  <div class="container-fluid bg-white">
    <div class="list-filters">
      <form class="device-details-form" [formGroup]="alarmDetailsForm">
        <div class="list-title">
          <div class="list-title-content">
            {{ "Base Detail" | safeTranslate : lang }}
          </div>
        </div>
        <mat-divider class="common-divider"></mat-divider>
        <div class="base-details px-24 mt-5" formGroupName="baseDetail">
          <div class="row">
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Alarm ID" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="alarmID" />
              <button
                (click)="clearHandler('baseDetail','alarmID')"
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Alarm Status" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="alarmStatus" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','alarmStatus')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Alarm Type" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="alarmType" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','alarmType')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Alarm Priority" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="alarmPriority" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','alarmPriority')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Alarm Raise Date" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="alarmRaiseDate" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','alarmRaiseDate')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <div class="col-sm-12 row px-24">
              <mat-form-field class="col-sm-6 px-0">
                <mat-label
                  >{{ "Alarm Description" | safeTranslate : lang }}</mat-label
                >
                <textarea
                  matInput
                  formControlName="alarmDescription"
                ></textarea>
              </mat-form-field>
            </div>

            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Error Code" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="errorCode" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','errorCode')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Error Description" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="errorDescription" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','errorDescription')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Work Order Number" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="workordernum" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','workordernum')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Acknowledgement ID" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="ackID" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','ackID')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Created By" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="createdBy" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','createdBy')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Created Time" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="createdTime" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','createdTime')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Updated By" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="updatedBy" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','updatedBy')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Updated Time" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="updatedTime" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('baseDetail','updatedTime')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
          </div>
        </div>

        <div class="list-title">
          <div class="list-title-content">
            {{ "Related Device" | safeTranslate : lang }}
          </div>
        </div>
        <mat-divider class="common-divider"></mat-divider>

        <div class="base-details px-24 mt-5" formGroupName="relatedDevice">
          <div
            class="row"
            *ngFor="let device of relatedDevice.controls; let i = index"
            [formGroupName]="i"
          >
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Device Number" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="deviceNum" required />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('relatedDevice',i,'deviceNum')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Device Status" | safeTranslate : lang }}</mat-label
              >
              <mat-select formControlName="deviceStatus">
                <mat-option
                  *ngFor="let status of deviceStatusOptions$ | async as deviceStatusOptions"
                  [value]="status.value"
                >
                  {{status.label}}
                </mat-option>
              </mat-select>

              <button
                matSuffix
                disabled
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('relatedDevice',i,'deviceStatus')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label>{{ "Device EUI" | safeTranslate : lang }}</mat-label>
              <input matInput formControlName="deviceEUI" required />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('relatedDevice',i,'deviceEUI')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Short Description of Device" | safeTranslate : lang
                }}</mat-label
              >
              <input matInput formControlName="deviceShortDesp" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('relatedDevice',i,'deviceShortDesp')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "Low Temperatue(℃)" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="lowTmp" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('relatedDevice',i,'lowTmp')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
            <mat-form-field class="col-sm-6">
              <mat-label
                >{{ "High Temperatue(℃)" | safeTranslate : lang }}</mat-label
              >
              <input matInput formControlName="highTmp" />
              <button
                matSuffix
                disabled
                mat-icon-button
                aria-label="clear input"
                (click)="clearHandler('relatedDevice',i,'highTmp')"
              >
                <mat-icon>X</mat-icon>
              </button>
            </mat-form-field>
          </div>
        </div>
      </form>
    </div>
  </div>

对应的组件 class 为:

import { AfterViewInit, Component, ViewChild, OnInit, ElementRef, OnDestroy, ErrorHandler } from '@angular/core';
import { InventoryService } from '@c8y/client';
import { Router } from '@angular/router';
import { FormArray, FormControl, FormGroup, NgForm } from '@angular/forms';
import { Subject, timer, Observable, of } from 'rxjs';
import { slide, table, stay } from "../animations";
import { RouteStorageService } from '../services/route-storage.service';
import { LangStorageService } from '../services/current-language.service';
import { AlarmDataService } from '../services/alarm-data.service';
import { Title } from '@angular/platform-browser';
import { DeviceDetailsErrorHandler } from '../errorHandlers/DeviceDetailsErrorHandler';

export interface SelectOption {
  value: string;
  label: string;
}

export interface IAlarmDetails {
  baseDetail: {
    alarmID: string[];
    alarmStatus: string[];
    alarmType: string[];
    alarmPriority: string[];
    alarmRaiseDate: number[];
    alarmDescription: string[];
    errorCode: string[];
    errorDescription: string[];
    workordernum: string[];
    ackID: string[];
    createdBy: string[];
    createdTime: number[];
    updatedBy: string[];
    updatedTime: number[];
  };
  relatedDevice: {
    deviceNum: string[];
    deviceStatus: string;
    deviceEUI: string[];
    deviceShortDesp: string[];
    lowTmp: string[];
    highTmp: string[];
  }[];
}

@Component({
  selector: 'Alarm',
  styleUrls: ['./alarm-details.less'],
  templateUrl: './alarm-details.html',
  animations: [slide, table, stay],
  providers: [
    { provide: ErrorHandler, useClass: DeviceDetailsErrorHandler },
  ]
})
export class AlarmDetailsComponent implements AfterViewInit, OnInit, OnDestroy {
  cardTitle: string = "Alarm";
  disabled = false;
  show = true;
  deviceID: string;
  alarmDetailsForm: FormGroup;
  relatedDevice: FormArray;
  private isComponentDestroyed = new Subject<void>();
  deviceStatusOptions$: Observable<SelectOption[]>;

  constructor(
    public inventory: InventoryService,
    private router: Router,
    private routeStorageService: RouteStorageService,
    private langStorageService: LangStorageService,
    private alarmDataService: AlarmDataService,
    private errorHandler: ErrorHandler,
    private titleService: Title,
  ) {
    this.relatedDevice = new FormArray([
      new FormGroup({
        deviceNum: new FormControl({ value: null, disabled: true }),
        deviceStatus: new FormControl({ value: null, disabled: true }),
        deviceEUI: new FormControl({ value: null, disabled: true }),
        deviceShortDesp: new FormControl({ value: null, disabled: true }),
        lowTmp: new FormControl({ value: null, disabled: true }),
        highTmp: new FormControl({ value: null, disabled: true }),
      })
    ]);

    this.alarmDetailsForm = new FormGroup({
      baseDetail: new FormGroup({
        alarmID: new FormControl({ value: null, disabled: true }),
        alarmStatus: new FormControl({ value: null, disabled: true }),
        alarmType: new FormControl({ value: null, disabled: true }),
        alarmPriority: new FormControl({ value: null, disabled: true }),
        alarmRaiseDate: new FormControl({ value: null, disabled: true }),
        alarmDescription: new FormControl({ value: null, disabled: true }),
        errorCode: new FormControl({ value: null, disabled: true }),
        errorDescription: new FormControl({ value: null, disabled: true }),
        workordernum: new FormControl({ value: null, disabled: true }),
        ackID: new FormControl({ value: null, disabled: true }),
        createdBy: new FormControl({ value: null, disabled: true }),
        createdTime: new FormControl({ value: null, disabled: true }),
        updatedBy: new FormControl({ value: null, disabled: true }),
        updatedTime: new FormControl({ value: null, disabled: true }),
      }),
      relatedDevice: this.relatedDevice,
    })

    try {
      const currentPath = this.routeStorageService.getRouteStack().at(-1);
      this.deviceID = currentPath.split('/').at(-1);
      this.alarmDataService.getAlarmData(this.deviceID).subscribe((data) => {
        this.alarmDetailsForm.patchValue(data);
      });
    } catch (error) {
      this.errorHandler.handleError(new Error('Device id is not valid!'));
    }

    this.deviceStatusOptions$ = this.alarmDataService.statusOptions;
  }

  clearHandler(...args: any) {
    const control = this.alarmDetailsForm.get(args);
    if (control) {
      control.reset();
    }
  }

  ngOnDestroy() {
    this.isComponentDestroyed.next();
    this.isComponentDestroyed.complete();
  }

  ngAfterViewInit() {
    setTimeout(() => this.titleService.setTitle(this.cardTitle), 5000);
  }

  goBack(): void {
    this.show = false;
    const previousRoute = this.routeStorageService.popRoute();
    timer(400).subscribe(
      {
        next: () => {
          this.router.navigateByUrl(previousRoute)
        }
      }
    );
  }

  onSubmit(): void { }

  get lang() {
    return this.langStorageService.getLanguage();
  }

  switchLang() {
    const current_lang = this.lang === 'EN' ? 'ZH' : 'EN';
    return this.langStorageService.setLanguage(current_lang);
  }

  setFormValue(value: string, name: string, formRef: NgForm): void {
    formRef.form.get(name).setValue(value);
  }

  async ngOnInit() {
  }
}

而提供数据服务的服务为:

import { IAlarmDetails, SelectOption } from '../iotAlarmDetails/alarm-detail.component';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class AlarmDataService {
    private current_data = null;

    constructor() { }

    getAlarmData(id: string): Observable<IAlarmDetails> {
        if (!id) return of({} as IAlarmDetails);
        const currentValue = {
            "baseDetail": {
                "alarmID": [
                    "ALM#219481951"
                ],
                "alarmStatus": [
                    "$Active"
                ],
                "alarmType": [
                    "$Temperature"
                ],
                "alarmPriority": [
                    "Normal"
                ],
                "alarmRaiseDate": [
                    1719906188309
                ],
                "alarmDescription": [
                    "SD20240101"
                ],
                "errorCode": [
                    "400"
                ],
                "errorDescription": [
                    "unconnected"
                ],
                "workordernum": [
                    "WD#12345"
                ],
                "ackID": [
                    "XXXXXX"
                ],
                "createdBy": [
                    "TOM CARVERS"
                ],
                "createdTime": [
                    1719906188309
                ],
                "updatedBy": [
                    "TOM CARVERS"
                ],
                "updatedTime": [
                    1719906188309
                ]
            },
            "relatedDevice": [
                {
                    "deviceNum": [
                        "ENO#219481951"
                    ],
                    "deviceStatus": "Running",
                    "deviceEUI": [
                        "24E124723D495091"
                    ],
                    "deviceShortDesp": [
                        "It is a temperature sensor"
                    ],
                    "lowTmp": [
                        "-12.5"
                    ],
                    "highTmp": [
                        "35.5"
                    ]
                }
            ]
        };
        return of(currentValue);
    }

    getStatusOptions(): Observable<SelectOption[]> {
        return of([
            { value: '', label: 'None', },
            { value: 'Running', label: 'Running', },
            { value: 'Offline', label: 'Offline', },
        ]);
    }

    get statusOptions() {
        return this.getStatusOptions();
    }
}

这个例子中展示了:

  • 展示了如何在 constructor 中构建一个 ReactiveForm 模型,使用了 FormGroup FormControl FormArray, 在其中实现了动态表单元素。
  • 展示了如何通过服务获取表单的值,然后在 constructor 中将这些值通过 observable 订阅的方式赋值给表单模型。
  • 展示了如何点击 X 按钮重置对应表单元素的值的操作。
  • 展示了在 ReactiveForm 中如何将一个表单元素 disabled 的操作。
  • 展示了为什么用 patchValue 而不是 setValue.
  • 展示了 FormGroup FormGroupName FormControlName [FormGroupName]
  • 展示了 required 出现 *
  • 展示了部分 Bootstrap 的布局。