Angular反应式类型化表单--不仅仅是一个梦想
本文将重点介绍Angular 14和自Ivy以来最重要的更新,包括类型化的反应式表单和独立组件以及小的改进。
当新的Angular 14版本发布时,我对两个新功能相当满意,我想与你分享。第一个是类型化的反应式表单,第二个是独立组件。
在第一个版本发布6年后,经过几个月的讨论 和反馈,Angular仓库中最需要的功能和被上调的问题 现在在Angular v14中得到了解决!
Angular 14于6月2日发布,是自Ivy以来最重要的一次更新。它包括两个期待已久的功能:类型化的反应式表单和独立组件,以及一些小的改进。
在这篇文章中,我们将重点讨论类型化的反应式表单。与Angular v14之前一样,Reactive Forms在许多类中不包括类型定义,TypeScript在编译过程中不会捕捉到像下面这个例子中的错误。
TypeScript
const loginForm = new FormGroup({
email: new FormControl(''),
password: new FormControl(''),
});
console.log(login.value.username);
在Angular 14中,FormGroup、formControl和相关的类包含了类型定义,使TypeScript能够捕获许多常见的错误。
迁移到新的Typed Reactive Forms不是自动的。
已经存在的包含FormControls、FormGroups等的代码在升级过程中会被前缀为Untyped。值得一提的是,如果开发者想利用新的类型化反应式的优势,必须手动删除Untyped前缀,并修复任何可能出现的错误。
关于这个迁移的更多细节可以在官方的Typed Reactive Forms文档中找到。
一个未打字的反应式表单的逐步迁移例子
假设我们有以下的注册表单:
TypeScript
export class RegisterComponent {
registerForm: FormGroup;
constructor() {
this.registerForm = new FormGroup({
login: new FormControl(null, Validators.required),
passwordGroup: new FormGroup({
password: new FormControl('', Validators.required),
confirm: new FormControl('', Validators.required)
}),
rememberMe: new FormControl(false, Validators.required)
});
}
}
Angular还提供了一个自动迁移来加速这一过程。这个迁移将在我们作为开发者运行以下命令时运行。
ng update @angular/core 或者在需求时,如果我们已经通过运行下一个命令手动更新了你的项目。 ng update @angular/core --migrate-only=migration-v14-typed-forms .
在我们的例子中,如果我们使用自动迁移,我们最终会得到上述改变后的代码:
TypeScript
export class RegisterComponent {
registerForm: UntypedFormGroup;
constructor() {
this.registerForm = new UntypedFormGroup({
login: new UntypedFormControl(null, Validators.required),
passwordGroup: new UntypedFormGroup({
password: new UntypedFormControl('', Validators.required),
confirm: new UntypedFormControl('', Validators.required)
}),
rememberMe: new UntypedFormControl(false, Validators.required)
});
}
}
现在的下一步是删除所有的Untyped* 用法,并适当调整我们的表单。
每个UntypedFormControl必须被转换为FormControl,其中T是表单控件的值的类型。大多数情况下,TypeScript可以根据给FormControl的初始值来推断这些信息。
例如,passwordGroup可以很容易地被转换:
TypeScript
passwordGroup: new FormGroup({
password: new FormControl('', Validators.required), // inferred as `FormControl<string | null>`
confirm: new FormControl('', Validators.required) // inferred as `FormControl<string | null>`
}),
注意,推断的类型是字符串|空,而不是字符串。这是因为在没有指定重置值的情况下在控件上调用.reset()会将值重置为null。这种行为从Angular开始就存在了,所以推断的类型反映了它。我们将在下面的一个例子中再来讨论这个可能的空值,因为它可能很烦人(但总是有办法的)。
现在我们来看看字段registerForm。与FormControl不同,FormGroup所期望的通用类型不是它的值的类型,而是它在表单控件方面的结构描述:
TypeScript
registerForm: FormGroup<{
login: FormControl<string | null>;
passwordGroup: FormGroup<{
password: FormControl<string | null>;
confirm: FormControl<string | null>;
}>;
rememberMe: FormControl<boolean | null>;
}>;
constructor() {
this.registerForm = new FormGroup({
login: new FormControl<string | null>(null, Validators.required),
passwordGroup: new FormGroup({
password: new FormControl('', Validators.required),
confirm: new FormControl('', Validators.required)
}),
rememberMe: new FormControl<boolean | null>(false, Validators.required)
});
}
表单中的空值性
正如我们在上面看到的,控件的类型是字符串| null和布尔值| null,而不是我们所期望的字符串和布尔值。这是因为如果我们在一个字段上调用.reset()方法,会将其值重置为null。除非我们给一个值来重置,例如.reset(''),但由于TypeScript不知道你是否要调用.reset()以及如何调用,推断出的类型是nullable。
我们可以通过传递nonNullable 选项来调整行为(它取代了Angular v13.2 initialValueIsDefault中引入的新选项*)。有了这个选项,如果我们想的话,就可以摆脱空值了
一方面,如果你的应用程序使用严格的NullChecks,这是非常方便的,但另一方面,这是相当繁琐的,因为我们目前必须在每个字段上设置这个选项(我希望这在未来会有所改变)。
TypeScript
registerForm = new FormGroup({
login: new FormControl<string>('', { validators: Validators.required, nonNullable: true }),
passwordGroup: new FormGroup({
password: new FormControl('', { validators: Validators.required, nonNullable: true }),
confirm: new FormControl('', { validators: Validators.required, nonNullable: true })
}),
rememberMe: new FormControl<boolean>(false, { validators: Validators.required, nonNullable: true })
}); // incredibly verbose version, that yields non-nullable types
另一种实现相同结果的方法是使用NonNullableFormBuilder。Angular v14引入了一个名为nonNullable的新属性,它返回一个NonNullableFormBuilder,其中包含了通常已知的控件、组、数组等方法来构建非空的控件。
创建一个非空值表单组的例子:
TypeScript
constructor(private fb: NonNullableFormBuilder) {}
registerForm = this.fb.group({
login: ['', Validators.required]
});
那么,这种迁移是否值得?我们通过类型化的反应式表单获得了什么?
在Angular v14之前,现有的表单API在TypeScript中的表现非常好,因为每个表单控件的值都是类型化的。因此,我们可以很容易地编写类似于this.registerForm.value.something, 的东西,并且应用程序会成功编译。
现在情况不同了:新的表单API根据表单控件的类型正确地键入值。在我上面的例子中(使用nonNullable),this.registerForm.value的类型是:
TypeScript
// this.registerForm.value
{
login?: string;
passwordGroup?: {
password?: string;
confirm?: string;
};
rememberMe?: boolean;
}
我们可以在表单值的类型中发现一些问题。它是什么意思?
众所周知,在Angular中,我们可以禁用表单的任何部分;如果是这样,Angular会自动将禁用的控件的值从表单的值中移除。
TypeScript
this.registerForm.get('passwordGroup').disable();
console.log(this.registerForm.value); // logs '{ login: null, rememberMe: false }'
上面的结果有点奇怪,但它充分解释了为什么字段被标记为可选,如果它们被禁用。所以,它们不再是this.registerForm.value的一部分了。TypeScript将这种特性称为Partial value。
也有一种方法可以通过在表单上运行.getRawValue()函数来获得孔对象,即使是禁用的字段。
TypeScript
{
login: string;
passwordGroup: {
password: string;
confirm: string;
};
rememberMe: boolean;
} // this.registerForm.getRawValue()
事件更严格的类型.get()函数
get(key)方法也是更严格的类型化。这是一个好消息,因为我们以前可以用一个不存在的键来调用它,而编译器不会看到这个问题。
多亏了TypeScript的一些核心魔法,键现在被检查出来了,而且返回的控件也被正确地类型化了。它也可以用数组语法来处理键,如下所示:
TypeScript
his.registerForm.get('login') // AbstractControl<string> | null
this.registerForm.get('passwordGroup.password') // AbstractControl<string> | null
//Array Syntax
this.registerForm.get(['passwordGroup', '.password'] as const) // AbstractControl<string> | null
也能与嵌套的表单数组和组一起工作,如果我们使用一个不存在的键,我们终于可以得到一个错误。
TypeScript
this.registerForm.get('hobbies.0.name') // AbstractControl<string> | null
//Non existing key
this.registerForm.get('logon' /* typo */)!.setValue('cedric'); // does not compile
正如你所看到的,get()返回一个潜在的空值:这是因为你不能保证该控件在运行时存在,所以你必须检查它的存在或使用!像上面那样。
注意,你在模板中用于formControlName、formGroupName和formArrayName的键没有被检查,所以你的模板中仍然可能有未被发现的问题。
新鲜的东西:FormRecord
FormRecord是一个新的表单实体,已经被添加到API中。FormRecord类似于FormGroup,但控件必须都是相同的类型。如果你把表单组作为一个地图,动态地添加和删除控件,这将会有所帮助。在这种情况下,正确地输入 FormGroup 并不容易,这就是 FormRecord 可以帮助的地方。
当你想表示一个复选框的列表时,它可以很方便,例如,你的用户可以在其中添加或删除选项。例如,我们的用户在注册时可以添加和删除他们理解(或不理解)的语言。
TypeScript
languages: new FormRecord({
english: new FormControl(true, { nonNullable: true }),
french: new FormControl(false, { nonNullable: true })
});
// later
this.registerForm.get('languages').addControl('spanish', new FormControl(false, { nonNullable: true }));
如果我们试图添加一个不同类型的控件,TS会抛出一个编译错误!
但由于键可以是任何字符串,在removeControl(key)或setControl(key)中没有对键进行类型检查。而如果你使用的是 FormGroup,有明确定义的键,你就可以对这些方法进行类型检查:setControl 只允许一个已知的键,而 removeControl 只允许一个标记为可选的键(在其类型定义中带有 ?)
如果我们有一个FormGroup,我们想在上面动态地添加和删除控制,我们可能要寻找新的FormRecord类型。
总结
我很高兴在Angular中看到这种新形式的API!到目前为止,这是近年来对开发者来说最大的变化之一。Ivy很大,但不需要我们在我们的应用程序中做大量的改变。类型化表单则是另一回事:迁移很可能会影响到我们应用程序中的几十个、几百个、甚至几千个文件
Angular中的TypeScript支持一直都很出色,但在表单方面有一个很大的盲点:现在已经不是这样了
所以,是的。这是完全值得的!!
直到下一次。
编码愉快。