Angular 18+ 高级教程 – Reactive Forms

前言

上一篇的 Ajax 和这一篇的表单 (Form) 都是前端最最最常见的需求。

为此,Angular 分别提供了两个小型库来帮助开发者实现这些需求:

  1. Ajax – HttpClient

  2. Form – Reactive Forms (注:还有一个库叫 Template-driven Forms,它能做的 Reactive Forms 都能做到,所以我只教 Reactive Forms 而已)

虽然都是针对特定需求的库,但是 HttpClient 和 Reactive Forms 的难度不可相提并论。

HttpClient 只是基于 BOM XMLHttpRequest 和 Fetch 的上层封装,它属于 1 - n 的实现。

Reactive Forms 则完全没有利用游览器 Native Form 的功能 (因为体验太烂了),它属于从 0 - 1 的实现。

虽然 Reactive Forms 没有使用 Native Form 的功能,但 Angular Team 依然参考了 Native Form 的设计 (接口调用,管理等等)。

所以,我们要想掌握好 Reactive Forms,最好先掌握游览器 Native Form,对 Native Form 不熟悉的朋友,请看这篇 HTML – Native Form 原生表单功能集

 

Native Form 的强大于弱小

让我们通过一个例子来感受一下表单需求,以及使用 Native Form 与不使用 Native Form 去实现相同需求的难度。

下图是一个简单到爆的表单

Get formValue

第一个需求是:当用户 submit 的时候,获取表单里 3 个 input 的 value,然后放入一个 formValue 对象中。

效果

without Native Form

首先给 input 添加一个 name 属性,它用作于 input 的 unique 识别。

<form autocomplete="off">
  <input name="name" placeholder="Name">
  <input name="email" placeholder="Email">
  <input name="phone" placeholder="Phone">
  <button>submit</button>
</form>

然后是 JavaScript

// 1. select all related elements
const form = document.querySelector('form')!;
const nameInput = form.querySelector<HTMLInputElement>('[name="name"]')!; // 2. select by attribute name
const emailInput = form.querySelector<HTMLInputElement>('[name="email"]')!;
const phoneInput = form.querySelector<HTMLInputElement>('[name="phone"]')!;

// 3. listen to form submit
form.addEventListener('submit', e => {
  e.preventDefault(); // 4. 阻止游览器发请求到服务端

  // 5. get each input value put into formValue object
  console.log('formValue', {
    name: nameInput.value,
    email: emailInput.value,
    phone: phoneInput.value,
  });
});

with Native Form

如果我们利用 Native Form 的功能,代码可以优化成这样

// 1. select form element
const form = document.querySelector('form')!;

// 2. listen to form submit
form.addEventListener('submit', e => {
  e.preventDefault(); // 3. 阻止游览器发请求到服务端

  // 4. use FormData to collect all input value
  const formData = new FormData(form);

  const formValue: Record<string, unknown> = {};

  // 5. for loop FormData key and put into formValue object
  for (const [name, value] of formData.entries()) {
    formValue[name] = value;
  }

  console.log('formValue', formValue);
});

FormData 可以直接从 form 收集带有 name attribute 的 accessor (比如 input, textarea, select 等等),然后把 name 和 value 收集起来,省去了我们自己 query。

这样就搞定了!好,下一个需求。

Required & Email Format Validation

第二需求是:所有 input 必须要填,email 格式也要正确,不然就 submit 不到,报错提醒用户。

效果

without Native Form

在用户 submit 后,我们需要验证 input value,如果发现 value 不合格,那就要中断 submit,然后显示 error message (这个实现方式有很多,比如 add class, append element 都可以)

另外,这个 error message 什么时候消失也需要考虑到,比如当用户修改 input 时就 clear message,或者 3 秒后 clear message。这些都取决于用户体验要做到什么层度。

with Native Form

好,我们再来看看使用 Native Form 如何实现这个需求。

在 input 添加 required attribute,在 email input 添加 type="email" attribute。

搞定!JavaScript 不需要增加任何相关代码。这就是 Native Form 的强大之处!好,下一个需求。

Input Disabled

第三个需要是:如果 input 被 disable 了,最终的 formValue 不要包含这个 key。

without Native Form

首先在特定的 input 添加 disabled attribute 作为识别

<input name="phone" placeholder="Phone" required disabled>

接着在 submit 后,跳过 disabled input 的 validation (不需要检查它的 value,因为它最终不需要出现在 formValue),

在 collect formValue 时跳过 disabled input。JS 代码我就不写了,大家自己脑补。

with Native Form

我们只要在 input 添加 disabled attribute 就可以了,FormData 和 Validation 会自动 skip 掉 disabled 的 input。

这就是 Native Form 的强大之处!好,下一个需求。

Custom Validation

第四个需要是:value 不可以只是空格,required 虽然可以验证 empty string,但是纯空格也不是 empty string,这就导致纯可控可以 by pass required validation,这不是我们要的。

without Native Form

实现手法和第二个需求一样。

with Native Form

当 Native Form 无法完全满足需求的时候就很纠结了。因为我们不愿意完全放弃 Native Form 改为使用 without Native Form 方案 (因为我们将失去很多 Native Form 强大之处),

但是 Native Form 是否有提供扩展接口,扩展的维护成本高不高,这些都是不确定的。

我们每次遇到新需求就只能碰碰运气,比如

通过 setCustomValidity 和 reportValidity 底层接口,我们可以自定义 input 的 validation。

效果

虽然这个需求是过关了,但下一个呢?谁能保证下一次扩展会顺顺利利呢?这就是 Native Form 弱小之处。

总结

Native Form 的目标是让使用者在几乎不需要编写 CSS 和 JS 代码的情况下,实现出还不错的表单体验。

这个目标显然不是 Angular 期望达到的,所有 Angular 没有选择基于 Native Form,而是从起炉灶做出了 Reative Forms。

尽管如此,由于表单需求千差万别,Reactive Forms 有时也会遇到无法满足和难以扩展的情况。

 

Reactive Forms Quick View

我们先来感受一下 Reactive Forms 大体长什么样,大家不需要太在意代码,感受一下它的管理方式就好,下一 part 我会再一个一个细节讲解的。

同样是上一个例子,一样的 4 个需求,我们用 Reactive Forms 实现一遍。

create Angular project

ng new reactive-forms --ssr=false --routing=false --skip-tests --style=scss

Get formValue

第一个需求是:当用户 submit 的时候,获取表单里 3 个 input 的 value,然后放入一个 formValue 对象中。

App 组件

@Component({
  selector: 'app-root',
  standalone: true,
  // 1. import Reactive Forms Module,我们需要用到一些指令
  imports: [ReactiveFormsModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  // 2. 创建一个 FormGroup,它有点像是 Native Form 的 FormData
  form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
    phone: new FormControl(''),
  });

  submit() {
    // 3. submit 的时候 console formValue
    console.log('formValue', this.form.value);
  }
}

App Template

<form [formGroup]="form" (ngSubmit)="submit()" autocomplete="off" >
  <input formControlName="name" placeholder="Name">
  <input formControlName="email" placeholder="Email" type="email">
  <input formControlName="phone" placeholder="Phone">
  <button>submit</button>
</form>

[formGroup], [formControlName] 是指令,(ngSubmit) 是 [formGroup] 指令的 @Output。

几个知识点:

  1. Reactive Forms 的指令都是用 NgModule 管理的。它们都不是 Standalone Component。

    当我们 import ReactiveFormsModule 后,有一些看不见的指令其实偷偷在工作。

    除非有在 <form> element 上添加 ngNoForm 或者 ngNativeValidate attribute,否则 <form> 会自动加上 novalidate attribute,

    意思是关闭 Native Form Validation。

  2. FormControl 是一个综合管理器,下面我们会看到它可以操控非常多的东西,这里我们先把它当成一个简单的 value 管理器就好。

    name, email, phone 有 3 个 value,那就有 3 个 FormControl 咯。

    这 3 个 FormControl 被一个 FormGroup 包裹着,每一个 FormControl 都有专属名字。

  3. [formGroup] 指令把 <form> element 和 FormGroup 对象关联起来。

    [formControlName] 指令把 accessor (例子中是 <input> element) 和 FormControl 对象关联起来。(注:accessor 是读写器,意指所有可以读写 value 的 element,比如:input, textarea, select 等等)

  4. (ngSubmit) 是 [formGroup] 指令的 @Output,它会在 <form> element submit 时发布,此时我们通过 FormGroup.value 就可以拿到整个 form 的 value 了。

    FormGroup 实现了 Native FormData 获取 formValue 的功能。

这样就搞定了!好,下一个需求。

Required & Email Format Validation

第二需求是:所有 input 必须要填,email 格式也要正确,不然就 submit 不到,报错提醒用户。

预期效果

App 组件

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  form = new FormGroup({
    name: new FormControl('', {
      // 1. 在 FormControl 添加 Validation 逻辑
      validators: [Validators.required],
    }),
    email: new FormControl('', {
      // 2. required and email format
      validators: [Validators.required, Validators.email],
    }),
    phone: new FormControl('', {
      validators: [Validators.required],
    }),
  });

  submit() {
    // 3. skip when form invalid
    if (this.form.invalid) return;
    console.log('formValue', this.form.value);
  }
}

几个知识点:

  1. Reactive Forms 的 Validation 是通过 FormControl 来管理的 (上一 part 我说了,FormControl 是综合管理器,它负责很多事情的)。

  2. Validators.required, Validators.email 是 Angular built-in 的 Validation。Angular 有很多 built-in 的 Validation 基本上概括了所有 Native Form 的 Validation。

  3. 和 Native Form 不同,不管 Form 是否 valid (form 内所有 accessor value 都 pass validation),submit 是一定会触发的。

    Native Form 如果 form invalid 它是不会 submit 的。

    Reactive Forms 则一定会 submit,所以我们需要在 handle submit 时做一个 FormGroup.invalid/valid 检查。

App Template

<form [formGroup]="form" #ngForm="ngForm" (ngSubmit)="submit()" autocomplete="off" >
  <input formControlName="name" placeholder="Name">
  @if (form.get('name')!.hasError('required') && ngForm.submitted) {
    <p>Name is Required</p>
  }
  <input formControlName="email" placeholder="Email">
  @if (form.get('email')!.hasError('required') && ngForm.submitted) {
    <p>Email is Required</p>
  }
  @if (form.get('email')!.hasError('email') && ngForm.submitted) {
    <p>Incorrect Email Format</p>
  }
  <input formControlName="phone" placeholder="Phone">
  @if (form.get('phone')!.hasError('required') && ngForm.submitted) {
    <p>Phone is Required</p>
  }
  <button>submit</button>
</form>

多了很多 @if,这是因为要显示 error message。

Reactive Forms 不像 Native Form 有自带的 popup error message UI 体验,Reactive Forms 完全没有任何 UI 体验,这是 Angular 刻意画的线 -- 不碰 UI。

虽然 Angular 没有 built-in UI 但是我们可以搭配 Angular Material UI 组件库 来实现 UI 体验,所以也不是什么大问题。

本篇不可能为了 UI 引入 Angular Material,所以我们就随便写点简单的意思意思就好。

@if (form.get('name')!.hasError('required') && ngForm.submitted) {
  <p>Name is Required</p>
}

form 是 FromGroup 对象,.get('name') 方法会返回 FormControl。

FormControl 负责管理 Validation,所以通过它可以知道当前是否有 required error,如果 value 是 empty string 那 hasError('required') 就会返回 true。

required error message 只会在 2 种情况下显示

  1. 有 required error

  2. form 已经 submit 过了 (这是一种 UI 体验,我只是做个例子)

ngForm.submitted 可以判断当前 form 是否 submit 过了,

这个 ngForm 是一个 template variables,它来自

<form [formGroup]="form" #ngForm="ngForm" (ngSubmit)="submit()" autocomplete="off" >

FromGroupDirective exportAs 'ngForm',简单说 ngForm 就是 FormGroup 指令啦。

最终效果

可以看到 Validation 的检测是发生在 input 事件,而不是 submit 事件。所以 clear error message 会立刻发生。

这样就搞定了!好,下一个需求。

Input Disabled

第三个需要是:如果 input 被 disable 了,最终的 formValue 不要包含这个 key。

disabled 也是由 FormControl 管理的。

在创建好 FormGroup 对象后,找出指定的 FormControl 执行 disable 方法就可以了。

或者在指定 accessor 加上 [disabled]="true" attrbute

<input formControlName="phone" placeholder="Phone" [disabled]="true">

这个 [disabled] 是 [formControlName] 指令的 @Input,它内部其实也是调用 FormControl.disable 方法。

FormGroup.value 会 skip 掉 disabled 的 FormControl。

小心坑:当 FormGroup 里面所有的 FormControl 都是 disabled 的时候,FormGroup.value 将会返回所有的 value,就如同全部 FormControl 不是 disabled 一样。

这样就搞定了!好,下一个需求。

Custom Validation

第四个需要是:value 不可以只是空格,required 虽然可以验证 empty string,但是纯空格也不是 empty string,这就导致纯可控可以 by pass required validation,这不是我们要的。

App 组件,创建一个 Custom Validation

const mustContainNonSpace: ValidatorFn = control => {
  // 1. 如果 value 是 empty string 就 skip validation
  //    因为 Reactive Form Validation 的机制是所有 Validation 都会检查
  //    都会 error,而 required 和 mustContainNonSpace 在 empty string 的情况下都会 failed
  //    但 UI 体验上出一个 error message 会比较合理,所以当 empty string 的时候,
  //    只交给 required 负责就好,mustContainNonSpace 就 skip。
  //    return null 表示 validation passed 也可用于表达 skip validation
  if (control.value === '') return null;

  // 2. 如果 value trim 了是 empty string 那就返回 error code 和 info
  if (control.value.trim() === '') {
    return {
      // 3. key mustContainNonSpace 是 error code
      //    FormControl.hasError('mustContainNonSpace') 就是看这个
      mustContainNonSpace: true,
    };
  }
  // 3. 没有 error 就 return null 表示 validation passed.
  return null;
};

它是一个函数,类型是 ValidatorFn

参数是 AbstractControl (FormControl 的抽象类),每当 value changes ValidatorFn 就会执行,返回 null 表示 Validation Passed,返回 ValidationErrors 

表示 Validation Failed。key 是 Error Code,用于 FormControl.hasError('Error Code'),value 可以是任何值,可以用它提供更详细的错误资讯。

App 组件

export class AppComponent {
  form = new FormGroup({
    name: new FormControl('', {
      // 1. 把 mustContainNonSpace ValidatorFn 添加进 validators
      validators: [Validators.required, mustContainNonSpace],
    }),
    email: new FormControl(''),
    phone: new FormControl(''),
  });

  submit() {
    if (this.form.valid) {
      console.log('formValue', this.form.value);
    }
  }
} 

App Template

<input formControlName="name" placeholder="Name">
@if (form.get('name')!.hasError('required') && ngForm.submitted) {
  <p>Name is required</p>
}
@if (form.get('name')!.hasError('mustContainNonSpace') && ngForm.submitted) {
  <p>Input must contain non-space characters</p>
}

效果

总结

这一 part 我们很粗略的用 Reative Forms 重新实现了上一 part 表单的 4 个需求。

显然 Reactive Forms 在设计上 (接口,扩展等等) 是比 Native Form 要好很多的。

唯一比较烦人的地方是 Reactive Forms 对待 UI 体验的那条界限,它总是尽可能的不碰 UI。然而在现实场景中,UI 体验与表单紧密相连,正所谓常在河边走,哪有不湿鞋。

因此,我们常常会看见一些 Reactive Forms 专门为了提升 UI 体验而设计的接口,但它又仅仅只是辅助。这总给人一种感觉 -- "你明明就知道 UI 体验,不然你怎么会提供这些接口,但你又强调不碰 UI"。

举个例子:

FormControl 只负责 Validation 不负责 error message,但是 error message 和 Validation 是紧密相关的,于是 FormControl 的 MaxLength ValidatorFn 不得不返回一些有用的错误信息,比如 requiredLength 和 actualLength。

 

半场休息

本篇上半部分主要是带大家看一个整体性,Native Form 和 Reactive Forms 面对表单需求的解决思路。

当我们清楚了 Reactive Forms 的解决思路,再去看它具体的解决方案时就会比较容易理解,也容易举一反三。

这是一种比较好的学习 Angular 方式。

好,本篇下半部分,会逐个详细讲解 Reactive Forms 的各个功能。走起🚀

 

FormControl

FormControl 是 Reactive Forms 的核心,掌握了它就等于掌握了 Reactive Forms 超过 50% 的知识,我们就从它开始吧。

FormControl 虽然带有个 Form 字,又 under Reactive Forms,但其实它并不一定要用在表单,它是可以脱离表单使用的。

Reactive Forms 整体是针对表单没错,但是它内部是由好几个特性组成的,这几个特性是可以用于非表单场景的。

上一 part 我们一直提到说 FormControl 是一个综合管理器,它的职责非常多,功能非常多,它总共有 50 个属性和方法😱。

为了循序渐进的掌握它,我们可以把它拆分成 3 个职责 + 1 个综合知识来学习。

FormControl as Value Controller

第一个职责是 Value Controller,顾名思义 FormControl 是 Value 管理器,可用于管理 value。

创建 Value Control

const valueCtrl = new FormControl('Hello World');

get value from Value Control

console.log(valueCtrl.value); // 'Hello World'

set value to Value Control

valueCtrl.setValue('New Value'); // set value to 'New Value'
console.log(valueCtrl.value);    // 'New Value'

listen to value changes

valueCtrl.valueChanges.subscribe(() => {
  console.log('value changes'); // will fire after 1 second
});

setTimeout(() => {
  valueCtrl.setValue('New Value');
}, 1000);

get, set, listening 有没有让你想起 Signals 或者 RxJS BehaviorSubject

没错 FormControl 作为 Value Controller,它拥有像 Signals 或 BehaviorSubject 一样监听 value 变更的能力。

涉及到的 FormControl 属性和方法:

value, setValue, valueChanges

FormControl as Value Validator

FormControl 除了是 Value 管理器,它还是 Value 验证器 (Validator)。

how it work?

FormControl 是个大验证器,它里面包含许多小验证器。

这些小验证器便是我们设定给它的。

比如说,我想确保一个 value 不可以是 empty string,那我就添加一个 required 验证器到 FormControl。

这个 required 验证器负责检查 FormControl.value 是否等于 empty string。

当 FormControl initialize 或 set value 时,它就会使用 required 验证器进行验证,如果验证成功那就没事,如果验证失败,那就需要记入到 Error Collection。

FormControl 可以有无数个小验证器,每一个小验证器负责一种验证条件,比如 required 验证 empty string,email 验证 email 格式等等。

ValidatorFn

验证器是一个函数,它的类型是

当 FormControl 执行验证时,它会把自己传入 ValidatorFn 函数,函数内会对 FormControl 对象进行验证 (最主要是验证 FormControl.value)。

如果验证成功,那就返回 null,如果验证失败,那就返回 ValidationErrors 对象,它的类型是

ValidationErrors 的 key 表示这个 Validation 的 Error Code,比如 required 验证器的 Error Code 是 'required'。

ValidationErrors 的 value 用于提供额外的 Error Information,比如当 maxlength 验证器验证失败时,它的 ValidationErrors 对象是

Error Code 是 'maxlength'。

Error Information 包含要求的 max length 和验证时的 value.length。

如果没有额外的 Error Information,ValidationErrors value 通常放 true,比如 required 验证器的 ValidationErrors 

built-in ValidatorFn

Reactive Forms 提供了许多 built-in 的 ValidatorFn,它们都包含在 class Validators 静态属性或方法中。

  1. Validators.min(5)

    min ValidatorFn 用于验证 number 不可 < minNumber,>= minNumber 就 pass。

    它的 ValidationErrors

  2. Validators.max(5)

    max ValidatorFn 用于验证 number 不可 > maxNumber,<= maxNumber 就 pass。

    它的 ValidationErrors

  3. Validators.required

    required ValidatorFn 用于验证 value 不可以是 empty string,它的验证手法是 value.length === 0。

    所以 space 是可以 pass 的哦,没有 trim 的概念。

    它的 ValidationErrors

  4. Validators.requiredTrue

    requiredTrue ValidatorFn 用于验证 boolean value 必须 === true。

    它的 ValidationErrors

    注意:它的 Error Code 是 'required' 而不是 ‘requiredtrue’ 哦。
  5. Validators.email

    email ValidatorFn 用于验证 value 必须是 email format。

    这个是它的正则表达式

    const EMAIL_REGEXP =
        /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

    它的 ValidationErrors

  6. Validators.minLength(5)

    minLength ValidatorFn 用于验证 value.length 不可 < minLength,>= minNumber 就 pass。

    它的 ValidationErrors

  7. Validators.maxLength(5)

    maxLength ValidatorFn 用于验证 value.length 不可 > maxLength,<= maxNumber 就 pass。

    它的 ValidationErrors

  8. Validators.pattern(/^\d+$/)

    pattern ValidatorFn 用于验证 value 是否满足政治表达式,regex.test(value) 返回 true 就 pass。

    它的 ValidationErrors

set / add / remove ValidatorFn to FormControl

const valueCtrl = new FormControl('Hello World', {
  validators: [Validators.maxLength(10), Validators.pattern(/^\d+$/),
});

在初始化 FormControl 时,set 一个 maxLength 和 pattern ValidatorFn。

除了在初始化时可以 set ValidatorFn,我们也可以通过 FormControl 的其它方法添加或移除 ValidatorFn。

valueCtrl.addValidators([Validators.email]);    // 添加 ValidatorFn
valueCtrl.removeValidators([Validators.email]); // 移除 ValidatorFn
valueCtrl.clearValidators();                    // 清空 ValidatorFn
valueCtrl.setValidators([Validators.required]); // 清空后添加 ValidatorFn

function composition ValidatorFn

上面例子中,FormControl 的 multiple ValidatorFn 是通过 Array 来做管理的。

它运作起来类似这样

const valueCtrl = new FormControl('a');
const validatorFns = [Validators.minLength(3), Validators.email];
for (const validatorFn of validatorFns) {
  console.log(validatorFn(valueCtrl));
  // 1. { minlength: { requiredLength: 3, actualLength: 1 } }
  // 2. { email: true }
}

把 for loop 的每一个 ValidationErrors 合并起来是这样

// { 
//   minlength: { requiredLength: 3, actualLength: 1 }, 
//   email: true 
// }

早年的 Angular Team 没有使用 Array 来做管理 (Angular v12.2.0 以后才引入 Array 概念的),而是使用 functional programming 的 function composition 概念做管理。

它运作起来类似这样

const valueCtrl = new FormControl('a');
const composedValidatorFn = Validators.compose([Validators.minLength(3), Validators.email])!;
console.log(composedValidatorFn(valueCtrl));
// { 
//   minlength: { requiredLength: 3, actualLength: 1 }, 
//   email: true 
// }

Validators.compose 会把每一个 ValidatorFn 合并起来变成一个 ValidatorFn,执行这个 ValidatorFn 会返回合并后的 ValidationErrors。

function composition 最大的问题是它把 ValidatorFn 合并起来后就无法拆开了,我们无法 remove 其中一个,也无法获知 ValidatorFn 里头包含了哪些 ValidatorFn。

get existing ValidatorFn info

FormControl 有一个私有属性 _rawValidators

它保存了所有的 ValidatiorFn,不过没有任何公开属性或方法可以直接访问它。

我们只能通过 FormControl.validator 拿到 composed ValidatorFn 就是 Validators.compose(this._rawValidators)

还有一个相关方法是 FormControl.hasValidator(Validators.required),它可以让我们知道当前 ValidatiorFn[] 中是否有包含某个 ValidatiorFn。

我不清楚为什么 FormControl 不提供更多的接口让我们访问 existing ValidatorFn,或许以后会吧。

get / set error collection

FormControl 执行 ValidatorFn 后会把 ValidationErrors 合并起来记入在 FormControl.errors 属性

const valueCtrl = new FormControl('a', {
  validators: [Validators.minLength(3), Validators.email],
});
console.log(valueCtrl.errors); // { minlength: { requiredLength: 3, actualLength: 1 }, email: true }

如果没有 error 那 errors 属性将是 null。

通过 FormControll.getError 方法,可以获取指定 Error Code 的 Error Information。

console.log(valueCtrl.getError('minlength')); // { requiredLength: 3, actualLength: 1 }
// 等价于
console.log(valueCtrl.errors!['minlength']);

注1:Error Code best practice 是用 camelCase,但是 built-in 的 minlength 和 maxlength 却是 lowercase😂

注2:如果没有指定 Error,会返回 undefined。

通过 FormControll.hasError 方法,可以判断指定 Error Code 是否存在。

console.log(valueCtrl.hasError('minlength')); // true
// 等价于
console.log(valueCtrl.errors?.['minlength'] !== undefined);

除了通过 ValidationFn,我们也可以直接通过 FormControl.setErrors 方法修改 FormControl.errors 属性

const valueCtrl = new FormControl('a', {
  validators: [Validators.minLength(10), Validators.email],
});
// set new errors
valueCtrl.setErrors({
  ...valueCtrl.errors, // 保留原本的 errors
  customError: true,   // 添加一个 custom ValidationErrors
});
console.log(valueCtrl.hasError('minlength'));   // true
console.log(valueCtrl.hasError('customError')); // true

这个 setErrors 方法有点类似 Native Form 的 input.setCustomValidity 方法。

AsyncValidatorFn

顾名思义,AsyncValidatorFn 就是指异步的 ValidatorFn。通常验证程序都是同步的执行,但有时候需要依赖数据库资料等等,这时就可能出现异步执行。

AsyncValidatorFn 的类型是

和 ValidatorFn 一样,只是它返回的是 Promise 或者 RxJS Observable。

Reactive Forms 会把 AsyncValidatorFn 和 ValidatorFn 分开管理。

ValidatorFn 存放在属性 _rawValidators

AsyncValidatorFn 存放在属性 _rawAsyncValidators

FormControl 里,和 Validator 相关的属性方法都有 Async 版本:

  1. addAsyncValidators
  2. removeAsyncValidators
  3. clearAsyncValidators
  4. setAsyncValidators
  5. asyncValidator
  6. hasAsyncValidator

FormControl 初始化 set Validators 也是

不过 errors 是共享的哦,没有 FormControl.asyncErrors 这回事。

什么时候执行 ValidatorFn 和 AsyncValidatorFn?

FormControl 在 initialize 时会执行一遍 Validation。

FormControl.setValue 时会执行一遍 Validation。

只有这两种情况会自动执行 Validation,其余情况下都不会自动执行。包括 addValidators, removeValidators 之后也不会自动执行。

我们可以使用 FormControl.updateValueAndValidity 方法手动执行 Validation,这个方法类似于 Native Form 的 input.checkValidity 方法。

ValidatorFn 和 AsyncValidatorFn 的执行顺序细节

Validation 执行时会把 ValidatorFn 和 AsyncValidatorFn 分开执行,先执行所有的 ValidatorFn,如果有任何验证失败,那就不执行 AsyncValidatorFn 了。

如果所有的 ValidatorFn 都验证成功,那就继续执行所有的 AsyncValidatorFn。

上面我们有提到,FormControl 可以有多个 ValidatorFn / AsyncValidatorFn,它们以 Array 的形式被保存在 _rawValidators /  _rawAsyncValidators 属性。

我们来逛一逛源码,看看这 2 个 Array 的执行顺序细节。

相关源码在 abstract_model.ts

_rawValidators 可以是 ValidatorFn Array 也可以是单独一个 ValidatorFn。

_composedValidatorFn 指的是合并以后的 _rawValidators,它肯定就不是 Array 了,因为合并了嘛。

validator 是一个 getter setter ValidatorFn。

get 的时候返回合并后的 ValidatorFn -- _composedValidatorFn,

set 的时候赋值给 _rawValidators 和 _composedValidatorFn,

由于 set 的时候 value 不可能是 Array,所以它同时满足 _rawValidators 和 _composedValidatorFn 的类型,可以同时赋值给它们俩。

add / remove Validators 内部都调用了 setValidators 方法。因为 _rawValidators 保存着 ValidatorFn Array,所以每次重新 setValidators 都不是问题,

拿 existing ValidatorFn Array + new ValidatorFn 就可以重新 setValidators 了。

每当 setValidators,重新赋值给 _rawValidators 和 composedValidatorFn。

好,上面我们搞清楚了 FormControl 内部是如何维护所有 ValidatorFn 的,接着我们看执行 Validation 的过程

我们看 _composedValidatorFn 是怎样合并出来的。

composeValidators 函数的源码在 validators.ts

这个 compose 函数就是我们上面提到过的 function composition -- Validators.compose 方法。

for loop 执行 ValidatorFn Array,得出 ValidationErrors | null Array

把 Array 变成 Object,类似下面这样

// arrayOfError = [{ required: true }, { email: true }]
// 变成
// errors = { required: true, email: true }

这里有一个重点:FormControl.errors 是在执行完所有 ValidatorFn 后才被赋值的,而不是一边 for loop 执行 ValidatorFn 一边 update FormControl.errors。

好,我们继续看看 AsyncValidatorFn 的 Validation 过程

它的流程和 _runValidator 大同小异,我们直接跳到最后一步就好。

AsyncValidatorFn 和 ValidatorFn 流程大同小异,都是执行完后才 mergeErrors 然后 update FormControl.errors。

小考题:

如果 FormControl 原本有一个 AsyncValidationFn 的 ValidationErrors,后来 setValue 重新执行 Validation,此时有一个 ValidationFn ValidationErrors。

问:之前 AsyncValidationFn 的 ValidationErrors 会保留吗?

答:不会,因为每一次执行 Validation,FormControl.errors 会直接被覆盖掉。

更极端的例子

const valueCtrl = new FormControl('Hello World');
// 1. set custom errors
valueCtrl.setErrors({
  custom: true,
});
console.log(valueCtrl.errors); // { custom: true }

// 2. re-run validation
valueCtrl.updateValueAndValidity();

// 3. custom errors 没了
console.log(valueCtrl.errors); // null

即便是 custom errors 也不会保留,所以奉劝大家不要用那么底层的 setErrors 方法,还是乖乖用 ValidatorFn 来做验证。

delay execute AsyncValidatorFn

每当 FormControl.setValue 就会执行一遍 Validation,这对 AsyncValidatorFn 来说可能会有性能隐患。

试想想,假如每一秒钟有一次 FormControl.setValue,而 AsyncValidatorFn 却需要 2 秒才能完成。那会怎么样?

每一次执行 Validation 之前,FormControl 都会先把未完成的 AsyncValidatorFn 取消掉。

借助这个机制我们可以通过 delay AsyncValidatorFn 执行,做到类似 RxJS debounceTime 的效果。

AsyncValidatorFn

const myAsyncValidationFn: AsyncValidatorFn = () => {
  // 1. 通过 timer 延后 1.5 秒才执行 send Ajax
  //    倘若 1.5 秒内 timer Observable 被 unsubscribe
  //    那后续的 Ajax 自然也被 unsubscribe 不发了
  return timer(1500).pipe(
    switchMap(() => {
      console.log('send ajax');
      // 2. 两秒后 Ajax 回来
      return timer(2000).pipe(
        map(() => {
          console.log('ajax done');
          return null;
        })
      );
    })
  );
};

App 组件

export class AppComponent {
  constructor() {
    const valueCtrl = new FormControl('', {
      asyncValidators: [myAsyncValidationFn],
    });

    // 1. 每一秒 setValue 一次,总共 set 3,setValue 满 3 次就停
    const intervalNumber = setInterval(() => {
      valueCtrl.setValue(valueCtrl.value + 'a');

      if (valueCtrl.value!.length >= 3) {
        clearInterval(intervalNumber);
      }
      console.log(valueCtrl.value);
    }, 1000);
  }
}

效果

3 次 setValue 但却只发了一次 Ajax。这是因为 setValue 时间间隔是 1 秒钟,而 delay 是 1.5 秒,上一次的 delay 还没完就被下一次的 setValue unsubscribe 掉了。

dynamic validation logic

每当 value 发生变化,重新执行一次验证,这是一种非常合理的机制,Reactive Forms 也支持。

然而,有时候即使 value 没有变化,但验证条件却发生了变化,这种情况同样需要重新执行一次验证,很遗憾 Reactive Forms 没有提供这样的机制。

幸好,Reactive Forms 有提供一些底层方法,让我们可以利用它们来实现这套机制:

  1. removeValidators

    移除旧条件的 ValidatorFn 

  2. addValidators

    添加新条件的 ValidatorFn 

  3. updateValueAndValidity

    重新执行一次验证

我们来看例子

这是一个静态的 ValidatorFn

const numberCtrl = new FormControl(2, {
  nonNullable: true, // 先不要管 nonNullable 是什么作用,它和 type-safe 有关,下一 part 才会讲解
  validators: [Validators.min(3)],
});
console.log(numberCtrl.errors); // { min: { min: 3, actual: 2 } }

条件是 value 最少需要有 3 才合格,当前只有 2,所以不合格。

假设 value 不变,但条件从最少需要有 3 改成最少需要有 2,那原本的不合格就应该要变成合格。

// 1. 把 ValidatorFn 存入 variable
const min3ValidatorFn = Validators.min(3);

const numberCtrl = new FormControl(2, {
  nonNullable: true,
  validators: [min3ValidatorFn],
});

console.log(numberCtrl.valid); // 2. valid = false, 此时是不合格的

// 3. 创建一个新规则的 ValidatorFn
const min2ValidatorFn = Validators.min(2);

// 4. remove 旧规则的 ValidatorFn
numberCtrl.removeValidators(min3ValidatorFn);

// 5. add 新规则的 ValidatorFn
numberCtrl.addValidators(min2ValidatorFn);

// 6. 重新执行一遍 validation
numberCtrl.updateValueAndValidity();

console.log(numberCtrl.valid); // 7. valid = true, 此时变成合格的了

代码非常粗糙,但是 it work。

接下来我们用 RxJS 来美化它。

// 1. 初始化不设置 validators 了,改成动态设置
//    缺点是会多执行一次验证
const numberCtrl = new FormControl(2, {
  nonNullable: true,
});

// 2. 把变数抽出来,变成一个 stream
const minBS = new BehaviorSubject(3);
minBS
  .pipe(
    map(min => Validators.min(min)), // 3. 变数 -> ValidatorFn
    startWith(null),
    pairwise()
  )
  .subscribe(([prevValidatorFn, currValidatorFn]) => {
    // 4. 如果有旧规则的 ValidatorFn 就移除它,第一次不会有,所以是 null
    if (prevValidatorFn) numberCtrl.removeValidators(prevValidatorFn);
    // 5. 添加新规则的 ValidatorFn
    numberCtrl.addValidators(currValidatorFn!);
    // 6. 重新执行一遍验证
    numberCtrl.updateValueAndValidity();
  });

console.log(numberCtrl.valid); // 7. false, 此时是不合格的
minBS.next(2); // 8. 修改规则
console.log(numberCtrl.valid); // 8. true,此时变成合格的了

接着我们把它封装起来,让它支持不同的 ValidatorFn。

function setDynamicConditionalValidator<T>(config: {
  control: FormControl;
  dependency$: Observable<T>;
  validatorFn: (dependency: T) => ValidatorFn;
}) {
  const { dependency$, validatorFn, control } = config;
  const validatorFn$ = dependency$.pipe(
    map(dependency => validatorFn(dependency)),
    startWith(null),
    pairwise()
  );
  validatorFn$.subscribe(([prevValidatorFn, currValidatorFn]) => {
    if (prevValidatorFn) control.removeValidators(prevValidatorFn);
    control.addValidators(currValidatorFn!);
    control.updateValueAndValidity();
  });
}

使用

const numberCtrl = new FormControl(2, {
  nonNullable: true,
});

const minBS = new BehaviorSubject(3);

setDynamicConditionalValidator({
  control: numberCtrl,
  dependency$: minBS,
  validatorFn: min => Validators.min(min),
});

console.log(numberCtrl.valid); // false;
minBS.next(2);
console.log(numberCtrl.valid); // true;

换成 max ValidatorFn

const numberCtrl = new FormControl(2, {
  nonNullable: true,
});

const maxBS = new BehaviorSubject(3);

setDynamicConditionalValidator({
  control: numberCtrl,
  dependency$: maxBS,
  validatorFn: max => Validators.max(max),
});

console.log(numberCtrl.valid); // true;
maxBS.next(1);
console.log(numberCtrl.valid); // false;

涉及到的 FormControl 属性和方法:

addValidators, removeValidators, clearValidators, setValidators, validator, hasValidator, errors, getError, hasError, setErrors,

addAsyncValidators, removeAsyncValidators, clearAsyncValidators, setAsyncValidators, asyncValidator, hasAsyncValidator, updateValueAndValidity

FormControl as Value Accessor

FormControl 除了是 Value 管理器,Value 验证器 (Validator),它还是 Value Accessor (读写器)。

我们知道 HTML 有一些 built-in 的 Accessor,比如:input, select, textarea 等等

从这个角度看的话,FormControl 就有点像是 HTMLInputElement 对象。

disabled

input 有 disable 概念,当一个 input 被 disable 以后,它就没有 Validation 了。

const input = document.createElement('input');
input.required = true;
console.log(input.validationMessage); // Please fill out this field.
input.disabled = true;
console.log(input.validationMessage); // ''.

FormControl 也有这个相同的 disable 概念

const valueCtrl = new FormControl('', {
  validators: [Validators.required],
});
console.log(valueCtrl.errors);  // { required: true }
valueCtrl.disable();
console.log(valueCtrl.errors);  // null

当 FormControl 被 disable 以后,errors 就变成 null 了。

其它同类操作

const valueCtrl = new FormControl('', {
  validators: [Validators.required],
});
console.log(valueCtrl.disabled); // 返回 boolean,用于查看是否被 disabled 了
console.log(valueCtrl.enabled);  // enabled 就是 disabled 的反向. if(!disabled) 和 if(enabled) 是等价的
valueCtrl.disable();             // set disabled = true, enabled = false
valueCtrl.enable();              // set disabled = false, enabled = true

disable 会把 errors set to null,enable 会 re-run Validation。

default value & reset

input 有 default value 概念,它是搭配 form reset 一起使用的。

const form = document.createElement('form');
const input = document.createElement('input');
form.appendChild(input);
input.defaultValue = 'default value';
input.value = 'new value';
console.log(input.value); // 'new value'
form.reset();
console.log(input.value); // 'default value'

当 form reset,input.value 也会被 reset to default value。

FormControl 也有 default value 和 reset 概念,只是在接口调用上有些区别:

  1. nullable 概念和 default value

    默认情况下,所有 value 类型都是 nullable 的。

    FormControl.defaultValue 也是 null。

    如果我们要有 defaultValue 那就需要去掉默认的 nullable 概念。

  2.  FormControl 不需要搭配 form reset,它自己就有 reset 方法

    const valueCtrl = new FormControl('default value', {
      nonNullable: true,
    });
    valueCtrl.setValue('new value');
    console.log(valueCtrl.value); // 'new value'
    valueCtrl.reset();
    console.log(valueCtrl.value); // 'default value'

interface for Accessor Element

<input>, <textarea>, <select> 这些是 Accessor Element,它们对应的 JS 对象是 HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement,这些是游览器规定好的。

从某种角度上来说 FormControl 可以被认为是 HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement 以及所有的 Accessor 对象,

而它的 Accessor Element 则没有规定,任何 element 都可以成为 FormControl 的 Accessor Element。

FormControl 和 Accessor Element 的关系是这样的:

  1. FormControl 有 value,Accessor Element 要呈现这个 value 给用户看。

  2. 每当 FormControl value changes (by programmatically),Accessor Element 也要做改动呈现新的 value 给用户看。

    这个过程叫 Model to View change / update。

  3. 反过来,用户也可以通过 Accessor Element 修改 value。(读写器嘛,可以看,也可以改)

  4. 每当 Accessor Element value changes (by user),FormControl 也需要 set new value。

    这个过程叫 View to Model change / update。

我们看个例子,假设有一个 FormControl 它要以 <input> element 作为它的 Accessor Element。

const valueCtrl = new FormControl('Hello World', { nonNullable: true });
const input = document.createElement('input');
input.value = valueCtrl.value;
input.defaultValue = valueCtrl.defaultValue;

// Model to View
valueCtrl.registerOnChange((value: string) => {
  input.value = value;
});

// View to Model
input.addEventListener('input', () => {
  valueCtrl.setValue(input.value, {
    emitModelToViewChange: false,
  });
});

有两个知识点:

  1. Model to View 没有使用 FormControl.valueChanges 而是使用了 registerOnChange。

    valueChanges 和 registerOnChange 都可用来监听 value 变更,但它们的 dispatch 时机是不同的,下一 part 我会详细讲解这个部分。

    这里我们只要知道 Model to View 通常 (Angular Best Practice) 使用 registerOnChange。

  2. View to Model 执行 FormControl.setValue 时,设置了 emitModelToViewChange: false。

    它的意思是 "这不是 Model to View Change",对应的意思就是 "它是 View to Model Change"。

    emitModelToViewChange: false 不会 dispatch OnChange 事件,也就是说上面 registerOnChange 的 callback 不会被执行。

    其实挺合理的,registerOnChange 是 for Model to View,如果 setValue 不是 for Model to View 那 registerOnChange 就不应该被执行。

  3. 一下 Model to View,一下 View to Model,绕来绕去有时候挺晕的,背起来:

    a. Model 指的是 FormControl,View 指的是 Accessor Element。

    b. Model to View = 监听 Model -> 更新 View,使用 registerOnChange 做监听。

    c. View to Model = 监听 View -> 更新 Model,使用 setValue + emitModelToViewChange: false 做更新。

除了 value,disabled 也是需要同步的

const valueCtrl = new FormControl('Hello World', { nonNullable: true });
const input = document.createElement('input');
input.disabled = valueCtrl.disabled;

// Model to View
valueCtrl.registerOnDisabledChange(disabled => {
  input.disabled = disabled;
});

// View to Model
const mutationObserver = new MutationObserver(ee => {
  input.disabled ? valueCtrl.disable() : valueCtrl.enable();
});
mutationObserver.observe(input, {
  attributes: true,
  attributeFilter: ['disabled'],
});

不像同步 value 那样混乱,同步 disabled 的接口是统一的。

interface for Accessor Element UX (user experience)

Accessor Element 对用户体验要求很高,这就导致除了 value, disabled 以外,我们经常需要维护其它状态,比如:

  1. touched

    touched 表示这个 Accessor Element 已经被用户触碰过了

    比如说 <input> element,用户 focus and then blur 过后就算 touched。

  2. dirty

    比如说 <input> element,用户 typing 触发 input event 过后就算 dirty。

这些状态和 Value Controller 无关,和 Value Accessor 无关 (毕竟 Native Form 也没有这些),

和 Value Validator 倒有那么一点点关系,因为 touched 和 dirty 经常用于判断是否要显示 error message。

所以呢,它有点尴尬,你说让 FormControl 来管理吧,好像有点管多了,你说不让它管理吧,它的位置又挺适合的。

总之呢,最后 FormControl 是接管了这 2 个状态管理。

const valueCtrl = new FormControl('Hello World', { nonNullable: true });
// 接口设计和 disable / enable 是一摸一样的
console.log(valueCtrl.untouched);
console.log(valueCtrl.touched);
valueCtrl.markAsTouched();
valueCtrl.markAsUntouched();

// 接口设计和 disable / enable 是一摸一样的
console.log(valueCtrl.pristine);
console.log(valueCtrl.dirty);
valueCtrl.markAsPristine();
valueCtrl.markAsDirty();

用法和 disable / enable 是一样的,只是名字不同而已。它们不影响 Validation 任何规则,纯粹只是提供给 Accessor Element 做用户体验的而已。

除了 touched 和 dirty,FormControl 还有一个属性和用户体验或性能有关

const valueCtrl = new FormControl('Hello World', {
  nonNullable: true,
  updateOn: 'blur', // 'change' | 'blur' | 'submit'
});

console.log(valueCtrl.updateOn);

FormControl.updateOn 表示 Accessor Element 要在什么时机做 View to Model 更新。

比如说,Accessor Element 是 <input> element,

updateOn: 'change' 表示监听 input event 做 View to Model 更新。

updateOn: 'blur' 表示监听 blur event 做 View to Model 更新。

updateOn: 'submit' 表示监听 form submit event 做 View to Model 更新。

涉及到的 FormControl 属性和方法:

disable, disabled, enabled, enable, defaultValue, reset, registerOnChange, registerOnDisabledChange, 

untouched, touched, markAsTouched, markAsUntouched, pristine, dirty, markAsPristine, markAsDirty, updateOn

FormControl Status

FormControl 三大职责 (Value Controller, Value Validator, Value Accessor) 都介绍完了,接下来是综合知识。

FormControl 有一个 status 属性,它表示 FormControl 当前的状态。

一共有 4 种状态:Valid, Invalid, Pending, Disabled

简单介绍一下:

  1. Valid 就是 ok 咯,没有任何状况。
  2. Invalid 指的是有 errors (ValidatorFn 或 AsyncValidatorFn 有 failed 就对了)。
  3. Pending 指的是正在验证 AsyncValidatorFn,在等待结果中。
  4. Disabled 指的是被 disable 了。

有好几个方法会改变 status:

  1. disable

    执行 disable 方法后,status 会变成 Disabled。

  2. updateValueAndValidity (called by FormControl constructor, setValue, enable)

    initial status 是说,如果当前不是 Disabled 那就是 Valid。

    _allControlsDisabled 绕一圈其实就是看当前 status 是不是 Disabled。

    _calculateStatus 方法我们下面才讲解。

    FormControl constuctor 时会调用 updateValueAndValidity。

    setValue 时会调用 updateValueAndValidity。

    enable 时会调用 updateValueAndValidity。

  3. setErrors

    setErrors 也会调用 _calculateStatus 方法。

  4. markAsPending

    执行 markAsPending 方法后,status 会变成 Pending。

上面好几个方法都是会改变 status 的,有些是直接 set 一个 Status,有些则是通过 _calculateStatus 方法。

calculate status 的规则是这样的

有顺序的哦:

  1. 如果当前的 status 是 Disabled 那就是 Disabled。

  2. 如果不是 Disabled,那就看有没有 validation errors,有的话就 Invalid。

  3. 没有 errors 或许是还在验证着 AsyncValidatorFn,查看 _hasOwnPendingAsyncValidator

    _hasOwnPendingAsyncValidator 在执行 AsyncValidatorFn 会是 true。

    如果是在验证,那 status 就是 Pending。

  4. _anyControlsHaveStatus 和 _anyControlsHaveStatus 我们先忽略它们,它们是 FormGroup 和 FormArray 的知识,下面会教。

  5. 不是 Disabled,不是 Invalid,不是 Pending,那就是 Valid 咯。

status 总结

Disabled 最大,其二是 Invalid 表示有 validation errors,其三是 Pending 表示有正在执行的 Async Validation,最后是 Valid。

有 2 个方法可以直接写入 status,一个是 disable,一个是 markAsPending,其余的方法最终都是 calculate status。

markAsPending 经验分享

我曾经用 status: Pending 来表示 uploading file。当 uploading 的时候,我就 markAsPending。

理论上来说这是合理的,因为 Pending 只表达了 "等待",并没有表达 "为什么等待"。

可事实是 Pending 潜台词是 Validation Pending,不是其它的东西 Pending。

const valueCtrl = new FormControl('Hello World');

// 1. markAsPending
valueCtrl.markAsPending();
console.log(valueCtrl.status); // 'PENDING'

// 2. re-run validation
valueCtrl.updateValueAndValidity();

console.log(valueCtrl.status); // 'Valid'

在 re-run validation 之后,我设置的 status Pending 就被覆盖了,因为它的潜规则是 Validation 执行完了自然就没有 Pending 了咯。

这里只是分享给各位,有时候框架里是有很多潜规则的,它没有说可以,很可能就是不可以,而不是反过来。

computed status

FormControl 有几个派生自 status 的属性。

const valueCtrl = new FormControl('Hello World');
console.log(valueCtrl.valid);    // status === 'VALID'
console.log(valueCtrl.invalid);  // status === 'INVALID'
console.log(valueCtrl.pending);  // status === 'PENDING'
console.log(valueCtrl.disabled)  // status === 'DISABLED'
console.log(valueCtrl.enabled)   // status !== 'DISABLED'

没什么特别,只是为了提升 DX (Development Experience) 而已。

涉及到的 FormControl 属性和方法:

status, valid, invalid, pending, markAsPending

FormControl Event Emit Timing

FormControl 有 2 个属性值可以被监听,一个是 value,一个是 status。

但是,监听方法却有 4 个🤔,不只方法多,而且行为还很反直觉,我们一起来看看吧。

statusChanges

const valueCtrl = new FormControl('Hello World');
valueCtrl.statusChanges.subscribe(() => console.log('status changes'));

注:statusChanges 类型是 EventEmitter (类似 RxJS 的 Subject)。

顾名思义,应该是每当 status 变更的时候发布的吧。

问:如果 status 从 Pending change to Pending 会发布吗?

是的,每一次都会发布事件,有没有被惊喜到😏?

如果我们只希望在 status 真的改变的时候才接收,可以通过 RxJS Operators 做过滤。

registerOnDisabledChange

顾名思义,就是监听 status 从其它变更到 Disabled,或者从 Disabled 变更到其它。

也许我们预期的效果是这样的

// 1. 每当 status changes 
const disabled$ = valueCtrl.statusChanges.pipe(
  startWith(valueCtrl.status),
  // 2. 转换成 disabled boolean
  map(status => status === 'DISABLED'),
  // 3. 如果没有改变就不发布了
  distinctUntilChanged(),
  // 4. 过滤掉 startWith 造成的第一次
  skip(1)
);
disabled$.subscribe(disabled => console.log('disabled change', disabled));

但其实它内部是这样的

注:registerOnDisabledChange 不是 RxJS 风格,它是 register callback function 风格。

只要执行 disable 和 enable 方法,onDisabledChange 事件就会发布。

valueChanges

不用看也知道,它肯定不是单纯的 value 变更发布事件。

注:valueChanges 类型是 EventEmitter (类似 RxJS 的 Subject)。

没错,有好几个方法会发布 valueChanges 事件:

  1. disable

  2. updateValueAndValidity (called by FormControl constructor, setValue, enable)

如果我们只希望在 value 真的改变的时候才接收,可以通过 RxJS Operators 做过滤。

const valueCtrl = new FormControl('Hello World');
valueCtrl.valueChanges
  .pipe(startWith(valueCtrl.value), distinctUntilChanged(), skip(1))
  .subscribe(() => console.log('value change'));

valueChanges 的类型是 EventEmitter (类似 RxJS 的 Subject),它不是 BehaviorSubject。

这意味着 sbuscribe 不会立刻接收到 value,必须要等到下一次 emit 才会接收到 new value。

有时候我们会希望 subsribe 时可以立刻接收到当前的 value,只要把上面代码中的 skip(1) Operator 去掉就可以了。

const valueCtrl = new FormControl('Hello World');
valueCtrl.valueChanges
  .pipe(startWith(valueCtrl.value), distinctUntilChanged())
  .subscribe(() => console.log('value change')); // 1. 会立刻接收到 value

startWith 会在 stream 创建的时候把 value 存起来,到 subscribe 时发布。

在一些特殊场景中,创建和 subscribe 可能会发生在不同时段,这间中 value 可能会发生变化,这种情况需要使用 defer Operator。

const valueCtrl = new FormControl('Hello World');
// stream 创建和 subscribe 在不同时段,间中 value 发生了变更的情况
const value$ = valueCtrl.valueChanges.pipe(startWith(valueCtrl.value), distinctUntilChanged());
valueCtrl.setValue('New Value');
value$.subscribe(v => console.log(v)); // 拿到的是 'Hello World',而不是 'New Value'

正确做法是

const valueCtrl = new FormControl('Hello World');
const value$ = defer(() => valueCtrl.valueChanges.pipe(startWith(valueCtrl.value), distinctUntilChanged()));
valueCtrl.setValue('New Value');
value$.subscribe(v => console.log(v)); // 拿到的是 'New Value'

registerOnChange

registerOnChange 的 change 指的是 value change。它不像 valueChanges 那样 disable, enable 也乱发布 valueChanges event。

它只有在 setValue 时发布。

注:registerOnDisabledChange 不是 RxJS 风格,它是 register callback function 风格。

disable event emit

我们除了可以通过 RxJS Operators 过滤掉不相干的接收,还可以在源头阻止事件发布。

const valueCtrl = new FormControl('Hello World', { nonNullable: true });
valueCtrl.markAsPending({ emitEvent: false });
valueCtrl.disable({ emitEvent: false });
valueCtrl.reset(undefined, { emitEvent: false });
valueCtrl.enable({ emitEvent: false });
valueCtrl.updateValueAndValidity({ emitEvent: false });
valueCtrl.setErrors(null, { emitEvent: false });
valueCtrl.setValue('', { emitEvent: false });

emitEvent: false 表示不要发布 valueChanges 和 statusChanges (一定是两个一起配置,不能选) 事件,默认是会发布。

registerOnDisabledChange 是无法从源头阻止发布的。

const valueCtrl = new FormControl('Hello World', { nonNullable: true });
valueCtrl.setValue('', {
  emitModelToViewChange: false,
});

emitModelToViewChange: false 可以阻止 registerOnChange 发布。

总结

FormControl 的事件发布挺乱的,个人建议尽量使用 RxJS Operators 做各种过滤,确保即使它乱发布,你也能正确的接收。

涉及到的 FormControl 属性和方法:

statusChanges

 

FormControl with Accessor Element

上一 part 我们有讲到 FormControl 通常会配搭 Accessor Element 一起使用。

Angular 利用指令在原生 HTML Accessor Element (e.g. input, textarea, select 等等) 进行了包装,让它们成为 FormControl 的 Accessor Element。

这 part 我们就来逛一逛相关源码,看看这些 Accessor Element 如何与 FormControl 互动,最后我们再自己做一个 Accessor 组件。

App 组件,创建一个 FormControl

export class AppComponent {
  valueCtrl = new FormControl('Hello World');
}

App Template

<input [formControl]="valueCtrl">

<button (click)="valueCtrl.setValue('new value')">
  Change Value Programmatically
</button>

<p>value: {{ valueCtrl.value }}</p>

<button> 是 Model to View Update

<input> 是 View to Model Update

效果

FormControlDirective 和 DefaultValueAccessor 指令

App Template 中其实有两个指令。

  1. 第一个是 FormControl 指令 (源码在 form_control_directive.ts)

    通过 @Input 我们把 FormControl 对象传进去给了 FormControl 指令。

  2. 第二个指令是 DefaultValueAccessor 指令 (源码在 default_value_accessor.ts)

    [formControl] attribute + <input> element 就成了 DefaultValueAccessor 指令。

要实现 FormControl with Accessor Element,我们一定要用到 FormControl 指令。这个指令负责管理 FormControl 对象和 Accessor Element 之间的所有互动。

它可以应对任何 Accessor Element (input, textarea, select, custom component 等等)。

我们理一理大体流程:

  1. 首先我们有 FormControl 对象

  2. 然后我们有 FormControl 指令

  3. 通过 @Input 我们把 FormControl 对象交给了 FormControl 指令管理。

  4. 接着我们有 DefaultValueAccessor 指令 (它就是 FormControl 的 Accessor Element)

  5. 下一步是 FormControl 指令需要和 DefaultValueAccessor 指令建立联系,

    这样才能让 FormControl 对象和 Accessor Element 互动。

  6. 指令和指令如何建立联系?自然是用 Dependency Injection (DI) 咯。

    DefaultValueAccessor 指令有一个 Provider

    FormControl 指令会 inject NG_VALUE_ACCESSOR token。

  7. 至此,FormControl 指令就有了 FormControl 对象和它对应的 Accessor Element,接着它就可以 setup View to Model 和 Model to View 这些逻辑了。

FormControl 对象,我们很熟悉了。

FormControl 指令我们暂时不需要深入,只要知道它负责管理 FormControl 对象和对应的 Accessor Element 就可以了。

DefaultValueAccessor 指令,我们需要深入研究一下,因为待会儿我们要做一个 Custom Accessor Element 取代它。

ControlValueAccessor 接口

任何 FormControl Accessor Element (不管是 input, textarea, select 还是 custom component) 都一定要实现 ControlValueAccessor 接口,因为这样 FormControl 指令才能与其做互动。

ControlValueAccessor 接口长这样

我们看看 DefaultValueAccessor 指令对 ControlValueAccessor 接口的具体实现

  1. writeValue

    writeValue 用于 Model to View 更新。具体实现就是要把 value 展现给用户。

    DefaultValueAccessor 指令控制的是 <input> element,所以是 input.value = 'value from model'。

    _renderer 是 Angular 对 DOM 的封装,底层是 DOM Manipulation

  2. registerOnChange

    registerOnChange 会接收到一个函数 fn,这个 fn 用于 View to Model 更新。

    把这个 fn 保存起来,当 <input> value changes 时调用它 fn(input.value)。

    监听 input 事件然后执行保存起来的 onChange 函数。compositionstart 和 compositionend 是为了处理中文输入,不熟悉的朋友,可以看这篇

  3. registerOnTouched

    registerOnTouched 也是 View to Model 更新,只是它更新的不是 value 而是 touched。

    input blur 后代表 touched。

  4. setDisabledState

    setDisabledState 是 Model to View 更新,更新的不是 value 而是 disabled。

    相等于 input.disabled = isDisabled;

除了 DefaultValueAccessor,Reactive Forms 还 built-in 了好几个 ControlValueAccessor,有兴趣的可以自行逛一下源码:

  1. CheckboxControlValueAccessor -- input checkbox
  2. DefaultValueAccessor -- input & textarea
  3. NumberValueAccessor -- input number
  4. RadioControlValueAccessor -- input radio
  5. RangeValueAccessor input range
  6. SelectControlValueAccessor select 
  7. SelectMultipleControlValueAccessor select with multiple

BaseControlValueAccessor

BaseControlValueAccessor 是一个抽象指令。它简单的实现了大部分 ControlValueAccessor 接口 (除了 writeValue 方法没有实现)。

大部分 built-in 的 Accessor 指令都继承了它,很可惜 Reactive Forms 没有公开这个 class,我们无法继承。

Custom Accessor Element

首先创建一个 Counter 组件

ng g c counter

Counter 组件

export class CounterComponent {
  number = 0;
  add() {
    this.number++;
  }
  minus() {
    this.number--;
  }
}

一个 number,可以 add,可以 minus。

Counter Template

<h1>{{ number }}</h1>

<div class="action">
  <button (click)="minus()">minus</button>
  <button (click)="add()">add</button>
</div>

把 Counter 添加到 App Template

<app-counter />

<button (click)="numberCtrl.setValue(100)">
  Change Number Programmatically
</button>

<p>number: {{ numberCtrl.value }}</p>

我把原本的 <input> 换成了 <app-counter>,valueCtrl 也换成了 numberCtrl。

效果

现在 FormControl 和 Counter Accessor Element 是完全没有关系的。我们要用 FormControl 指令将它们俩关联起来。

当然在此之前,我们需要让 Counter 组件实现 ControlValueAccessor 接口,并且把组件自身 provide as NG_VALUE_ACCESSOR,这样 FormControl 组件才可以注入到 ControlValueAccessor。

Counter 组件 implements ControlValueAccessor

看注释理解

@Component({
  selector: 'app-counter',
  standalone: true,
  imports: [],
  templateUrl: './counter.component.html',
  styleUrl: './counter.component.scss',
  providers: [
    // 1. 把组件自身 provide as NG_VALUE_ACCESSOR
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: CounterComponent,
      // 2. 一定要加 multi 哦
      //    原因是 Reactive Forms 允许我们 override built-in 的 ControlValueAccessor
      //    比如我不满意 DefaultValueAccessor 对 input as ControlValueAccessor 的处理
      //    那我也写了一个 ControlValueAccessor for input
      //    那 FormControl 指令注入时就会拿到 2 个 NG_VALUE_ACCESSOR -- ControlValueAccessor
      //    它会优先选择 Custom ControlValueAccessor。
      multi: true,
    },
  ],
})
export class CounterComponent implements ControlValueAccessor {
  // 3. 四个接口都挺简单的,字面意思
  writeValue(number: number): void {
    this.number = number;
  }

  private onChange!: (number: number) => void;
  registerOnChange(fn: (number: number) => void): void {
    this.onChange = fn;
  }

  private onTouched!: () => void;
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  // 4. 加多了一个 disabled 例子
  disabled = false;
  number = 0;
  add() {
    this.number++;
    // 5. 当 value changes
    //    执行 View to Model value Update
    this.onChange(this.number);
    // 6. 同时也执行 View to Model disabled Update
    this.onTouched();
  }
  minus() {
    this.number--;
    // 7. minus 也一样
    this.onChange(this.number);
    this.onTouched();
  }
}

Counter Template

<h1>{{ number }}</h1>

<div class="action">
  <!-- 1. 加多了一个 disabled 例子  -->
  <button (click)="minus()" [disabled]="disabled">minus</button>
  <button (click)="add()" [disabled]="disabled">add</button>
</div>

App Template

<!-- 1. 添加 FormControl 指令 -->
<app-counter [formControl]="numberCtrl" />

<button (click)="numberCtrl.setValue(100)">
  Change Number Programmatically
</button>

<!-- 加多了一个 disabled 例子 -->
<button (click)="numberCtrl.disable()">
  Disable
</button>

<p>number: {{ numberCtrl.value }}</p>
<!-- 加多了一个 touched 例子 -->
<p>touched: {{ numberCtrl.touched }}</p>

最终效果

第一次 writeValue 执行的时机

每一次 Model to View update,writeValue 方法都会被调用。

当然也包括第一次。而第一次调用的时机是在 Custom Accessor 组件 OnInit 之后,AfterContentInit 之前。

 

FormControl 指令源码逛一逛

FormControl 指令包含了 Reactive Forms 和 Template-driven Forms 两种模式的代码,我们只逛 Reactive Forms 就好。主要是看 FormControl 对象和 ControlValueAccessor 指令之间的互动。

FormControl 指令的源码在 form_control_directive.ts

@Input FormControl

inject ControlValueAccessor 指令

choose the right ControlValueAccessor 指令

selectValueAccessor 函数

优先使用 Custom ControlValueAccessor 指令。

connect FormControl 对象 and ControlValueAccessor 指令

setUpControl 函数的源码在 shared.ts

继续看细节:

  1. setUpViewChangePipeline 函数

    registerOnChange 是 ControlValueAccessor 接口的四大方法之一。

  2. setUpModelChangePipeline 函数

    writeValue 是 ControlValueAccessor 接口的四大方法之一。

  3. setUpBlurPipeline

    registerOnTouched 是 ControlValueAccessor 接口的四大方法之一。

  4. setUpDisabledChangeHandler

    setDisabledState 是 ControlValueAccessor 接口的四大方法之一。

总结

FormControl 指令的主要职责就是同步 FormControl 对象和 ControlValueAccessor 指令,也就是所谓的 Model to View 和 View to Model。

做法很简单,调用 FormControl 对象和 ControlValueAccessor 指令的方法,做各种 Model / View 监听,然后更新各种 View / Model 属性。

FormControl 对象和 FormControl 指令我们很少会做 customize (因为一做就要做一大套改动),

经常会 customize 的是 ControlValueAccessor 指令,所以 Custom ControlValueAccessor 指令需要 implements 指定接口,这样才能确保 FormControl 指令知道怎么调用它。

 

FormGroup

FormGroup 是一个 Group,group 什么呢?-- group FormControl。

FormGroup 长这样

const formGroup = new FormGroup({
  name: new FormControl('Derrick'),
  email: new FormControl('hengkeat@gmail.com'),
});
console.log(formGroup.value); // { name: 'Derrick', email: 'hengkeat@gmail.com' }

它以对象的形式 (key value pair) 把 FormControl group 起来,每一个 FormControl 都被赋予了一个 key / name。

FormControl Typesafe

在讲解 FormGroup Structure 之前,我们需要先补上 FormControl Typesafe 概念,因为 Typesafe 和 FormGroup Structure 有密切关系。

FormControl 是有类型管理的 (v14 版本以前是没有的哦)。

创建 FormControl

TypeScript 会从参数 'Hello World' 推导出 string 类型。

setValue 时,如果传入的 value 不符合类型,IDE 就会报错。这就叫 Typesafe。

默认情况下,FormControl 一定是 nullable,所以 'Hello World' 推导出来的类型是 string | null,而不是单单 string。

我们可以通过 { nonNullable: true } 将它设置成 non-nullable。

我们也可以通过泛型指定它的类型,让它支持 union types / multiple types

const valueCtrl = new FormControl<string | number>('Hello World', { nonNullable: true });
valueCtrl.setValue(100);           // no type error
valueCtrl.setValue('Hello World'); // no type error
valueCtrl.setValue(true);          // type error!

甚至是 any types

const value1Ctrl = new FormControl();          // type: FormControl<any>, default value is null
const value2Ctrl = new FormControl<any>(null); // type: FormControl<any>, must provide any default value
const value3Ctrl = new UntypedFormControl();   // type: FormControl<any>, default value is null

3 种写法虽然效果是一样的,但表达却是不一样的。

第一种写法是因为没给 value,TypeScript 推导不出类型,所以它是 any。(注:如果 FormControl 没设置初始值,它默认的初始值是 null)

第二种写法是通过泛型明确表达类型是 any,这个写法也会导致我们必须提供初始值 (因为 new FormControl 有不同的 overload 方法),我个人是比较推荐这个写法的,因为它有明确的表明。

第三种写法表达的是不要 Typesafe,当初 v13 migrations v14,CLI 会自动把旧版本的 new FormControl 改成 new UntypedFormControl,因为 v14 以前是没有 Typesafe 概念的。

FormGroup Structure & Typesafe

好,回到 FormGroup。

FormGroup 是对象结构,那自然少不了一些改动结构的方法,比如:get/set/has/add/remove/loop property 等等。

get FormControl from FormGroup

const formGroup = new FormGroup({
  name: new FormControl('Derrick', { nonNullable: true }),
  email: new FormControl('hengkeat@gmail.com', { nonNullable: true }),
  age: new FormControl(11, { nonNullable: true }),
});

const emailCtrl = formGroup.controls.email; // 1. get FormControl by name
emailCtrl.setValue('alex@gmail.com');

通过 FormGroup.controls.propertyName 可以直接获取到 FormControl。

string literal 的方式也可以

const emailCtrl = formGroup.controls['email'];

但是用 string 则不行

因为它有 Typesafe 限制。

我们可以使用 FormGroup.get 方法突破这个限制。

虽然是没有 Type Error 了,但与此同时我们也失去了类型提示。(注:为什么返回类型是 AbstractControl 而不是 FormControl 下面会讲解,这里我们知道 any 类型就好了)

get all name and FormControl

const allNames = Object.keys(formGroup.controls); // ['name', 'email', 'age']
for (const [name, formControl] of Object.entries(formGroup.controls)) {
  console.log([name, formControl]);
}

FormGroup.controls 就是一个普通的对象,用 Object.key 或 Object.entries 操作它就可以了。

check has FormControl by name

console.log(formGroup.contains('email')); // true
console.log(formGroup.contains('phone')); // false

FormGroup 提供了一个检查 name 是否存在的方法。它的源码是

注意:它还检查了 FormControl 是否是 enabled 状态,也就是说 disabled 的 FormControl 被视为不存在于 FormGroup!

console.log(formGroup.contains('email')); // true
formGroup.controls.email.disable();
console.log(formGroup.contains('email')); // false

remove FormControl

它之所以会报错,是因为 Typesafe。FormGroup 的结构因为涉及 Typesafe 所有它是静态的,不能做过多的改动。如果我们想要 flexible 一点,需要耍点花招。

首先创建一个 FormGroup interface

interface PersonFormGroup {
  name?: FormControl<string>;
  email: FormControl<string>;
  age: FormControl<number>;
}

属性 name? 是 optional 的。

接着在创建 FormGroup 时通过泛型把 interface 传进去。

const formGroup = new FormGroup<PersonFormGroup>({
  name: new FormControl('Derrick', { nonNullable: true }),
  email: new FormControl('hengkeat@gmail.com', { nonNullable: true }),
  age: new FormControl(11, { nonNullable: true }),
});

这样就不会再 type error 了

console.log(formGroup.contains('name')); // true
formGroup.removeControl('name');
console.log(formGroup.contains('name')); // false

add new FormControl

只有 optional property 可以 remove and add。这种限制有时候太局限了,所以我们需要耍更厉害的花招。

使用 FormRecord 替代 FormGroup

const formRecord = new FormRecord({
  name: new FormControl('Derrick', { nonNullable: true }),
  email: new FormControl('hengkeat@gmail.com', { nonNullable: true }),
  age: new FormControl(11, { nonNullable: true }),
});

formRecord.addControl('phone', new FormControl('+65 1122 3344', { nonNullable: true }));

FormRecord 对 Typesafe 比较宽松,它等价于 TypeScript 的 Record

两者的对比用 TypeScript 来表达大概是这样

type FormGroup = { name: string; email: string; age: number };
type FormRecord = Record<string, string | number>;

Record 的好处是可以 dynamic add 任何 name,但是它也牺牲了许多 Typesafe 功能。

另外,Record 还不是最宽松的 Typesafe。

FormRecord<string | number> 表示 FormControl.value 只能是 string | number,如果我们添加一个 boolean 依然会报错。

要完完全全 flexible 到可以 dynamic add whatever name and whatever type,我们要用 FormRecord<any> 或者 UntypedFormGroup

const untypedFormGroup = new UntypedFormGroup({});
const formRecord = new FormRecord<any>({});

untypedFormGroup.addControl('married', new FormControl(true, { nonNullable: true }));
formRecord.addControl('married', new FormControl(true, { nonNullable: true }));

与此同时,我们也失去了所有 Typesafe 的便利。

set / replace FormControl

formGroup.setControl('name', new FormControl('New Name', { nonNullable: true }));
formGroup.setControl('name', new FormControl(11, { nonNullable: true })); // type error!

set / replace 等价于 remove + add。

替换 FormControl 也受 Typesafe 限制,只能换上同样类型的 FormControl。

解决方法是通过泛型设置 Union Types

总之,每当遇到 Typesafe 限制,第一招就是自己定义类型 (union types, optional property 等等),不要用 TypeScript 推导,因为推导是通过 value,一个 value 只能是一个类型,那就不 flexible 了。

还是不行就用杀手锏 any Type (a.k.a Untyped)。

FormGroup.registerControl 方法

registerControl 是 addControl 的底层方法,虽然它是公开的,但我们比较少会用到。

它和 addControl 的区别是

多了两个执行。第一个是执行 Validation (FormGroup 和 FormControl 一样可以有 value 验证逻辑,下一 part 会教),第二件事是发布事件。

nested FormGroup

FormGroup 是可以无限嵌套的。

const formGroup = new FormGroup({
  name: new FormControl('Derrick', { nonNullable: true }),
  email: new FormControl('hengkeat@gmail.com', { nonNullable: true }),
  age: new FormControl(11, { nonNullable: true }),
  address: new FormGroup({
    postalCode: new FormControl('81300', { nonNullable: true }),
    country: new FormControl('Malaysia', { nonNullable: true }),
  }),
});

console.log(formGroup.value);
// {
//   name: 'Derrick',
//   email: 'hengkeat@gmail.com',
//   age: 11,
//   address: { PostalCode: '81300', country: 'Malaysia' }
// }

address 属性不是 FormControl 而是另一个 FormGroup。

也因为这个原因,如果我们用 dynamic string 的方式尝试获取 FormGroup 里的 Control,我们将得到 FormControl 和 FormGroup 的抽象类型 class AbstractControl,而不是具体类型。

这是因为 TypeScript 无法单凭 string 推导出最终拿到的 Control 类型。

所以尽量使用 string literal 的方式获取 Control,这样 TypeScript 才能获推导出具体类型。

const propertyName1: string = 'address';             // 这是 string
const propertyName2 = 'address';                     // 这是 string literal
const control1 = formGroup.get(propertyName1);       // 返回 AbstractControl
const control2 = formGroup.controls[propertyName2];  // 返回 FormGroup

另外,假如嵌套很深,我们想要拿里面的 FormControl 的话,代码就会很长,很丑。

const formGroup = new FormGroup({
  child1: new FormGroup({
    child2: new FormGroup({
      child3: new FormGroup({
        firstName: new FormControl(''),
      }),
    }),
  }),
});

const firstNameCtrl = formGroup.controls.child1.controls.child2.controls.child3.controls.firstName; // 很长很长

这时我们可以用 get + path 来缓解

const firstNameCtrl1 = formGroup.controls.child1.controls.child2.controls.child3.controls.firstName; // 很长很长
const firstNameCtrl2 = formGroup.get('child1.child2.child3.firstName');                              // 短了,因为中间的 controls 都去掉了

虽然是短了,但是类型也跑掉了

使用 get 方法获取到的 Control 是抽象的 AbstractControl,而长长 path 获取到的是具体的 FormControl。

AbstractControl 是所有 Control 的抽象,它的派生有 FormControl、FormGroup、FormArray (这个下面会教)。

显然拿到具体的 FormControl 会更正确,也更方便使用,但没办法,类型和代码长度只能选一个,谁让 Angular 团队 TypeScript 写的那么烂呢😔。。。

get parent FormGroup

FormControl 和 FormGroup 都可以被 FormGroup 包起来,包起来后可以通过 parent 属性获取到把它们包起来的 FormGroup (a.k.a parent)。

const formGroup = new FormGroup({
  name: new FormControl('Derrick', { nonNullable: true }),
});

const nameCtrl = formGroup.controls.name;
console.log(nameCtrl.parent === formGroup); // true

get root FormGroup

最低层的 Control 可以使用 root 属性获取到最顶层的 FormGroup。

const formGroup = new FormGroup({
  name: new FormControl('Derrick', { nonNullable: true }),
  address: new FormGroup({
    postalCode: new FormControl('81300', { nonNullable: true }),
  }),
});

const postalCodeCtrl = formGroup.controls.address.controls.postalCode;
const addressCtrl = formGroup.controls.address;
console.log(postalCodeCtrl.parent === addressCtrl); // true
console.log(postalCodeCtrl.root === formGroup);     // true

formGroup.controls.address.controls.postalCode

上面这句代码很长很丑,遇到这种情况可以考虑使用 get 方法

const postalCodeCtrl1 = formGroup.controls.address.controls.postalCode; // 类型是 FormControl<string>
const postalCodeCtrl2 = formGroup.get('address.postalCode');            // 类型是 AbstractControl<string, string> | null

虽然代码比较短,但是 Typesafe 能力也被削弱了,只能说 everything has a price。

set parent FormGroup

interface PersonFormGroup {
  name: FormControl<string>;
  age?: FormControl<number>;
}

const formGroup = new FormGroup<PersonFormGroup>({
  name: new FormControl('Derrick', { nonNullable: true }),
});

age 是 optionals

const ageCtrl = new FormControl(11, { nonNullable: true });
ageCtrl.setParent(formGroup);

通过 setParent 方法把 FormControl 添加进 FormGroup 里。

或许你以为这个操作和 FormGroup addControl / registerControl 是一样的,但其实不是!

console.log(ageCtrl.parent === formGroup);       // true 
console.log(formGroup.controls.age === ageCtrl); // false

FormControl 视乎没有被添加进 FormGroup,why?

因为 setParent 方法只是 update FormControl.parent 属性而已,要让 FormControl 进入 FormGroup 还需要修改 FormGroup.controls 属性。

像 regsterControl 方法这样才是正确的。

所以尽量不要用 setParent 方法,我觉得它和 registerControl 一样都是忘了 private 的底层方法。 

涉及到的 FormGroup 属性和方法:

controls, get, contains, removeControl, addControl, setControl, registerControl, parent, root, setParent

FormGroup as Batch Manager

FormGroup 和 FormControl 的关系很妙,FormControl 管理一个 value,FormGroup 则管理一群 FormControl 的 value。

基于这一原理,我们可以简单的认为 FormGroup 是对一组 FormControl 进行批量管理的容器。

再举一个例子,当我调用 FormGroup.disable() 方法时,它的效果是 disable 所有的 FormControl (批量 disable)。

正因如此,FormGroup 和 FormControl 共享了 32 个属性和方法接口,这些属性和方法源自它们的抽象类 -- AbstractControl。

上一 part 我们介绍的 45 个 FormControl 属性和方法,其中有很大一部分都来自于这个抽象类 AbstractControl。

尽管属性和方法名字是相同的,但是具体执行却有所不同,因为 FormControl 是操作自身,而 FormGroup 更多地是操作其包含的 FormControl。

好,我们带着这个概念继续往下看看 FormGroup 所有的属性和方法具体怎么执行。

FormGroup as Value Controller

FormControl 有 3 个与 value 相关的属性和方法:value, setValue, valueChages。

这些 FormGroup 也有,而且它有的还不只这些。

创建 FormGroup

const formGroup = new FormGroup({
  name: new FormControl('Derrick'),
  email: new FormControl('derrick@gmail.com'),
});

get value / raw value from FormGroup

console.log(formGroup.value); // { name: 'Derrick', email: 'hengkeat@gmail.com' }

FormGroup.value 一定是个对象,属性名来自 FormGroup,属性值来自于 FormControl.value。

它有一个特别的规则,FormGroup.value 只拿 enabled FormControl,disabled 的它不拿。

console.log(formGroup.value);  // { name: 'Derrick', email: 'derrick@gmail.com' }
formGroup.controls.name.disable();
console.log(formGroup.value);  // { email: 'derrick@gmail.com' }

这个机制不算难理解,因为 Native Form 的 FormData 也是这样。

不过,它还有一个更特别的规则,当所有的 FormControl 都是 disabled,FormGroup.value 会拿到所有属性值,就跟所有 FormControl 是 enabled 一样。

console.log(formGroup.value); // { name: 'Derrick', email: 'derrick@gmail.com' }
formGroup.controls.name.disable();
formGroup.controls.email.disable();
console.log(formGroup.value); // { name: 'Derrick', email: 'derrick@gmail.com' }

这个规则有点反直觉,我不清楚谁设计的,设计的目的是啥,或许是历史原因所以 Angular Team 一直没有改动它。这是相关 Github Issue

另外,FormGroup.value 的更新时机也需要留意。

formGroup.controls.name.valueChanges
  .subscribe(v => console.log(formGroup.value)); // { name: 'Derrick', email: 'derrick@gmail.com' }

formGroup.controls.name.setValue('Alex');

console.log(formGroup.value);                    // { name: 'Alex', email: 'derrick@gmail.com' }

在 FormControl.valueChanges 时 FormGroup.value 是还没有更新的。它一直等到所有 FormControl event 都发布完了后才更新 (即使是使用 FormGroup.setValue 也是一样)。

拿个 value 而已,又要顾虑 disabled 又要顾虑 timing,有没有简简单单直接拿到当下所有 value 的办法?哎哟,还真的有哦。

formGroup.controls.name.disable();
formGroup.controls.email.valueChanges.subscribe(() =>
  // 1. 使用 getRawValue 方法
  console.log(formGroup.getRawValue()) // { name: 'Derrick', email: 'alex@gmail.com' }
);
formGroup.patchValue({ email: 'alex@gmail.com' });

getRawValue 方法可以获得当下所有 FormControl value。

set value to FormGroup

formGroup.setValue({
  name: 'Alex',
  email: 'alex@gmail.com',
});
console.log(formGroup.value);                // { name: 'Alex', email: 'alex@gmail.com' }
console.log(formGroup.controls.name.value);  // 'Alex'
console.log(formGroup.controls.email.value); // 'alex@gmail.com'

setValue 就是批量把 value set 去 FormControl。

setValue 很严格的,必须提供完整的对象才行,不可以缺少任何属性 (optional property 除外)

如果只想更新部分属性值,可以使用 patchValue 方法

formGroup.patchValue({
  name: 'Alex',
});

它的功效和 setValue 是一样的,只是它允许 partial update,而 setValue 只允许 full update。

listen to value changes

formGroup.valueChanges.subscribe(formValue => console.log(formValue.name)); // 'Alex'
formGroup.patchValue({ name: 'Alex' });

和 FormControl.valueChanges 用法一模一样,触发机制也一样 (比如 set same value 它也会触发多次😔)。

这里的 value 指的是 FormGroup.value 哦,也就是说,disabled 的 FormControl 不会出现在 form value 里。

formGroup.valueChanges.subscribe(formValue => console.log(formValue.name)); // undefined
formGroup.controls.name.disable();

子层 FormControl.disabled 也会 trigger 父层 FormGroup.valueChanges。

FormControl 有许多操作都会通知祖先,比如 disable

updateValueAndValidity

还有其它的我就不一一列出来了。

涉及到的 FormGroup 属性和方法:

value, getRawValue, setValue, patchValue, valueChanges

FormGroup as Value Validator

FormControl 有很多与 Validation 相关的属性和方法,比如:addValidators, removeAsyncValidators, errors 等等等,这些 FormGroup 都有,而且机制一模一样 (比如,先执行 validator 再执行 async validator 等等)。

它俩最大的区别是,FormControl 是对自己的 value 做验证,而 FormGroup 是对旗下所有 FormControl.value 做验证 (综合验证)。

不过,有一点要搞清楚哦,FormGroup 和 FormControl 的 validators 是完全分开的,它们各自管理各自的 validators。

看例子

const formGroup = new FormGroup(
  {
    password: new FormControl('abc', { nonNullable: true }),
    confirmPassword: new FormControl('xyz', { nonNullable: true }),
  }
);

需求是,password 和 confirmPassword value 要一致,如果不一致就 invalid。

大家可以想想看,假如使用 FormControl validators 来实现,大概长什么样。

const passwordCtrl = formGroup.controls.password;
const confirmPasswordCtrl = formGroup.controls.confirmPassword;

passwordCtrl.addValidators(c => (c.value === confirmPasswordCtrl.value ? null : { samePassword: true }));
confirmPasswordCtrl.addValidators(c => (c.value === passwordCtrl.value ? null : { samePassword: true }));

passwordCtrl.updateValueAndValidity();
confirmPasswordCtrl.updateValueAndValidity();

console.log(passwordCtrl.errors);        // { samePassword: true }
console.log(confirmPasswordCtrl.errors); // { samePassword: true }

两个 FormControl 都加上 validator,相互比对 value🤔?

不,这还不够,因为 FormControl validators 只会在 value changes 时才执行。

当 password 变更时,confirmPassword 的 validator 是不会执行的,而这会导致 confirmPassword 漏掉验证。

passwordCtrl.setValue('xyz');
console.log(passwordCtrl.errors);        // null <-- 没有 errors 了
console.log(confirmPasswordCtrl.errors); // { samePassword: true } <-- errors 还在!

所以,我们还用上 dynamic validation 才能应对这种情况。

const password$ = defer(() =>
  passwordCtrl.valueChanges.pipe(startWith(passwordCtrl.value), distinctUntilChanged()),
);

const confirmPassword$ = defer(() =>
  confirmPasswordCtrl.valueChanges.pipe(startWith(confirmPasswordCtrl.value), distinctUntilChanged()),
);

setDynamicConditionalValidator({
  control: passwordCtrl,
  dependency$: confirmPassword$,
  validatorFn: confirmPassword => c => (c.value === confirmPassword ? null : { samePassword: true }),
});

setDynamicConditionalValidator({
  control: confirmPasswordCtrl,
  dependency$: password$,
  validatorFn: password => c => (c.value === password ? null : { samePassword: true }),
});

console.log(passwordCtrl.errors);        // { samePassword: true }
console.log(confirmPasswordCtrl.errors); // { samePassword: true }

passwordCtrl.setValue('xyz');
console.log(passwordCtrl.errors);        // null <-- 没有 errors 了
console.log(confirmPasswordCtrl.errors); // null <-- 也没有 errors 了

注:setDynamicConditionalValidator 上面教过了,这里不再赘述。

是不是感觉有点大费周章?

好,我们再看看 FormGroup validators 如何解决相同的问题。

const samePasswordValidator: ValidatorFn = formGroup => {
  if (!(formGroup instanceof FormGroup)) return null;
  if (formGroup.get('password')!.disabled || formGroup.get('confirmPassword')!.disabled) return null;

  const password = formGroup.value.password;
  const confirmPassword = formGroup.value.confirmPassword;
  return password === confirmPassword ? null : { samePassword: true };
};

const formGroup = new FormGroup(
  {
    password: new FormControl('abc', { nonNullable: true }),
    confirmPassword: new FormControl('xyz', { nonNullable: true }),
  },
  {
    validators: [samePasswordValidator],
  },
);

console.log(formGroup.errors); // { samePassword: true }
formGroup.patchValue({ password: 'xyz' });
console.log(formGroup.errors); // null

因为 validator 是 apply 到 FormGroup 上,所以 ValidatorFn 的参数是 FormGroup。

通过 FormGroup 我们可以拿到所有需要的 FormControl,然后对比它们的 value。

另外,不管是使用 formGroup.patchValue 还是 formGroup.controls.password.setValue('xyz'),它们都会触发 FormGroup.valueChanges (任何子 FormControl.valueChanges, FormGroup 都会跟着 valueChanges),

valueChanges 后就会执行 FormGroup validators,因此我们不需要像上一 part 搞 dynamic validation 那样,自己 valueChanges.subscribe 和 updateValueAndValidity,简化了很多呢😊。

FormGroup.errors

FormGroup.errors 来自它自身的 validators,而不是它旗下 FormControl.errors 的总和。

const formGroup = new FormGroup({
  password: new FormControl('', { nonNullable: true, validators: Validators.required }),
  confirmPassword: new FormControl('', { nonNullable: true }),
});

console.log(formGroup.controls.password.errors); // { required: true }
console.log(formGroup.errors);                   // null

FormGroup  和 FormControl 的 errors 是完全没有关联的。

涉及到的 FormGroup 属性和方法:

addValidators, removeValidators, clearValidators, setValidators, validator, hasValidator, errors, getError, hasError, setErrors,

addAsyncValidators, removeAsyncValidators, clearAsyncValidators, setAsyncValidators, asyncValidator, hasAsyncValidator, updateValueAndValidity

由于属性和方法特性都和 FormControl 一样,这里我就不重复讲了。

FormGroup is not Value Accessor

FormControl 作为 Value Accessor,它有 Model to View,View to Model 的概念。

而 FormGroup 没有这些,因为它不是 Value Accessor。

FormControl 有一堆跟 Value Accessor 相关的的属性方法:

disable, disabled, enabled, enable, defaultValue, reset, registerOnChange, registerOnDisabledChange, 

untouched, touched, markAsTouched, markAsUntouched, pristine, dirty, markAsPristine, markAsDirty, updateOn。

这些方法 FormGroup 也有 (除了 defaultValue, registerOnChange, registerOnDisabledChange)。

夷...FormGroup 既然不是 Value Accessor,那它为什么会有这些和 Value Accessor 相关的属性方法呢🤔?

答案是:FormGroup as Batch Manager

FormGroup 的这些属性方法是用来批量管理旗下 FormControl 的。

disabled

const formGroup = new FormGroup({
  password: new FormControl('', { nonNullable: true }),
  confirmPassword: new FormControl('', { nonNullable: true }),
});

console.log(formGroup.disable());
console.log(formGroup.controls.password.disabled);        // true
console.log(formGroup.controls.confirmPassword.disabled); // true

FormGroup.disable 会把旗下所有 FormControl 给 disable 掉。

当 FormGroup 旗下所有 FormControl 都 disabled 后,FormGroup.disabled 也会等于 true。 

formGroup.controls.password.disable();
formGroup.controls.confirmPassword.disable();
console.log(formGroup.disabled); // true (所有 FormControl 都 disabled FormGroup 才是 disabled 哦,只有部分 disabled 的话,FormGroup 依然算是 enabled)

reset

FormGroup 没有 defaultValue,但有 reset 方法。

formGroup.controls.password.setValue('new password');
formGroup.controls.confirmPassword.setValue('new confirm password');
console.log(formGroup.value); // { password: 'new password', confirmPassword: 'new confirm password' }

formGroup.reset();
console.log(formGroup.value); // { password: 'default password', confirmPassword: 'default confirm password' }

等价于 

formGroup.controls.password.reset();
formGroup.controls.confirmPassword.reset();

touched & dirty

touched 和 dirty 的规则和 disabled 不同,为什么会不同呢?只有 Angular 团队自己知道🙄。

formGroup.markAsDirty();
console.log(formGroup.dirty);                   // true 
console.log(formGroup.controls.password.dirty); // false

FormGroup.markAsDirty 只会让 FormGroup dirty,不会让旗下 FormControl 也 dirty。

但反过来就会

formGroup.controls.password.markAsDirty();
console.log(formGroup.controls.password.dirty); // true
console.log(formGroup.dirty);                   // true

简而言之,markAsDirty 会往上 update,但不会往下 update。

markAsTouched 也是同一个规则,但 FormGroup 有一个特别的方法叫 markAllAsTouched,顾名思义,它的作用就是调用旗下 FormControl.markAsTouched。

注:只有 markAllAsTouched,没有 markAllAsDirty 哦... 厉害了吧,我的 Angular 团队🙄

updateOn

FormControl 和 FormGroup 都有属于自己的 updateOn 属性,它的的规则是这样

假如自身有就用自身的,假如自身没有就用 parent 的,假如祖先都没有那默认是 "change"。

涉及到的 FormGroup 属性和方法:

disable, disabled, enabled, enable, reset, 

untouched, touched, markAsTouched, markAsUntouched, markAllAsUntouched, pristine, dirty, markAsPristine, markAsDirty, updateOn

FormGroup Status

FormGroup.status 和 FormControl.status 有点关联。

比如说,只要有一个 FormControl.status 是 invalid,那 FormGroup.status 就一定是 invalid,哪怕 FormGroup 自己的 validation 是 valid 的。

pending 和 invalid 也是同一个道理,看自身也看子孙 FormControl,只要其中一个 pending / invalid 那就算是 pending / invalid。

而 valid 则表示旗下 FormControl 和自身都是 valid。

另外,markAsPending 和 markAsDirty,markAsTouched 规则一样,只往上 update,不往下 update。

涉及到的 FormGroup 属性和方法:

status, valid, invalid, pending, markAsPending

 

FormGroup with Form Element

FormControl 搭配 input, textarea, select 这些 elements,而 FormGroup 则是搭配 form element。

FormGroupDirective & FormControlName 指令

App 组件

export class AppComponent {
  readonly formGroup = new FormGroup({
    firstName: new FormControl('', { nonNullable: true }),
    lastName: new FormControl('', { nonNullable: true }),
  });

  handleSubmit(event: SubmitEvent) {
    console.log(event);
  }
}

App Template

<form [formGroup]="formGroup" #ngForm="ngForm" (ngSubmit)="handleSubmit($event)">
  <input formControlName="firstName" placeholder="First Name">
  <input formControlName="lastName" placeholder="Last Name">
  <button type="submit">submit</button>
</form>

<pre>{{ formGroup.value | json }}</pre>
<p>submitted: {{ form.submitted }}</p>

里面有好几个知识点:

  1. [formGroup] 就是 FormGroupDirective,#ngForm=”ngForm“ 是它的 exportAs

    (ngSubmit) 是它的 @Output,当 form submit 时会触发,$event 就是 native form SubmitEvent。

  2. formControlName 指令是搭配 FormGroupDirective 使用的。

    顾名思义,formControlName="firstName" 等价于

    <input [formControl]="formGroup.controls.firstName" placeholder="First Name">

    完全一模一样的功能,formControlName 就只是看上去比较整齐而已 (尤其是有嵌套的时候)。

  3. FormGroupDirective.submitted 表示这个 form 已经 submit 过了。

    注:不管 FormGroup 是不是 valid,form 都可以 submit,(ngSubmit) 也一定会触发,我们需要在 handleSubmit 方法里自行处理。

效果

form reset

native form 除了有 submit,还有一个功能叫 reset。

reset 不仅仅把 value set 回 default value,submitted 也会被设回 false。

<form [formGroup]="formGroup" #ngForm="ngForm" (ngSubmit)="handleSubmit($event)"> 
  <input [formControl]="formGroup.controls.firstName" placeholder="First Name" [defaultValue]="formGroup.controls.firstName.defaultValue">
  <input [formControl]="formGroup.controls.lastName" placeholder="Last Name" [defaultValue]="formGroup.controls.lastName.defaultValue">

  <button type="submit">submit</button>
  <button type="reset">reset form</button>
</form>

两个知识点:

  1. reset button

    reset 是 native form 的功能,就如 submit 一样,我们只要放一个 button type 就可以了。

  2. input [defaultValue]

    Angular built-in 的 ControlValueAccessor 没有 cover 到 defaultValue,所以我们必须手动添加 defaultValue 给 input element。

    不然的话,native form 本身的功能就会把 input element value 设置成 input.defaultValue,我们没设置 input.defaultValue 它在 reset 后就会变成 empty string 了。

如果我们不希望它按 native form reset 的逻辑走,那我们可以这样写

<button type="button" (click)="ngForm.resetForm()">reset form</button>

不要使用 type="reset",这样就不会触发 native form 的 reset。

接着我们监听 click 然后手动调用 ngForm.resetForm 方法。

底层原理是一样的,

FormGroupDirective 本来就是监听了 native form reset event 然后调用 resetForm 方法。

Nested FormGroupDirective

<form> element 不能嵌套,但是 FormGroup 和 FormGroupDirective 都可以嵌套。

App 组件

export class AppComponent {
  readonly formGroup = new FormGroup({
    firstName: new FormControl('Derrick', { nonNullable: true }),
    lastName: new FormControl('Yam', { nonNullable: true }),

    address: new FormGroup({
      postalCode: new FormControl('', { nonNullable: true }),
      country: new FormControl('', { nonNullable: true }),
    }),
  });

  handleSubmit(_event: SubmitEvent) {
    if (!this.formGroup.valid) return;
    console.log(this.formGroup.value);
  }
}

address 是子 FormGroup。

App Template

<form [formGroup]="formGroup" #ngForm="ngForm" (ngSubmit)="handleSubmit($event)"> 
  <input formControlName="firstName" placeholder="First Name">
  <input formControlName="lastName" placeholder="Last Name">

  <fieldset formGroupName="address">
    <legend>Address</legend>
    <input formControlName="postalCode" placeholder="Postal Code">
    <input formControlName="country" placeholder="Country">
  </fieldset>
  <button type="submit">submit</button>
</form>

<pre>{{ formGroup.value | json }}</pre>
<p>submitted: {{ ngForm.submitted }}</p>

FormGroupName 指令的用途和 formControlName 一样,区别是前者是 for FormGroup 后者是 for FormControl。

相等于

<fieldset [formGroup]="formGroup.controls.address">

关键在于 FormGroupDirective 不一定要 apply 到 <form> element 身上,任何 element 都可以 apply 的,比如 <fieldset> 等等。

但要注意哦,只有 apply 到 <form> element 才会有 submitted,apply 到其它 element 的话,submitted 永远是 false。

分享一个掉坑的例子:

下面是一个 Angular Material 表单

<form [formGroup]="formGroup">
  <fieldset formGroupName="child">
    <mat-form-field>
      <mat-label>First Name</mat-label>
      <input formControlName="firstName" matInput>
      @if (formGroup.controls.child.controls.firstName.hasError('required')) {
        <mat-error>First Name is required</mat-error>
      }
    </mat-form-field>
  </fieldset>
  <button type="submit">submit</button>
</form>

App 组件

export class AppComponent {
  private readonly formBuilder = inject(FormBuilder);
  readonly formGroup = this.formBuilder.nonNullable.group({
    child: this.formBuilder.nonNullable.group({
      firstName: ['', Validators.required],
    }),
  });
}

当点击 submit 时,它会出现 required error,效果:

现在把 formGroupName 换成 [formGroup]

再去点击 submit

required error 不再出现了。

原因是,MatInput 会 inject FormGroupDirective 然后判断它是否是 submitted 决定显示 error。

但 <fieldset [formGroup]> 是绝对不会 submitted 的,所以不会出现 error。

而 <fieldset formGroupName> 不是 FormGroupDirective,所以 MatInput inject 到的是 <form [formGroup>,它会 submitted。

显然,这是 Angular Material 没有考虑周全,迫使我们必须使用 formGroupName,而这又会导致我们失去 typesafe,是不是很无语...🙄

总结

FormGroup 主要是对一组 FormControl 包装和批量管理,还有与 <form> element 做关联。

这样一个完整的 form solutions 就算成型了。

 

ControlEvent

Angular v18 推出了新功能 ControlEvent。

在 v18 之前,我们用

  1. AbstractControl.valueChanges 监听 value 变更

  2. AbstractControl.statusChanges 监听 status 变更

  3. FormGroupDirective.ngSubmit 监听 form submit

同时,我们无法监听到

  1. FormGroupDirective.resetForm

  2. AbstractControl.markAsDirty

  3. AbstractControl.markAsTouched

在 v18 之后,我们可以监听到上面 6 个事件,并且有了一个统一的接口 -- AbstractControl.events (它是一个 RxJS Subject)。

它的调用方式是这样

const control = new FormControl();
control.events.subscribe(e => {
  if (e instanceof ValueChangeEvent) {
    console.log('value changes');
    console.log('control', e.source);
  }
});

ValueChangeEvent 是一个 class,继承自 ControlEvent (这个是抽象类),源码在 abstract_model.ts

6 个事件对应 6 个 ControlEvent

  1. ValueChangeEvent

    和 AbstractControl.valueChanges 触发时机一样

  2. StatusChangeEvent

    和 AbstractControl.statusChanges 触发时机一样 

  3. FormSubmittedEvent

    和 FormControlDirective.ngSubmit 触发时机一样

  4. FormResetEvent

    当 FormControlDirective.resetForm 时触发 (注:FormGroup.reset 时,不会触发这个哦)

  5. PristineChangeEvent

    当 AbstractControl.markAsDirty 和 AbstractControl.markAsPristine 时触发

  6. TouchedChangeEvent

    当 AbstractControl.markAsTouched 和 AbstractControl.markAsUntouched 时触发

 

FormArray

FormArray 和 FormGroup 基本上是同一个概念,它们最大的特色就是作为一个包装容器。

直接看具体例子体会吧🚀

App 组件

interface AddressFormGroup {
  postalCode: FormControl<string>;
  country: FormControl<string>;
}

export class AppComponent {
  readonly formGroup = new FormGroup({
    firstName: new FormControl('Derrick', { nonNullable: true }),
    lastName: new FormControl('Yam', { nonNullable: true }),

    // 1. addresses 是一个 array
    addresses: new FormArray<FormGroup<AddressFormGroup>>([
      new FormGroup({
        postalCode: new FormControl('', { nonNullable: true }),
        country: new FormControl('', { nonNullable: true }),
      }),
    ]),
  });

  // 2. 添加 new address
  addAddress() {
    this.formGroup.controls.addresses.push(
      new FormGroup({
        postalCode: new FormControl('', { nonNullable: true }),
        country: new FormControl('', { nonNullable: true }),
      }),
    );
  }

  handleSubmit(_event: SubmitEvent) {
    if (!this.formGroup.valid) return;
    console.log(this.formGroup.value);
  }
}

App Template

<form [formGroup]="formGroup" #ngForm="ngForm" (ngSubmit)="handleSubmit($event)"> 
  <input formControlName="firstName" placeholder="First Name">
  <input formControlName="lastName" placeholder="Last Name">

  <ng-container formArrayName="addresses">
    <!-- for loop array, 它里面是一个一个 addressFormGroup -->
    @for (_ of formGroup.controls.addresses.controls; track $index) {
      <!-- 这里 formGroupName 放入的是 index number,因为 FormArray.controls 是 array,所以用的是 index 去拿 -->
      <fieldset [formGroupName]="$index">
        <legend>Address {{ $index + 1 }}</legend>
        <input formControlName="postalCode" placeholder="Postal Code">
        <input formControlName="country" placeholder="Country">
      </fieldset>
    }
  </ng-container>
  <button (click)="addAddress()">add more address</button>
  <button type="submit">submit</button>
</form>

<pre>{{ formGroup.value | json }}</pre>
<p>submitted: {{ ngForm.submitted }}</p>

注:使用 FormArrayName、FormGroupName、FormControlName 指令可以让代码比较少,但是也失去了 typesafe,自己衡量怎么管理比较妥当。

效果

很简单,FormGroup 是 Object 结构,FormArray 是 Array 结构。

FormGroup 的结构是 Record<string, AbstractControl>,AbstractControl 可以是 FormControl,FormGroup,FormArray。

FormArray 的结构是 Array<AbstractControl>。

注:Array 内容可以不同类型哦,比如说 [FormControl, FormGroup, FormGroup, FormArray] 这种混搭也是可以的 (虽然少见)。

只谈结构的话,FormArray 和 FormGroup 极为相似,FormGroup 有的,FormArray 几乎都有,比如:disable, markAllTouched, validator 等等等。

FormGroup 特别的地方是它可以链接 <form> element,FormGroupDirective 有 submited 和 resetForm 概念,这些 FormArrayDirective 自然是没有的。

好,FormArray 就教到这里,其它我就不一一举例了,大家有兴趣的可以照着上面 FormGroup,FormControl 的属性方法,拿 FormArray 来试一遍,体验体验。

 

FormBuilder

FormBuilder 是一个 Root Service Provider。它的作用就是帮助我们 build form。

看例子

const formGroup = new FormGroup({
  firstName: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
  lastName: new FormControl(
    { value: '', disabled: true },
    { nonNullable: true, validators: [Validators.required] },
  ),
  address: new FormGroup({
    country: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
  }),
  addresses: new FormArray(
    [
      new FormGroup({
        country: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
      }),
      new FormGroup({
        country: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
      }),
    ],
    { validators: [Validators.minLength(1)] },
  ),
});

上面是一个复杂的 FormGroup,代码很碎,一堆的 new,一堆的 nonNullable。

好,现在换用 FormBuilder 做相同的 FormGroup 出来

const formBuilder = inject(FormBuilder);
const formGroup = formBuilder.nonNullable.group({
  firstName: ['', [Validators.required]],
  lastName: [{ value: '', disabled: true }, [Validators.required]],
  address: formBuilder.nonNullable.group({
    country: ['', [Validators.required]],
  }),
  addresses: formBuilder.array(
    [
      formBuilder.nonNullable.group({
        country: ['', [Validators.required]],
      }),
      formBuilder.nonNullable.group({
        country: ['', [Validators.required]],
      }),
    ],
    { validators: [Validators.minLength(1)] },
  ),
});

可以看到,代码少了很多,没有一堆的 new,也没有一堆的 nonNullable,只保留有用的信息。

它的用法蛮直观的,我就不解释了,说三个点就好:

  1. FormControl

    lastName: [{ value: '', disabled: true }, [Validators.required]]

    array 代表 FormControl,index 0 是 value,index 1 是 Validator,index 2 是 AsyncValidator。

    假如有 AsyncValidator,没有 Validator,那 index 1 放 undefined or null 就可以了。

  2. FormGroup and FormArray
    address: formBuilder.nonNullable.group()
    addresses: formBuilder.array([
      formBuilder.nonNullable.group()
    ])

    子层是 FormGroup 或 FormArray 的话,继续使用 FormBuilder 来 build。

  3. FormArray > FormControl

    const formArray = formBuilder.nonNullable.array([
      ['', Validators.required],
      ['', Validators.required]
    ]);
    // formArray: FormArray<FormControl<string>>

    FormArray 里面是 FormControl,直接放 array 就可以了,总之 array 代表 FormControl。

    另外,FormArray 里面是 FormGroup/Array 的话,可以不需要 nonNullable (formBuilder.array 就可以了),因为里面的 FormGroup/Array 会负责。

    如果 FormArray 里面是 FormControl 那就要 nonNullable (像这样 formBuilder.nonNullable.array)。

最后讲一下 FormBuilder 的 typesafe,

const formGroup = formBuilder.nonNullable.group({
  firstName: [[Validators.required], 'Derrick'],
});
// formGroup: FormGroup<{ firstName: FormControl<string> }>

build 出来的 FormGroup 类型是正确的,即便我故意把 array 的顺序弄反了。

它在 compile 阶段不会有任何 error,但在 runtime 时会直接报错。

总是,Reactive Forms 的 typesafe 非常不完整就是了。

 

劝退 Reactive Forms

最后,说一说我对 Reactive Forms 的使用心得。

Reactive Forms 乍一看有非常非常多的功能。初学者很容易被它误导,以为它可以很好的解决表单需求。

但正真深入使用它后,必然会发现它有多么的脆弱...

Github Issue – A proposal to improve ReactiveFormsModule

这一个 issue 点出了很多 Reactive Forms 的缺陷,和许多改进的意见。

从 2019 到 2024,都五年过去了,Angular 团队任然无动于衷,把这个 issue 晾在一旁...😓

他们宁愿花时间去搞 SSR,试图和 react next.js 竞争新用户,也不花时间强化 Reactive Forms 善待老用户,真的很无语...😓

还有另一个 Github Issue – Signals for Reactive and Template-based Forms

宣传的时候不是一直吹 Signal 吗?不是一直叫我们用 Signal 吗?

Reactive Forms 都不支持 Signal,你要大家怎样用?

难道你项目不用表单的吗?还是你也认可 Reactive Forms 太烂了,大家早该弃用了?

总之,一言难尽...无力吐槽。

我个人的经验,从好用 > 难用 > 魔改 > 弃用 > 到最后自己做了一套 form solutions。

特此建议各位,不要指望 Angular 团队,当你感受到 Reactive Forms 的不足时,果断放弃它,自己仿照它的思路实现一个类似的方案,然后自己用自己爽,不要再被它束缚了。

 

 

目录

上一篇 Angular 18+ 高级教程 – Animation

下一篇 Angular 18+ 高级教程 – Routing 路由 (原理篇)

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻

 

posted @ 2024-02-12 15:05  兴杰  阅读(597)  评论(0编辑  收藏  举报