起因
最近在重构公司项目的表单部分,因为业务的数据流转都依靠着字段丰富的表格作为基础。因为是重构,所以编码工作也进行地更细心一点,考虑到是否便于以后维护,和是否有足够清晰的架构等。这其中一个一直无法解决的难点就是Angular自身提供的响应式表单(ReactiveForm)并没有提供类型确定的 value 值给用户,在项目的表单逻辑复杂的场景下(比如说有20几中不同的表单字段),常常让人感到很难受。
想象中
现实
大家都知道,使用TypeScript可以在编码时享受像强类型语言那样的类型提示。但是TypeScript中存在 any 类型,这种类型会导致变量的数据类型随意更改,编辑器也无法给出正确的智能提示(IntelliSense),这就很僵硬。
// 类型定义
我主要想解决的问题是 Angular 响应式表单中 FormGroup 对象的 value 属性返回的是 any 类型。比如说我们要创造一个带有两个表单控件的响应式表单,这两个表单控件的数据类型分别为: string Member 。下面通过代码来展示一下理想与现实的差距= =。
准备
// 一个自定义的 Member 类型
interface Member {
name: string;
uid: number;
}
// 创建出来的表单类型
interface IMyForm {
dpt: string;
member: Member;
}
// 一个 FormGroup 对象,有 `dpt` 和 `member` 两个字段
myForm = this.formBuilder.group({
dpt: '前端',
member: { name: '小张', uid: 1 }
});
理想和现实的差距
// 理想
myForm.value; // IMyForm 类型
// 现实
myForm.value; // any 类型
// 理想 get方法中自动提示 'dpt' | 'member' 等应该出现的key值
myForm.get('member').value;
// 现实 (必须知道表单中有 `member` 属性)
myForm.get('member').value;
翻看Angular在github上的issue后得知,官方已经在跟进此feature的优化,可是我迫不及待地想解决这个问题,所以用了点小hack来解决此问题。
以下是官方实现进度的链接
- Proposal - ReactiveForms: add AbstractControl.getChild<T> method
- Reactive forms are not strongly typed
HACK一下
主要的解决思路是利用到了TypeScript中的泛型 ,和索引类型 等特性。
主要实现细节如下
FormGroupTypeSafe
将原生的FormGroup类型扩展成我们期望的带泛型的抽象类
export abstract class FormGroupTypeSafe<T> extends FormGroup {
// 返回一个确定类型的value
value: T;
// 创建一个额外的getSafe方法,使用 keyof 索引查询方法,找到泛型中所可能出现的所有key值
public abstract getSafe(p: keyof T): AbstractControl;
public abstract setControlSafe(p: keyof T, control: AbstractControl): void;
}
FormBuilderTypeSafe
将原生的formBuilder扩展成支持泛型的service
export class FormBuilderTypeSafe extends FormBuilder {
// 重写group方法使之返回的数据带有类型约束
group<T>(controlsConfig: T, extra?: { [key: string]: any;} | null): FormGroupTypeSafe<T> {
// 必须使用父类方法实例化group
let gr = super.group(controlsConfig, extra) as FormGroupTypeSafe<T>;
// 实现自己的抽象方法
if (gr) {
gr.getSafe = (p: keyof T): AbstractControl => {
return gr.get(p as string) as FormGroupTypeSafe<T>;
}
gr.setControlSafe = (p: keyof T, control: AbstractControl): void => {
gr.setControl(p as string, control);
}
}
return gr;
}
}
ok,到此为止所有的前置工作都准备完毕,接下来就是去组件中使用检测效果的时候啦~
// 一个自定义的 Member 类型
interface Member {
name: string;
uid: number;
}
// 创建出来的表单类型
interface IMyForm {
dpt: string;
member: Member;
}
// 组件中的代码
export class HelloComponent implements OnInit {
form: FormGroupTypeSafe<IHeroFormModel>;
constructor (private formHelper: FormBuilderTypeSafe) {}
ngOnInit(): void {
this.form = this.formHelper.group<IHeroFormModel>({
dept: '前端',
member: { name: '小张', uid: 1 }
});
console.log(this.form.value); // value 为 `IMyForm` 类型
this.form.getSafe('member'); // 编辑器自动提示'member'属性啦~
}
}
可以在stackblitz上看到文章中所有代码——demo