はまったりひらめいたり…とか…

Angularや.NETやAzureやその他色々。

ReactiveFormsで使用できるカスタムコンポーネントを作成する

はじめに

本記事ではAngularは下記Versionを使用しています。

  • AngularCLI: 7.3.6
  • Angular: 7.2.10

ゴール

Inputコントロールなんかと同様に formControlName を指定できるような

ReactiveFormsで使用できるComponentを作成します。

今回は「ダイアログ表示ボタン+入力ダイアログ」を一つのコンポーネントとし

そのコンポーネントをReactiveFormsで使用します。

操作イメージは下のGifのような動作イメージです。

「Result」内にあるように、ダイアログの入力内容が反映されます。

f:id:TakasDev:20190324184459g:plain

ControlValueAccessor

Angular

Angularの公式リファレンスによると、ControlValueAccessorを使用して、Valueとコントロールのリンクを行っていると記載されています。

Angular

ControlValueAccessorは、下記4つの機能のインタフェースを持つようです。

  • writeValue(obj: any) →ModelからViewへの値の書き込み
  • registerOnChange(fn: any) →View変更時のCallback
  • registerOnTouched(fn: any) → ViewTouch時のCallback
  • setDisabledState(isDisabled: boolean) → disabled/enabledをViewに反映する

全容は下記のソースです。

@Component({
  selector: 'app-address-input-dialog',
  templateUrl: './address-input-dialog.component.html',
  styleUrls: ['./address-input-dialog.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => AddressInputDialogComponent),
      multi: true
    }
  ]
})
export class AddressInputDialogComponent implements ControlValueAccessor {

  private addressData: AddressModel;
  private isDisabled = false;
  private onChange: any = () => {};
  private onTouched: any =  () => {};

  constructor(
    private dialog: MatDialog
  ) { }

  openDiaog() {
    const dialog =  this.dialog.open(DialogComponent, {
      data: this.addressData
    });
    dialog.afterClosed().subscribe(data => {
      this.addressData = data;
      this.onChange(data);
      this.onTouched();
    });
  }

  // ↓ControlValueAccessorのInterface群
  writeValue(obj: any): void {
    this.addressData = obj;
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

}

コントロールの動きとして、ボタン押下時にダイアログを表示

ダイアログが確定されたらデータを反映。という流れです。

そのため、dialog.afterClosed() でダイアログが閉じられた際に、formcontrolの値変更時のイベントを実行しています。

ダイアログコンポーネント側はオーソドックスなReactiveFormなので割愛します。

何が嬉しい?

下記は作成したカスタムコンポーネントを使用しているコンポーネントのソースです。

export class MyFormGroupFieldComponent implements OnInit {

  public fg: FormGroup;

  constructor(
    private fb: FormBuilder
  ) { }

  get formResult() {
    if (typeof(this.fg) !== 'undefined') {
      return JSON.stringify(this.fg.value);
    } else {
      return '';
    }
  }

  ngOnInit() {
    const initAddressData: AddressModel = { city: '', prefecture: '', zipCode: ''};
    this.fg = this.fb.group({
      userId: ['', Validators.required],
      familyName: ['', Validators.required],
      lastName: ['', Validators.required],
      address: [initAddressData]
    });
  }

  patchData() {
    const initAddressData: AddressModel = { city: '港区', prefecture: '東京都', zipCode: '000-0000'};
    const data: MyFormGroupModel = {
      userId: 'devtakas',
      familyName: 'familyname',
      lastName: 'lastname',
      address: initAddressData
    };
    this.fg.patchValue(data);
  }

}

色々と書かれているように見えますが

初期値を設定したり、固定データを反映したり、画面上に表示する文字列を作成しているだけです。

なので、「ダイアログ開いたときは値を反映して~」とか「閉じたときは変数に値を格納~」みたいな処理は記述しておりません。

@ViewChild で子コンポーネントのプロパティにアクセスする。なんてこともしていないです。

コンポーネントの処理記述が非常に簡潔になります。

またformControlNameをカスタムコンポーネントに指定することができるため

Htmlテンプレートも下記のようにシンプルに記述できるようになります。

<form [formGroup]="fg">
...
  <div>
    <app-address-input-dialog formControlName="address"></app-address-input-dialog>
  </div>
...
</form>

formに対して構造体の値そのままでやり取りできるのも良いポイントですね。楽。

参考ページ

Angular

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

最後に

今回作成したデモは下記に格納しています。

デモソースのごった煮ですがご容赦ください ><

github.com