ReactiveFormsで使用できるカスタムコンポーネントを作成する
はじめに
本記事ではAngularは下記Versionを使用しています。
- AngularCLI: 7.3.6
- Angular: 7.2.10
ゴール
Inputコントロールなんかと同様に formControlName
を指定できるような
ReactiveFormsで使用できるComponentを作成します。
今回は「ダイアログ表示ボタン+入力ダイアログ」を一つのコンポーネントとし
そのコンポーネントをReactiveFormsで使用します。
操作イメージは下のGifのような動作イメージです。
「Result」内にあるように、ダイアログの入力内容が反映されます。
ControlValueAccessor
Angularの公式リファレンスによると、ControlValueAccessorを使用して、Valueとコントロールのリンクを行っていると記載されています。
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でカスタムフォーム(CustomFormControls)を作る(1/2) - Qiita
最後に
今回作成したデモは下記に格納しています。
デモソースのごった煮ですがご容赦ください ><