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

# はじめに

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

- [Angular de Custom Form](https://zenn.dev/seapolis/articles/31e80bd2c477f0)

この記事にはValidatorについても書かれており隙がありません。
ではもう書くことがないかと言われると、それもまた違います。
`formControl` には[markAllAsTouched()](https://angular.jp/api/forms/AbstractControl#markAllAsTouched)というメソッドが生えているのですが、 `ControlValueAccessor` はこれに対応していません。[formGroup](https://angular.io/api/forms/FormGroup)に `ControlValueAccessor` で定義したフォームコンポーネントを含めて `markAllAsTouched()` したとしても、フォームコンポーネント内のフォームは `touched = true` とはならないのです。
ではどうするか？　 本記事で解説します。

`ControlValueAccessor` については上記した記事[Angular de Custom Form](https://zenn.dev/seapolis/articles/31e80bd2c477f0)をみていただくよう、よろしくお願いします。

# 完成

https://stackblitz.com/edit/angular-ivy-ifhrpz?file=src/app/forms/form2/form2.component.ts

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

# 結論

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

```ts
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` で型を合わせられるようになってよりコーディングがしやすくなりました。

```html
<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>
```

```ts
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のissue[angular/angular#45089](https://github.com/angular/angular/issues/45089)で `markAsTouched` を `ControlValueAccessor` に入れようという話がありますが、見たところ現在は入っていません。
このissueの最後に代替案としてのディレクティブが書かれていたため、本記事を執筆しようと思い立った次第です。

- https://stackblitz.com/edit/angular-ivy-zr9src?file=src%2Fapp%2Ffix-cva.directive.ts

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