Angular 深入浅出三十天:表单与测试 Day30 - 表单原理

Day30

经过前面二十九天的的练习与学习,相信大家应该在表单的实作上都熟悉了不少,只要不是太复杂、太特别的表单应该也都难不倒你们。

今天是本系列文的最後一天,就让我们来好好地深入了解一下 Angular 表单会这麽强大的原因吧!

首先,我想趁大家记忆犹新时,先带大家来看为什麽我们昨天可以用 ControlContainer 来自订一个可以被 Template Driven Forms 或是 Reactive Forms 所使用的 Component 。

ControlContainer

Diagram

上图是我根据 Angular 的 Source Code 找出 ControlContainer 的关系所画的(斜体表抽象类别)。

从图中我们会发现, ControlContainer 其实只是一个抽象类别,并继承了另一个抽象类别 AbstractControlDirective ,而 AbstractControlDirective 这个抽象类别其实也被另一个抽象类别 NgControl 所继承。

NgControl 晚点会提到,此处暂不多做说明。

至於 ControlContainer ,它其实也被 AbstractFormGroupDirectNgFormFormGroupDirectiveFormArrayName 这四个 Directive 所继承;甚至 AbstractFormGroupDirect 还被 FormGroupNameNgModelGroup 这两个 Directive 所继承。

换句话说, Angular 根据 ControlContainer 为基底,做出了以下五个 Directive :

  • FormGroupDirective
  • FormGroupName
  • FormArrayName
  • NgForm
  • NgModelGroup

在这五个 Directive 里,前面三个是为什麽我们在用 Reactive Forms 的方式来开发表单时,可以在 Template 里用 [formGroup][formGroupName][formArrayName] 的方式将元素与 FormGroupFormArray 绑定的原因。

大家应该都还记得我们是怎麽将 FormGroupFormArray 绑定到元素上的吧?!

而後面两个则是为什麽我们在用 Template Driven Forms 的方式来开发表单时,可以在 Template 里在元素上使用 #XXX="ngForm'#XXX="ngModelGroup" 之後,可以拿到 NgFormNgModelGroup 的实体的原因。

虽然本系列文没有特别提到 ngModelGroup 的用法,想知道的朋友可以参考官方的 NgModelGroup API 文件

那为什麽我们可以使用 ControlContainer 来自订元件呢?

其实这正是因为上述五个 Directive 都透过 ControlContainer 这个令牌,把自己注册到 Angular 的 DI 系统里,让想使用它们的类别,可以很方便地透过 Angular 的 DI 系统来找到它们的实体。

像是在昨天的文章里所分享的那样(Reactive Forms 的方式):

export class AddressInfoComponent {
  constructor(private controlContainer: ControlContainer) { }
}

Template Driven Forms 则是透过 viewProvider 的方式,忘记的话请看昨天的文章

NgControl

Diagram

同样地, Angular 也根据 NgControl 为基底,做出了以下三个 Directive :

  • FormControlName
  • FormControlDirective
  • NgModel

在这三个 Directive 里,前面两个是为什麽我们在用 Reactive Forms 的方式来开发表单时,可以在 Template 里用 [formControl][formControlName] 的方式将元素与 FormControl 绑定的原因。

大家应该都还记得我们是怎麽将 FormControl 绑定到元素上的吧?!

而最後一个则是为什麽我们在用 Template Driven Forms 的方式来开发表单时,会在 Template 里在元素上使用 #XXX="ngModel' 之後,可以拿到 NgFormNgModelGroup 的实体的原因。

不过,大家还记不记得我们在第二十八天的时候,是怎麽自订表单元件的吗?

没错!就是 ControlValueAccessor

上述三个 Directive 实作时,也是透过 DI 拿到实作了 ControlValueAccessor 介面的实体,并在初始化的时候透过 ControlValueAccessor 这个介面,搭建 FormControl 与实作了它的实体之间的沟通管道。

想看原始码的朋友可以点我看原始码。

如果想知道 @Self@Optional 装饰器是干嘛用的,可以参考这篇很前显易懂的文章: @Self or @Optional @Host? The visual guide to Angular DI decorators.

但除了 ControlValueAccessor 之外,其实 Angular 还有其他内建的 ValueAccessor:

Diagram

从上图中我们可以发现,内建的 ValueAccessor 基本上都是继承於 BaseControlValueAccessor 这个类别,然後分别被 DefaultValueAccessorBuiltInControlValueAccessor 继承,而只有 DefaultValueAccessor 有实作 ControlValueAccessor 这个介面。

然後 Angular 再基於 BuiltInControlValueAccessor 之上去建立了以下六个 ValueAccessor:

  • NumberValueAccessor
  • RangeValueAccessor
  • RadioControlValueAccessor
  • CheckboxControlValueAccessor
  • SelectControlValueAccessor
  • SelectMultipleControlValueAccessor

而这六个 ValueAccessor 对於我们的表单开发来说是至关重要的存在,没有了它们,我们就没办法在这些元素上绑定我们的表单控制项。

但其实 BaseControlValueAccessorBuiltInControlValueAccessor 本身并没有什麽比较特别的实作或定义,之所以会有 DefaultValueAccessorBuiltInControlValueAccessor 的区别,是为了在机制上,能够做到一个优先权判断的机制。

当我们在使用自订的 ValueAccessor 时候, DefaultValueAccessor 或是上述六个 ValueAccessor 其实都有可能与我们自订的 ValueAccessor 同时存在。

因此,在将我们的表单控制项与元素绑定的时候, Angular 会根据以下的优先权来抓取对应的 ValueAccessor :

  1. CustomValueAccessor
  2. BuiltInValueAccessor
  3. DefaultValueAccessor

如此一来,只要我们没有自订 ValueAccessor ,预设就是会使用内建的 ValueAccessor 来搭建表单控制项与元素之间的沟通桥梁。

我觉得 Angular 的开发团队真的很聪明!

同步与非同步

除了上述的东西之外,其实还有一个比较特别的点,就是关於同步更新与非同步更新的问题。

Day16 - Template Driven Forms vs Reactive Forms 的小结里,我分享了一个表格,表格里提到了 Template Driven Forms 的可预测性是非同步的。

而这件事情其实可以在 NgModel原始码第 222 行 看出一点端倪。

原始码第 222 行这行是指 NgModel 在初始化的时候,会去设置表单控制项。

而在原始码第 268 行中可以看到,它会判断该 NgModel 是不是 _isStandalone ,也就是它是不是单独存在,还是有被 <form></form> 包住。

如果是单独存在, NgModel 的行为其实会跟 FormControlDirectiveFormControlName 一样,因为他们会用一样的方法设置表单控制项。

但如果不是单独存在,它会用 NgFormaddControl 来设置表单控制项,这时,它就会是非同步的,因为他必须等到 resolvedPromise 发出 resolver 事件的时候,才会进行设置。

addControl 的实作在原始码的第 187 行到 196 行

resolvedPromise 的定义则是在原始码的第 27 行

我其实不太懂 Angular 为什麽要在 NgModel<form></form> 包起来的时候这样子做,因为 resolvedPromise 其实也没什麽特别的地方,但它们就让 NgFormaddControlremoveControladdFormGroupremoveFormGroup 这四个方法要变成非同步的方式处理。

如果大家有兴趣,或许找个时间研究一下,说不定,你就帮官方解决了一个问题呢!

本日小结

今天主要是帮本系列文做个结尾,希望能让大家透过我的分享,更熟悉、更了解 Angular 一点。

也因为分享的关系,其实在撰写本系列文的同时,我也得到了许多。

以前尚不熟悉的更熟悉了;以前不知道的知道了。

这或许就是所谓的「施比受更有福」吧?!

未来,还会不会再写铁人赛还不晓得(目前是不想再写了,哈哈!)。

但是,写铁人赛真的是一个非常好的学习机会。

透过撰写文章,来疏理自己所学,仔细咀嚼後再回馈给社群、回馈给社会。

如果你还没写过铁人赛,我衷心推荐你这一辈子一定至少要写一次铁人赛。

最後,我想感谢订阅我的文章、阅读我的文章、喜欢我的文章的你们,谢谢你们不嫌弃,也谢谢你们愿意让我能够帮到你们。

我也想感谢我的家人们,写铁人赛的这段期间,真的是非常地疏於陪伴,谢谢他们的支持(虽然他们看不到)。

感谢大家的收看,我们有缘再见! :)

其他资源


<<:  压力平衡

>>:  [Day30] 第三十 - 总结技能交换系统(整合Laravel以及Express的Microservices)

VoK 整合式登入 - day10

登入功能不外乎输入帐密、验证、赋予角色功能权限。Vaadin-on-Kotlin 提供了 VoK-S...

IOS Swift 什麽是Closure?不能只会func吗?

前言: 又到了发文的时间了,最近我的屁股一直长疔子真的好痛,有人知道要怎麽治标吗,一直跪着打程序也不...

Day 30:结束後的下一步

回顾 & 总结 今天是最後一天了,来回顾一下这些日子分享及学习的内容,虽然是写LeetCod...

Day4-丛集是在集这个 Node介绍

在上一章我们提到了丛集是所谓的节点管理者,其中控制平面控制着k8s内所有的节点与资源。 那麽,所谓的...

这位秘书,懂喝喔

在还没有担任秘书之前,心底总是对高管秘书有个憧憬:一人之下万人之上,只要打扮的漂漂亮亮,负责帮董事长...