はじめに
本記事はAngular Advent Calendar 202211日目担当です。
Angularにはフォームに係る処理を操作するために、FormControlが用意されています。
これは主にform系に適用するわけですが、複数のフォームがあり、それらをまとめてひとつのフォームで扱いたい場合があるかと思います。これの実現手段としてControlValueAccessorがあります。
結構ニッチな内容ですから書いている人は少ないだろうとシメシメ考えていましたが、昨年のアドベントカレンダーにありました。
この記事にはValidatorについても書かれており隙がありません。
ではもう書くことがないかと言われると、それもまた違います。
formControl
にはmarkAllAsTouched()というメソッドが生えているのですが、 ControlValueAccessor
はこれに対応していません。formGroupに ControlValueAccessor
で定義したフォームコンポーネントを含めて 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#45089で markAsTouched
を ControlValueAccessor
に入れようという話がありますが、見たところ現在は入っていません。
このissueの最後に代替案としてのディレクティブが書かれていたため、本記事を執筆しようと思い立った次第です。
上のディレクティブと私のディレクティブの違いは controls[0]
を取らずに、foreachで markAllAsTouched()
を回す点のみです。
しかしこれにより配下のフォーム全てのtouchedをtrueにできます。
ControlValueAccessor
はフォームが多いアプリケーションを作るのに、影響範囲をスコープできて便利です。さらに求められる知識が多くなりますが、ngOnChangesとdetectChangesによるゴリ押し変更検知から解放されるかもしれません。
今後益々使いやすくなってくれることを期待です。
以上、Angular Advent Calendar 202211日目でした。