在 Angular v14 中最重要的 2 個功能,除了 Single Component 之外,就是 Typeform 了,而 Typeform 也就是在建立 Form 功能時,終於可以套用型別,方便開發及除錯,是期待很久的功能。 查詢官方文件可以發現,範列都是使用從建立 'FormGroup',再建立 'FormControl' 的方式。
本人還是比較偏好用 FormBuilder
來建立 FormGroup,所以分享一下用 FromBuilder
建立的心得。
使用方式
import { Component, OnInit } from '@angular/core';
import { UntypedFormGroup, NgForm, Validators, UntypedFormBuilder } from '@angular/forms';
首先使用 Angular CLI 來將之前建立的小專案升級至 V.14,如上,升級後原本的程式會自動轉換成 UntypedFormGroup
、 UntypedFormBuilder
。
import { FormGroup, NgForm, Validators, FormBuilder, FormControl } from '@angular/forms';
interface MemberForm {
id?: FormControl<string>;
name: FormControl<string>;
email: FormControl<string>;
mobile: FormControl<string>;
birthday: FormControl<string>;
account: FormControl<string>;
password: FormControl<string>;
}
接著替換成 FormGroup
、 FormBuilder
,並且建立使用於 FormBuilder
上的 interface。
group: FormGroup<MemberForm>;
接著在 FormGroup
中使用已建立的 MemberForm
。
constructor(
private fb: FormBuilder,
) {}
ngOnInit() {
this.group = this.fb.nonNullable.group({
name: new FormControl('', Validators.required),
email: new FormControl('', Validators.required),
mobile: new FormControl('', Validators.required),
birthday: new FormControl('', Validators.required),
account: new FormControl('', Validators.required),
password: new FormControl('', Validators.required),
});
}
之後像之前 v.13 的一樣寫法,使用 'group' 方式來建立 FormGroup
,因為每個屬性都是必填,所以設定 Validators.required
。
class Member extends BaseModel {
name = '';
email = '';
mobile = '';
birthday = '';
password = '';
}
interface MemberForm {
id?: FormControl<string>;
name: FormControl<string>;
email: FormControl<string>;
mobile: FormControl<string>;
birthday: FormControl<string>;
account: FormControl<string>;
password: FormControl<string>;
}
一般說來,每個 Form 都會有相對應的 Model,也就是類型,比如上面程式碼中的 Member
。而 FormGroup
也會建立都是相同屬性的類型,比如上面程式碼的 MemberForm
,如此就會重覆建立。
import { FormArray, FormControl, FormGroup } from '@angular/forms';
export type Unpacked<T> = T extends Array<infer U> ? U : T;
export type ToForm<OriginalType> = {
[key in keyof OriginalType]: OriginalType[key] extends Array<any>
? FormArray<
Unpacked<OriginalType[key]> extends object
? FormGroup<ToForm<Unpacked<OriginalType[key]>>>
: FormControl<Unpacked<OriginalType[key]> | null>
>
: OriginalType[key] extends object
? FormGroup<ToForm<OriginalType[key]>>
: FormControl<OriginalType[key] | null>;
};
可以參考文章搶先體驗強型別表單(Strict Typed Reactive Forms),就可以方便轉換。
import { ToForm } from '../utils/toForm';
group: FormGroup<ToForm<Member>>;
只需改成 FormGroup<ToForm<Member>>
即可。
class-validator
因為驗證是每個 Form 都是必須會需要做的行為,所以會使用 class-validator
來共同驗證,之前有接觸過 class-validator
這個套件,是用 decorator 的方式,在物件的屬性上設定所要驗證的格式,之前都是在 Node.js 上來使用,不過 Angular 上使用的話,需要整合一下。
import {
IsNotEmpty,
IsEmail,
IsMobilePhone,
ValidationOptions,
Matches,
MinLength,
MaxLength,
} from 'class-validator';
import { plainToClassFromExist } from 'class-transformer';
import { BaseModel } from './baseModel';
const options: ValidationOptions = { message: '填寫正式資料' };
export class Member extends BaseModel {
@IsNotEmpty({
message: '姓名需填寫',
})
name = '';
@IsNotEmpty({
message: 'Email需填寫',
})
@IsEmail()
email = '';
@IsNotEmpty({
message: '手機需填寫',
})
@IsMobilePhone(
'zh-TW',
{
strictMode: false,
},
{
message: '手機需填寫',
}
)
mobile = '';
birthday: string;
@IsNotEmpty(options)
@MinLength(6, options)
@MaxLength(12, options)
@Matches(/[a-zA-Z\d]/g, options)
account = '';
@IsNotEmpty(options)
@MinLength(6, options)
@MaxLength(12, options)
@Matches(/[a-zA-Z\d]/g, options)
password = '';
constructor(data?: any) {
super();
plainToClassFromExist(this, data);
}
}
class-validator
預設已經定義一些常用的驗證方式,例如是否必填、Email 或是手機驗證,直接套用在類別上的屬性即可,可以參考文件。
import { ValidationErrors, ValidatorFn } from '@angular/forms';
import { validateSync } from 'class-validator';
export function utilValidator<T extends {}>(model: T, prop: string): ValidatorFn {
return (control): ValidationErrors | null => {
let invalid = false;
model[prop] = control.value;
const errors = validateSync(model, {
skipMissingProperties: true,
});
if (errors && errors.length) {
const propError = errors.filter((e) => e.property == prop);
if (propError.length > 0) {
const msg = propError.map(({ constraints }) => Object.values(constraints).join(', '));
invalid = true;
return { hasError: invalid && (control.dirty || control.touched), msg };
}
}
return null;
};
}
為了要將 'class-validator' 和 'FormGroup' 、'FormControl' 整合,本人建立了共同方法,首先使用 validateSync
來驗證 model,接著取得特定屬性的錯誤訊息。
<div class="text-danger" *ngIf="group.controls.name.errors?.hasError">
{{ group.controls.name.errors?.msg }}
</div>
而如果有錯誤的話,固定回傳格式為 { hasError: true, msg }
,所以頁面上只需判斷 hasError === true
即可。
並且因為是強型別的關系,在 html 上也有程式碼提示的幫助。
結論
一直以來,TypeForm 就是榜上有名的希望實作的功能,對於開發及除錯的幫助很大,我將以前的小專案改為 TypeForm 的方式嘗試看看,這是小小的心得分享。