Angularでカスタムフォーム(CustomFormControls)を作る(1/2)


Angulerで標準のコンポーネントをカスタマイズして自前のフォームを作るやり方です。

AngularConnect2017の動画を公式のドキュメントとして参考にしています。
https://youtu.be/ZNTsdaZiqP8?t=9896

やりたいこと

上記の動画にあわせて、入力必須のテキストフィールドコンポーネントを作って行きます。

  • 標準のinputなどのフォームと同様の使い方ができること
  • 入力必須のバリデーションが行われること
  • 未入力の場合にエラーメッセージを表示されること

コンポーネントのひな型

まずはこれから実装していくコンポーネントの最初の状態です。

requiredText.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-required-text',
  templateUrl: './requiredText.component.html',
  styleUrls: [ './requiredText.component.css' ],
})
export class RequiredTextComponent implements ControlValueAccessor {  
}
requiredText.component.html
<div>
  <input type="text">
</div>

ControlValueAccessor

カスタムコンポーネントを作るにあたり、下の例のようにngModelやFormControlのプロパティを標準のフォームのように使えるようにします。

<custom-input [(ngModel)]="name"></custom-input>

ngModelやFormControlのプロパティからデータをバインドさせるようにするには、「ControlValueAccessor」をimplmentさせます。これは標準のinputやtextareaなどすべてのフォームがimplementしているもので、新しくフォームを作る際にもimplementを行います。

requiredText.component.ts
export class RequiredTextComponent implements ControlValueAccessor 

Interface Methodの実装

ControlValueAccessorをimplemntを行ったので、インターフェースで定義されている以下のメソッドを実装します。

  • writeValue(obj: any): void {}
  • registerOnChange(fn: any): void {}
  • registerOnTouched(fn: any): void {}
  • (Optional) setDisabledState(isDisabled: boolean){}

writeValue

モデルの値をViewのフォームに設定します。
まず、Viewのフォームを取得するためにDOM側に#でIDを設定します。
モデル側ではViewChildでID参照して要素を取得できる状態にします。

requiredText.component.html
<input type="text" #input>
requiredText.component.ts
@ViewChild('input') private input: ElementRef;

writeValue(obj: any): void {
  this.input.nativeElement.value = obj;
}

registerOnChange

Viewでの変更時のコールバックを登録します。
onChangeのメソッドを定義し、View側で変更が起きたらonChangeを呼ぶようにします。
registerOnChangeでonChangeのからコールバックを呼び出すようにします。

requiredText.component.html
<input type="text" (input)="onChange($event.target.value)" #input>
requiredText.component.ts
onChange: (obj: any) => void;

registerOnChange(fn: any): void {
  this.onChange = fn;
}

registerOnTouched

ViewでのTouch(blur)イベントのコールバックを登録します。
registerOnChangeと同様に、onTouchedメソッドを定義しregisterOnTouched内でコールバックを呼び出すようにします。

requiredText.component.html
<input type="text" #input 
 (input)="onChange($event.target.value)" 
 (blur)="onTouched()">

requiredText.component.ts
onTouched: (obj: any) => void;

registerOnTouched(fn: any): void {
  this.onTouched = fn;
}

setDisabledState

フォームの活性/非活性をViewに設定します。

requiredText.component.html
<input type="text" #input 
 (input)="onChange($event.target.value)" 
 (blur)="onTouched()" 
 [disabled]="disabled">
requiredText.component.ts
disabled: boolean

setDisabledState(isDisabled: boolean){
 this.disabled = isDisabled;
}

Provider

さらにControlValueAccessorをimplementした際には、ValueAccessorをDIする必要があります。

providers: [
   {
      provide: NG_VALUE_ACCESSOR,
      useExisting: CustomRadioFormControlComponent,
      multi: true,
    },
  ]

ここまでのDEMO

自作したapp-required-textにngModelを設定し、2wayバインドができていることが確認できます。
もちろんReactiveFormにしてFormControlを渡して使用することができます。

app.component.html
<app-required-text [(ngModel)]="name"></app-required-text>
  {{name}}

次回に続きます。

参考