Angular反应式类型化表单的详细指南

421 阅读8分钟

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支持一直都很出色,但在表单方面有一个很大的盲点:现在已经不是这样了

所以,是的。这是完全值得的!!

直到下一次。

编码愉快。