Angular でフォームのコンポーネントを作る
Angular でフォーム UI のコンポーネントを作成する Tips です。
Angular には フォームを管理するための ReactiveModule というものが用意されており、
Vue.js などのフォームUIコンポーネントの考え方とは異なる部分があるので注意が必要です。
フォーム UI のコンポーネント設計
Reactive Form でフォームを作成する場合、
フォーム UI のコンポーネントは以下のような形で設計できます。
- 親コンポーネントで FormGroup/FormControl を作成する
- 子コンポーネントは、FormControl を受け取る
- 子コンポーネントは、内部の値変化を検知し、FormCntrol の値を変化させる
親から子に FormControl を渡すことで、
子供の UI コンポーネントはイベントを Emit することなく UI コンポーネントを実装できます。
(input/change をEmit しても良いですが、親が 子のイベントをハンドリングすると、記述がだいぶ複雑になります。)
シンプルなテキストフォーム
シンプルなテキストフォームで、UIコンポーネントを実装する場合、以下のようなコードになります。
フォーム要素の input / change を取得して、値の変更や dirty/touched のフラグ操作を実施しています。
import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-birth-input',
templateUrl: './birth-input.component.html',
styleUrls: ['./birth-input.component.css'],
})
export class BirthInputComponent implements OnInit {
@Input() formControl: FormControl;
onInput(e) {
this.formControl.setValue(e.target.value);
this.formControl.markAsDirty();
}
onChange() {
this.formControl.markAsTouched();
}
}
<div>
<label>誕生日</label>
<input
type="text"
[value]="formControl.value"
(input)="onInput($event)"
(change)="onChange()" />
</div>
親コンポーネントでは以下のような形で、フォーム要素を利用することができます。
(birth_on
は FormControl の変数です。)
<app-my-uiform [formControl]="birth_on"></app-my-uiform>
生年月日 UI を作成する
上記のような形で フォームが一つのみのケースはまれで、
通常コンポーネントに分割するフォームUIを作成する際には、
フォームの要素が複数あるパターンが多いと思います。
複数のフォームの制御を行う際には、コンポーネントの内部でも ReactiveForm を利用したくなるでしょう。
このような場合には、(input) で値の変更を取得するのではなく、
formControl の valueChanges を subscribe して変更検知を受け取ります。
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { distinct } from 'rxjs/operators';
interface DateObj {
year: string;
month: string;
day: string;
}
@Component({
selector: 'app-birth-input',
templateUrl: './birth-input.component.html',
styleUrls: ['./birth-input.component.css'],
})
export class BirthInputComponent implements OnInit {
@Input() control: FormControl;
dateform = new FormGroup({
year: new FormControl(''),
month: new FormControl(''),
day: new FormControl(''),
});
ngOnInit() {
this.setParentValue(this.control.value);
this.control.valueChanges.pipe(distinct()).subscribe((r) => {
this.setParentValue(r);
});
this.dateform.valueChanges.pipe(distinct()).subscribe((r: DateObj) => {
if (!this.isValidDate(r)) {
return;
}
this.control.setValue(this.dateStr(r));
});
}
setParentValue(datestr: string) {
const [year, month, day] = datestr.split('/');
const dateObj: DateObj = { year, month, day };
if (this.isValidDate(dateObj)) {
this.dateform.setValue(dateObj);
}
}
dateStr(date: DateObj) {
return `${date.year}/${date.month}/${date.day}`;
}
isValidDate(date: DateObj) {
const dateStr = this.dateStr(date);
return !!Date.parse(dateStr); // null on Error
}
onInput() {
this.control.markAsDirty();
}
onChange() {
this.control.markAsTouched();
}
}
<div>
<label>誕生日</label>
<form [formGroup]="dateform">
<input
type="text"
formControlName="year"
(input)="onInput()"
(change)="onChange()"
/>
年
<input
type="text"
formControlName="month"
(input)="onInput()"
(change)="onChange()"
/>
月
<input
type="text"
formControlName="day"
(input)="onInput()"
(change)="onChange()"
/>
日
</form>
</div>
Sample
Select リストで作成したフォームの例が以下になります。
Discussion