实际使用form的时候,最外层的form的某个表单元素可能是个组合的,这种情况如果是可多场景复用的,最好封装一个表单元素。本文以组合复选框为例来说明下自定义表单元素的过程
实现效果
展示效果
html
<form nz-form [formGroup]="form">
<nz-form-item nz-row>
<nz-form-label style="width: 120px;" nzRequired>{{ label }}</nz-form-label>
<nz-form-control [nzSpan]="18" [nzErrorTip]="'至少选择一个类型'">
<app-checkbox
[data]="checkList"
formControlName="type"
></app-checkbox>
</nz-form-control>
</nz-form-item>
<div nz-row>
<div nz-col [nzSpan]="18" style="margin-left: 120px;">
<button nz-button nzType="primary" (click)="onVerify()">校验</button>
</div>
</div>
</form>
<div>
{{formValueStr}}
</div>
ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { FormUtils } from '../../utils/form-utils';
import { CheckItem } from '../checkbox/checkbox.component';
@Component({
selector: 'app-form-test',
templateUrl: './form-test.component.html',
styleUrls: ['./form-test.component.scss']
})
export class FormTestComponent implements OnInit {
form!: FormGroup;
label = '支出类型';
checkList: CheckItem[] = [
{ label: '交通', value: '1' },
{ label: '日常', value: '2' },
{ label: '旅游', value: '3' },
{ label: '理财', value: '4' }
];
get formValueStr(): string{
return JSON.stringify(this.form.value);
}
constructor(
private fb: FormBuilder,
) { }
ngOnInit(): void {
this.form = this.fb.group({
type: [
['1'],
(c: FormControl) => {
console.log('value', c.value);
return c.value && c.value.length > 0 ? null : { required: '123' };
}
]
});
}
onVerify(): void {
FormUtils.verifyState(this.form);
}
}
用到的API
ControlValueAccessor - 定义一个接口,该接口充当 Angular 表单 API 和 DOM 中的原生元素之间的桥梁。
interface ControlValueAccessor {
// 当请求从模型到视图的编程更改时,表单 API 会调用此方法以写入视图。
writeValue(obj: any): void
// 注册一个回调函数,该控件的值在 UI 中更改时将调用该回调函数。
registerOnChange(fn: any): void
// 注册一个在初始化时由表单 API 调用的回调函数,以在失焦时更新表单模型。
registerOnTouched(fn: any): void
// 当控件状态更改为 “DISABLED” 或从 “DISABLED” 更改时,表单 API 要调用的函数。
// 根据其状态,它会启用或禁用适当的 DOM 元素。
setDisabledState(isDisabled: boolean)?: void
}
checkList组件
html
<div [formGroup]="form">
<label
*ngFor="let item of data;let i = index;"
nz-checkbox
[nzValue]="item.value"
[formControlName]="i"
>{{ item.label }}</label>
</div>
ts
import { ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
export interface CheckItem {
label: string;
value: string;
checked?: boolean;
}
@Component({
selector: 'app-checkbox',
templateUrl: './checkbox.component.html',
styleUrls: ['./checkbox.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckboxComponent),
multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckboxComponent implements ControlValueAccessor, OnInit {
@Input() data: CheckItem[] = [];
form: FormGroup = new FormGroup({});
private _onChange = (_: any) => { };
constructor(
private fb: FormBuilder,
) { }
reGroupValues(values: CheckItem[]) {
let data: any[] = this.data.map((item, index) => {
return {
...item,
checked: values[index],
};
}).filter(item => item.checked).map(item => item.value);
return data;
}
resetCpValue(values: any) {
const cpValue: any = {};
this.data.forEach((item, index) => {
cpValue[index] = values.indexOf(item.value) > -1 ? true : false;
});
return cpValue;
}
ngOnInit() {
const options: any = {};
this.data.forEach((item, index) => {
options[index] = [!item.checked ? false : true];
});
this.form = this.fb.group(options);
// 监听控件的每一个元素change事件,然后调用 _onChange
this.form.valueChanges.subscribe(values => {
this._onChange(this.reGroupValues(values));
});
}
// 向控件form中写入values-form通过setValue,patchValue给form赋值的时候
writeValue(values: any) {
if (!values) {
return;
}
const cpValue: any = {};
this.data.forEach((item, index) => {
cpValue[index] = values.indexOf(item.value) > -1 ? true : false;
});
this.form.setValue(cpValue);
}
// 注册空间change事件-用于外部form对该控件的监听
registerOnChange(fn: any) {
this._onChange = fn;
}
registerOnTouched(fn: any) {
// 此组件是否触碰可以不设置,无实际意思
// 如果是封装的输入元素可以设置此操作,并在组件中某些输入blur事件中调用 _onTouched 来进行触发
// eg this._onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// 因为当前组件为check的组合,每个元素都可以进行disable设置,故此可以调用form的方法
if (isDisabled) {
this.form.disable();
} else {
this.form.enable();
}
}
}
至此可以实现本文开头截图中的效果
FormUtils工具
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
export enum MarkType {
DIRTY = 'dirty',
PRISTINE = 'pristine',
}
export class FormUtils {
static markAs(control: AbstractControl, markType: MarkType) {
if (control instanceof FormControl && !control.dirty) {
control.markAsDirty();
control.updateValueAndValidity();
} else if (control instanceof FormArray) {
control.controls.forEach(item => {
this.markAs(item, markType);
});
} else if (control instanceof FormGroup) {
Object.keys(control.controls).forEach(key => {
this.markAs(control.controls[key], markType);
});
}
}
static verifyState(control: AbstractControl) {
FormUtils.markAs(control, MarkType.DIRTY);
}
static resetState(control: AbstractControl) {
FormUtils.markAs(control, MarkType.PRISTINE);
}
}