iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0
Modern Web

用 Angular Material 開發應用程式系列 第 26

Day 26 - 自定義表單欄位

  • 分享至 

  • xImage
  •  

在開發應用程式時,常會因應需求開發一表單元件。然而,,除非在此元件就已經內含使用 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 Control

若要讓我們定義的元件可以放在 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 介面時,我們需要定義下面需要的屬性。

id

此屬性定義 Form Field 所使用的唯一性編號資訊

private static nextId = 0;

@HostBinding()
id = `task-type-select-${TaskTypeSelectComponent.nextId++}`;

stateChanges

在 Form Field 內表單有更新時,需要利用此屬性來發送變動資訊。需要記得要在元件銷毀一併把此屬性結束。

readonly stateChanges = new Subject<void>();

ngOnDestroy(): void {
  this.stateChanges.complete();
}

placeholder、required、disabled

用來定義描述文字、必填與停用等資訊,這些屬性在有變更時,就需要觸發 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);
  });
  ...
}

ngControl

在提供者設定上,我們用 MatFormFieldControl 取代 NG_VALUE_ACCESSOR 後,需要把 ngControl 屬性中的 valueAccessor 設定成此元件實體。

ngControl = inject(NgControl, { optional: true, self: true });

constructor() {
  if (this.ngControl !== null) {
    this.ngControl.valueAccessor = this;
  }
  ...
}

其他屬性

最後主要使用 focusedemptyerrorState 等屬性來定義 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>

上一篇
Day 25 - 切換應用程式樣式
下一篇
Day 27 - CDK 初探
系列文
用 Angular Material 開發應用程式30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言