LoginSignup
3

More than 1 year has passed since last update.

ControlValueAccessorコンポーネントをmarkAllAsTouchedできるようにする

Last updated at Posted at 2022-12-10

はじめに

本記事はAngular Advent Calendar 202211日目担当です。
Angularにはフォームに係る処理を操作するために、FormControlが用意されています。
これは主にform系に適用するわけですが、複数のフォームがあり、それらをまとめてひとつのフォームで扱いたい場合があるかと思います。これの実現手段としてControlValueAccessorがあります。
結構ニッチな内容ですから書いている人は少ないだろうとシメシメ考えていましたが、昨年のアドベントカレンダーにありました。

この記事にはValidatorについても書かれており隙がありません。
ではもう書くことがないかと言われると、それもまた違います。
formControl にはmarkAllAsTouched()というメソッドが生えているのですが、 ControlValueAccessor はこれに対応していません。formGroupControlValueAccessor で定義したフォームコンポーネントを含めて markAllAsTouched() したとしても、フォームコンポーネント内のフォームは touched = true とはならないのです。
ではどうするか?  本記事で解説します。

ControlValueAccessor については上記した記事Angular de Custom Formをみていただくよう、よろしくお願いします。

完成

適用を押下することで配下のフォーム全てに値を挿入し、 markAllAsTouched() を伝搬させます。

結論

下記ディレクティブを定義し、そのディレクティブを markAllAsTouched() を行った時に適用したいフォームコンポーネントにつけることで対象とします。

import {
  ChangeDetectorRef,
  Directive,
  Inject,
  OnInit,
  Optional,
  Self,
} from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor,
  NG_VALUE_ACCESSOR,
  NgControl,
} from '@angular/forms';

@Directive({
  selector: '[appAlteWriteTouched]',
})
export class AlteWriteTouchedDirective implements OnInit {
  constructor(
    private changeDetectorRef: ChangeDetectorRef,
    @Inject(NgControl)
    @Optional()
    private ngControl?: NgControl,
    @Optional()
    @Self()
    @Inject(NG_VALUE_ACCESSOR)
    private valueAccessors?: ControlValueAccessor[]
  ) {}

  controls: AbstractControl[] = [];

  ngOnInit(): void {
    const control = this.ngControl?.control;
    if (!control || !this.valueAccessors || this.valueAccessors.length !== 1) {
      return;
    }

    const controls: AbstractControl[] = Object.values(
      this.valueAccessors[0]
    ).filter((v: unknown) => v instanceof AbstractControl);

    if (controls.length === 0) {
      return;
    }

    this.controls = controls;

    const parentMarkAllAsTouched = control.markAllAsTouched;
    control.markAllAsTouched = (): void => {
      parentMarkAllAsTouched.bind(this.ngControl?.control)();
      this.controls.forEach((c) => c.markAllAsTouched());
      this.changeDetectorRef.markForCheck();
    };
  }
}

使用例

一部抜き出しています。
全体のコードは完成のstackblizを見てください。
余談ではありますが、Angular v14からformControlに型をつけられるようになりました。
ControlValueAccessor で型を合わせられるようになってよりコーディングがしやすくなりました。

<div class="content-wrapper">
  <button mat-raised-button (click)="onClickButton()">適用</button>
  <app-form1 appAlteWriteTouched [formControl]="form1"></app-form1>
  <app-form2 appAlteWriteTouched [formControl]="form2"></app-form2>
</div>
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

import {
  Form1Type,
  Form1Initial,
  Form2Type,
  Form2Initial,
} from './inputs.interface';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  form1 = new FormControl<Form1Type>(Form1Initial, {
    nonNullable: true,
  });
  form2 = new FormControl<Form2Type>(Form2Initial, {
    nonNullable: true,
  });
  formGroup = new FormGroup({
    form1: this.form1,
    form2: this.form2,
  });

  onClickButton(): void {
    this.form1.setValue({
      a: 'abcdefghijklmn',
      b: 1000,
    });
    this.form2.setValue({
      a: 'opqrstuvwxyz',
      b: 1000,
    });
    this.formGroup.markAllAsTouched();
  }
}

おわりに

Angularのissueangular/angular#45089markAsTouchedControlValueAccessor に入れようという話がありますが、見たところ現在は入っていません。
このissueの最後に代替案としてのディレクティブが書かれていたため、本記事を執筆しようと思い立った次第です。

上のディレクティブと私のディレクティブの違いは controls[0] を取らずに、foreachで markAllAsTouched() を回す点のみです。
しかしこれにより配下のフォーム全てのtouchedをtrueにできます。
ControlValueAccessor はフォームが多いアプリケーションを作るのに、影響範囲をスコープできて便利です。さらに求められる知識が多くなりますが、ngOnChangesdetectChangesによるゴリ押し変更検知から解放されるかもしれません。
今後益々使いやすくなってくれることを期待です。
以上、Angular Advent Calendar 202211日目でした。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3