【angular开发总结】-【angular表单】- Angular响应式表单和表单控件封装

angular的表单分响应式表单和模板驱动表单。

响应式表单比模板驱动表单更有可伸缩性。它们提供对底层表单 API 的直接访问,并且在视图和数据模型之间使用同步数据流,从而可以更轻松地创建大型表单。

模板驱动表单专注于简单的场景,可复用性没那么高。在视图和数据模型之间使用异步数据流

1、理解angular响应式表单

常用表单基础类

  • FormControl 实例用于追踪单个表单控件的值和验证状态。
  • FormGroup 用于追踪一个表单控件组的值和状态。
  • FormArray 用于追踪表单控件数组的值和状态。
  • ControlValueAccessor 用于在 Angular 的 FormControl 实例和内置 DOM 元素之间创建一个桥梁。

建立响应式表单

对于响应式表单,你可以直接在组件类中定义表单模型。[formControl] 指令会通过内部值访问器ControlValueAccessor来把显式创建的 FormControl 实例与视图中的特定表单元素联系起来。

在下面例子中,表单模型是 FormControl 实例。

  1. import { Component } from @angular/core;
  2. import { FormControl } from @angular/forms;
  3. @Component({
  4. selector: app-reactive-favorite-color,
  5. template: `
  6. Favorite Color: <input type="text" [formControl]="favoriteColorControl">
  7. `
  8. })
  9. export class FavoriteColorComponent {
  10. favoriteColorControl = new FormControl();
  11. }

图 1.在响应式表单中直接访问表单模型

响应式表单中的数据流

在响应式表单中,视图中的每个表单元素都直接链接到一个表单模型(FormControl 实例)。 从视图到模型的修改以及从模型到视图的修改都是同步的,而且不依赖于 UI 的渲染方式。

视图=>模型 的数据流步骤:

  1. 最终用户在输入框元素中键入了一个值,这里是 “Blue”。
  2. 这个输入框元素会发出一个带有最新值的 “input” 事件。
  3. 这个控件值访问器 ControlValueAccessor 会监听表单输入框元素上的事件,并立即把新值传给 FormControl 实例。
  4. FormControl 实例会通过 valueChanges 这个可观察对象发出这个新值。
  5. valueChanges 的任何一个订阅者都会收到这个新值。

模型=>视图 的数据流步骤:

  1. favoriteColorControl.setValue() 方法被调用,它会更新这个 FormControl 的值。
  2. FormControl 实例会通过 valueChanges 这个可观察对象发出新值。
  3. valueChanges 的任何订阅者都会收到这个新值。
  4. 该表单输入框元素上的控件值访问器ControlValueAccessor会把控件更新为这个新值。

响应式表单实现原理

响应式表单将formControl实例挂载到formControl指令或者formControlName指令上,两种指令再通过内部的值访问器ControlValueAccessorFormControl 实例与视图中的特定表单元素联系起来。

Angular 为所有原生 DOM 表单元素创建了 Angular 表单控件

Accessor Form Element
DefaultValueAccessor input,textarea
CheckboxControlValueAccessor input[type=checkbox]
NumberValueAccessor input[type=number]
RadioControlValueAccessor input[type=radio]
RangeValueAccessor input[type=range]
SelectControlValueAccessor select
SelectMultipleControlValueAccessor select[multiple]

从上表中可看到,当 Angular 在组件模板中中遇到 inputtextarea DOM 原生控件时,会使用DefaultValueAccessor 指令。

1 源码分析
  • formControl指令

    实例化时,初始化ControlValueAccessor,调用 setUpControl() 函数

    1. // form_control_directive.ts
    2. export class FormControlDirective extends NgControl implements OnChanges {
    3. ...
    4. constructor(
    5. ...
    6. @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
    7. ) {
    8. ...
    9. this.valueAccessor = selectValueAccessor(this, valueAccessors);
    10. }
    11. /** @nodoc */
    12. ngOnChanges(changes: SimpleChanges): void {
    13. if (this._isControlChanged(changes)) {
    14. setUpControl(this.form, this);
    15. ....
    16. }
    17. }
    18. }
  • formControlName指令

    实例化时,初始化ControlValueAccessor,调用formGroup指令的addControl()addControl方法中再调用setUpControl() 函数。

    1. // form_control_name.ts
    2. export class FormControlName extends NgControl implements OnChanges, OnDestroy {
    3. ...
    4. constructor(
    5. ...
    6. @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],) {
    7. this.valueAccessor = selectValueAccessor(this, valueAccessors);
    8. }
    9. /** @nodoc */
    10. ngOnChanges(changes: SimpleChanges) {
    11. if (!this._added) this._setUpControl();
    12. ...
    13. }
    14. private _setUpControl() {
    15. ...
    16. // 调用formGroup指令里的addControl()
    17. (this as {control: FormControl}).control = this.formDirective.addControl(this);
    18. ...
    19. this._added = true;
    20. }
    21. }
  • formGroup指令

    1. // form_group_directive.ts
    2. export class FormGroupDirective ... {
    3. ...
    4. /**
    5. * @description
    6. * Method that sets up the control directive in this group, re-calculates its value
    7. * and validity, and adds the instance to the internal list of directives.
    8. *
    9. * @param dir The `FormControlName` directive instance.
    10. */
    11. addControl(dir: FormControlName): FormControl {
    12. ...
    13. setUpControl(ctrl, dir);
    14. ...
    15. return ctrl;
    16. }
    17. }
  • setUpControl()

    为formControl实例注册事件监听,实现原生表单控件和 Angular 表单控件的数据同步。

    1. //shared.ts
    2. // 为formControl实例注册事件监听
    3. export function setUpControl(control: FormControl, dir: NgControl): void {
    4. ...
    5. // 调用 writeValue() 初始化视图表单控件值
    6. dir.valueAccessor!.writeValue(control.value);
    7. // 注册视图改变的监听事件
    8. setUpViewChangePipeline(control, dir);
    9. // 注册表单控件值更新监听事件
    10. setUpModelChangePipeline(control, dir);
    11. // 注册视图失焦事件
    12. setUpBlurPipeline(control, dir);
    13. }
    14. // 原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新 视图 => 模型
    15. function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
    16. dir.valueAccessor!.registerOnChange((newValue: any) => {
    17. ...
    18. if (control.updateOn === change) updateControl(control, dir);
    19. });
    20. }
    21. // 原生控件失焦,Angular 表单控件值也更新 视图 => 模型
    22. function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
    23. dir.valueAccessor!.registerOnTouched(() => {
    24. ...
    25. if (control.updateOn === blur && control._pendingChange) updateControl(control, dir);
    26. });
    27. }
    28. // 更新formcontrol实例值
    29. function updateControl(control: FormControl, dir: NgControl): void {
    30. ...
    31. control.setValue(control._pendingValue, {emitModelToViewChange: false});
    32. }
    33. // 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新 模型 => 视图
    34. function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
    35. control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
    36. // control -> view
    37. dir.valueAccessor!.writeValue(newValue);
    38. // control -> ngModel
    39. if (emitModelEvent) dir.viewToModelUpdate(newValue);
    40. });
    41. }
  • FormControl实例

    1. export class FormControl extends AbstractControl {
    2. // 控件值改变事件
    3. _onChange: Function[] = [];
    4. // 更新控件值
    5. setValue(value: any, options: {
    6. onlySelf?: boolean,
    7. emitEvent?: boolean,
    8. emitModelToViewChange?: boolean,
    9. emitViewToModelChange?: boolean
    10. } = {}): void {
    11. (this as {value: any}).value = this._pendingValue = value;
    12. if (this._onChange.length && options.emitModelToViewChange !== false) {
    13. this._onChange.forEach(
    14. (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
    15. }
    16. // 更新值和校验
    17. this.updateValueAndValidity(options);
    18. }
    19. /**
    20. * Register a listener for change events.
    21. *
    22. * @param fn The method that is called when the value changes
    23. */
    24. registerOnChange(fn: Function): void {
    25. this._onChange.push(fn);
    26. }
    27. }
2 响应式表单原理图

响应式表单.png

视图 => 模型:

input输入改变,触发ControlValueAccessor值访问器onChange()方法,在钩子函数registerOnChange()中,onChange()与回调函数fn()绑定,fn()是指令实例化的时候调用setUpControl()函数注册事件时候的回调。fn()调用updateControl()updateControl()中会执行control.setValue()从而更新FormControl 实例的值。

模型 => 视图:

control.setValue()更新表单控件值,然后遍历control.registerOnChange()注册的事件列表_onChange,该事件列表中注册了值访问器的writeValue()钩子,执行writeValue()就会更新DOM控件的值。

2、 如何新建一个表单(FormGroup、FormArray、FormBuilder)

  • FormGroup

    1. import { Component } from @angular/core;
    2. import { FormGroup, FormControl } from @angular/forms;
    3. @Component({
    4. selector: app-profile-editor,
    5. templateUrl: ./profile-editor.component.html,
    6. styleUrls: [./profile-editor.component.css]
    7. })
    8. export class ProfileEditorComponent {
    9. profileForm = new FormGroup({
    10. firstName: new FormControl(),
    11. lastName: new FormControl(),
    12. address: new FormGroup({
    13. street: new FormControl(),
    14. city: new FormControl(),
    15. state: new FormControl(),
    16. zip: new FormControl()
    17. })
    18. });
    19. }
  • FormArray

    适用于创建动态表单,管理任意数量的匿名控件。不需要为每个控件定义一个名字作为 key,因此,如果事先不知道子控件的数量,可选择FormArray创建表单。

    定义 FormArray 控件

    你可以通过把一组(从零项到多项)控件定义在一个数组中来初始化一个 FormArray

    1. profileForm = this.fb.group({
    2. firstName: [, Validators.required],
    3. lastName: [],
    4. address: this.fb.group({
    5. street: [],
    6. city: [],
    7. state: [],
    8. zip: []
    9. }),
    10. aliases: this.fb.array([
    11. this.fb.control()
    12. ])
    13. });

    FormGroup 中的这个 aliases 控件现在管理着一个控件,将来还可以动态添加多个。

    访问 FormArray 控件

    通过 getter 来访问控件很方便,这种方法还能很容易地重复处理更多控件。

    1. get aliases() {
    2. return this.profileForm.get(aliases) as FormArray;
    3. }

    动态添加控件

    1. addAlias() {
    2. this.aliases.push(this.fb.control());
    3. }
  • FormBuilder

    FormBuilder 服务有三个方法:control()group()array()。这些方法都是工厂方法,用于在组件类中分别生成 FormControlFormGroupFormArray

    1. import { Component } from @angular/core;
    2. import { FormBuilder } from @angular/forms;
    3. @Component({
    4. selector: app-profile-editor,
    5. templateUrl: ./profile-editor.component.html,
    6. styleUrls: [./profile-editor.component.css]
    7. })
    8. export class ProfileEditorComponent {
    9. profileForm = this.fb.group({
    10. firstName: [],
    11. lastName: [],
    12. address: this.fb.group({
    13. street: [],
    14. city: [],
    15. state: [],
    16. zip: []
    17. }),
    18. });
    19. constructor(private fb: FormBuilder) { }
    20. }

3、自定义表单验证器

  1. ngOnInit(): void {
  2. this.heroForm = new FormGroup({
  3. name: new FormControl(this.hero.name, [
  4. forbiddenNameValidator(/bob/i) // <-- Heres how you pass in the custom validator.
  5. ]),
  6. });
  7. }
  8. get name() { return this.heroForm.get(name); }
  9. export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  10. return (control: AbstractControl): ValidationErrors | null => {
  11. const forbidden = nameRe.test(control.value);
  12. return forbidden ? {forbiddenName: {value: control.value}} : null;
  13. };
  14. }

4、交叉验证

创建表单模型时,把一个新的验证器传给FormGroup的第二个参数。

  1. const heroForm = new FormGroup({
  2. name: new FormControl(),
  3. alterEgo: new FormControl(),
  4. power: new FormControl()
  5. }, { validators: identityRevealedValidator });
  6. export const identityRevealedValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
  7. const name = control.get(name);
  8. const alterEgo = control.get(alterEgo);
  9. return name && alterEgo && name.value === alterEgo.value ? { identityRevealed: true } : null;
  10. };

5、如何封装表单控件

  • 封装表单控件的有什么好处?

    1、表单是由各种控件组合在一起的,封装表单控件,可以将复杂的表单拆解为不同的控件,表单需要什么控件就引入相应的控件,这样表单功能容易扩充,在业务多变性的情况下,表单控件可以让表单更灵活。

    2、表单控件可复用,将复杂的表单拆解为控件,有利于开发和维护。

  • 封装表单控件注意事项

    1、必须为表单控件提供值访问器ControlValueAccessor。必须将表单控件加入到验证器集合NG_VALIDATORS,这样控件的校验才会绑定到表单校验。

    2、必须实现ControlValueAccessor类和Validator接口。

例:

  1. import { Component, forwardRef, Input, OnInit } from @angular/core;
  2. import {
  3. AbstractControl,
  4. ControlValueAccessor,
  5. FormBuilder,
  6. FormControl,
  7. FormGroup,
  8. NG_VALIDATORS,
  9. NG_VALUE_ACCESSOR,
  10. ValidationErrors,
  11. Validator,
  12. } from @angular/forms;
  13. @Component({
  14. selector: app-mpi-mode-control,
  15. templateUrl: ./mpi-mode-control.component.html,
  16. styleUrls: [./mpi-mode-control.component.scss],
  17. providers: [
  18. {
  19. provide: NG_VALUE_ACCESSOR,
  20. useExisting: forwardRef(() => MpiModeControlComponent),
  21. multi: true,
  22. },
  23. {
  24. provide: NG_VALIDATORS,
  25. useExisting: forwardRef(() => MpiModeControlComponent),
  26. multi: true,
  27. },
  28. ],
  29. })
  30. // 需要实现ControlValueAccessor, Validator
  31. export class MpiModeControlComponent implements OnInit,
  32. ControlValueAccessor, Validator {
  33. formGroup: FormGroup;
  34. private propagateChange = (_: any) => {};
  35. private propagateTunched = (_: any) => {};
  36. constructor(
  37. private fb: FormBuilder,
  38. private customValidatorsService: CustomValidatorsService
  39. ) {
  40. this.formGroupConfig();
  41. this.getFormGroupState();
  42. }
  43. // 更新视图
  44. writeValue(mpiRunFormData: TMpiRunFormInfo) {
  45. ...
  46. this.formGroup.patchValue(mpiRunFormData);
  47. }
  48. // 视图控件change事件,更新表单控件值
  49. registerOnChange(fn: any): void {
  50. this.propagateChange = fn;
  51. }
  52. // 视图控件blue事件,更新表单控件值
  53. registerOnTouched(fn: any): void {
  54. this.propagateTunched = fn;
  55. }
  56. // 将控件校验添加到表单校验
  57. validate(control: AbstractControl): ValidationErrors {
  58. return this.formGroup?.valid
  59. ? null
  60. : { missionHpcCreateControl: { valid: false } }; // 可以为任意对象,比如{ valid: false },返回值为control.errors,详见源码updateValueAndValidity()方法
  61. }
  62. /**
  63. * 设置响应式表单
  64. */
  65. private formGroupConfig() {
  66. this.formGroup = this.fb.group(
  67. {
  68. mpiOnly: [false],
  69. shareDirectory: [
  70. ,
  71. [
  72. this.customValidatorsService.checkEmpty(),
  73. this.customValidatorsService.pathValidator()
  74. ],
  75. ],
  76. systemPerformance: this.fb.group({
  77. system: [false],
  78. }),
  79. },
  80. { validators: this.textValidator() }
  81. );
  82. }
  83. // 获取表单状态
  84. private getFormGroupState() {
  85. this.formGroup.valueChanges.subscribe((valuesAndVaild) => {
  86. ...
  87. this.propagateChange(valuesAndVaild);
  88. });
  89. }
  90. }
  1. <div [formGroup]="formGroup">
  2. <app-mpi-mode-control formControlName="missionControl">
  3. </app-mpi-mode-control>
  4. </div>

参考blog


文章标签:

原文连接:https://juejin.cn/post/7114584669294690340

相关推荐