在開發應用程式時,常會因應需求開發一表單元件。然而,,除非在此元件就已經內含使用 Form Field 外,是無法在應用端中直接把此表單元件放在 Material 的 Form Field 內。
在撰寫表單元件時,會去實作 ControlValueAccessor
介面,並設定 NG_VALUE_ACCESSOR
提供者。
type TaskType = { first: string, second: string };
@Component({
selector: 'app-task-type-select',
...
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TaskTypeFormComponent), multi: true }],
})
export class TaskTypeFormComponent implements OnInit, ControlValueAccessor {
protected readonly form = new FormGroup({
first: new FormControl<string | null>(null),
second: new FormControl<string | null>({ value: null, disabled: true }),
});
onChange!: (_: TaskType) => void;
onTouched!: () => void;
writeValue(data: TaskType): void {
if (data) {
this.form.patchValue(data);
} else {
this.form.reset();
}
}
registerOnChange(fn: () => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
}
若要讓我們定義的元件可以放在 Form Field 裡,就需要去實作 MatFormFieldControl<TaskType>
介面,其泛型型別就是要回傳的型別。其次,在提供者方面,針對 MatFormFieldControl
取代 NG_VALUE_ACCESSOR
來進行設定。
@Component({
...
providers: [{ provide: MatFormFieldControl, useExisting: forwardRef(() => TaskTypeFormComponent), multi: true }],
})
export class TaskTypeFormComponent implements OnInit, MatFormFieldControl<TaskType>, ControlValueAccessor {
...
}
在實作 MatFormFieldControl
介面時,我們需要定義下面需要的屬性。
此屬性定義 Form Field 所使用的唯一性編號資訊
private static nextId = 0;
@HostBinding()
id = `task-type-select-${TaskTypeSelectComponent.nextId++}`;
在 Form Field 內表單有更新時,需要利用此屬性來發送變動資訊。需要記得要在元件銷毀一併把此屬性結束。
readonly stateChanges = new Subject<void>();
ngOnDestroy(): void {
this.stateChanges.complete();
}
用來定義描述文字、必填與停用等資訊,這些屬性在有變更時,就需要觸發 stateChanges.next()
。
readonly _placeholder = input<string>('', { alias: 'placeholder' });
get placeholder(): string {
return this._placeholder();
}
readonly _required = input<boolean, string | boolean>(false, {
alias: 'required',
transform: booleanAttribute,
});
get required(): boolean {
return this._required();
}
readonly _disabled = input<boolean, boolean | string>(false, {
alias: 'disabled',
transform: booleanAttribute,
});
get disabled() {
return this._disabled();
}
constructor() {
...
effect(() => {
this._placeholder();
this._required();
this._disabled();
untracked(() => this.stateChanges.next());
});
effect(() => {
const fn = this._required()
? this.formControl.addValidators(Validators.required)
: this.formControl.removeValidators(Validators.required);
untracked(() => fn);
});
effect(() => {
const fn = this._disabled()
? this.formControl.disable()
: this.formControl.enable();
untracked(() => fn);
});
...
}
在提供者設定上,我們用 MatFormFieldControl
取代 NG_VALUE_ACCESSOR
後,需要把 ngControl
屬性中的 valueAccessor
設定成此元件實體。
ngControl = inject(NgControl, { optional: true, self: true });
constructor() {
if (this.ngControl !== null) {
this.ngControl.valueAccessor = this;
}
...
}
最後主要使用 focused
、empty
、errorState
等屬性來定義 Form Field 表單狀態。
private readonly _focused = signal(false);
get focused(): boolean {
return this._focused();
}
get empty(): boolean {
return (
this.formControl.value === undefined || this.formControl.value === null
);
}
get errorState(): boolean {
return this.formControl.invalid;
}
@HostBinding('class.floating')
get shouldLabelFloat(): boolean {
return this.focused || !this.empty;
}
private readonly fm = inject(FocusMonitor);
private readonly elRef = inject<ElementRef<HTMLElement>>(ElementRef);
constructor() {
...
this.fm.monitor(this.elRef.nativeElement, true).subscribe((origin) => {
this._focused.set(!!origin);
this.stateChanges.next();
});
}
如此一次,我們就可以把自訂的表單元件使用在 FormField 內。
<mat-form-field>
<app-task-type-select [formControl]="formControl" />
</mat-form-field>