2020/01/26 nishitakuさんのご指摘により、angular version8で動く様に修正しました。
初めに
Reactive Forms に使用方法に関して既に多くの記事が上がっているのですが、自分なりに包括的にまとまっていてわかりやすいものがなかったため、勝手にまとめて見ました。
私は業務でangularを使用しているのですが、とにかくフォーム周りの実装が面倒で複雑です(汗。階層的にフォームを入れ子にするにはどうすれば良いか等様々なテクニックがありますが、情報が散らばっており収集に時間がかかりました。
そこで、本記事では取りあえずフォームの実装パターンを(たぶん)網羅的に組み込んだ例を掲載いたしますので、参考になればと思います。
今回解説で使用しているコードはgithubにアップしているので、必要があればお試しください。
Reactive Forms とは
公式より
リアクティブフォームは、リアクティブスタイルでフォームを作成するためのAngularのテクニックです。
Angularには入力フォーム類を記述する方法に、テンプテート駆動型とモデル駆動型が存在します。Reactive Form はモデル駆動型で実装するためのAPI群です。
準備
moduleにReactiveFormsModule
を追加してください。
@NgModule({
imports: [
ReactiveFormsModule
],
})
基本知識
- 使用するAPIは以下の3つです
- FormControl // 1個のフォームに相当。( input, checkbox...タグ 単位で用意する )
- FormGroup // 複数のフォームを束ねるクラス。 ( formタグ単位で用意する )
- FormArray // 同じ形式のフォームを配列で管理する。 ( address_1, address_2, address_3, ... )
全てのクラスは抽象クラス(
AbstractControl
)を継承しています。
example(本題)
以下の様なフォームの実装を考えます。
↑図が間違っていたので修正しました。(2018/7/19)
上記の図では
フォームが 「単体か or 配列か」 ✖️ フォームを 「親コンポーネントに記載するか or 子コンポーネントに切り出すか」
の4パターンの実装を想定しており、この4パターンでほとんどのケースは網羅できると思います。
親コンポーネント(ParentComponent.ts
)
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.css']
})
export class ParentComponent {
public formGroup: FormGroup; // 全てのコントロールをまとめる為のグループを宣言
// 住所のフォームの数だけ、ラベルを宣言する
public addresses = [ '住所1', '住所2' ];
// 電話番号のフォームの数だけ、ラベルを宣言する
public phone_numbers = [ '電話番号1', '電話番号2' ];
constructor( private formBuilder: FormBuilder ) {
setInterval( () => {
console.log(this.formGroup);
}, 3000);
this.formGroup = this.formBuilder.group( { // group関数でFormGroupを生成
addresses: this.formBuilder.array( [] ), // Arrayで宣言する部分をあらかじめセットする
phone_numbers: this.formBuilder.array( [] ) // 同上
} );
// addControl で氏名のコントロールを追加
// validatorは必要があれば入力
this.formGroup.addControl( 'name', new FormControl(
'' /* 初期値を設定する。もしくは { value: '初期値', disabled: true } の様にも書ける */,
[ /* validator */ ] )
);
// 年齢のコントロールを追加
// validatorに 0 ~ 100 までの値を入力する様にvalidationルールを設定する
this.formGroup.addControl( 'age', new FormControl( '',
[ this.range(0, 100) ] // validatorに関しては[おまけ]を参照
) );
// 住所のformGroupをセット
this.addresses.map( () => {
const child_form_group = this.formBuilder.group({});
// formGroupにコントロールをセット
child_form_group.addControl( 'prefecture', new FormControl( '', [] ) );
child_form_group.addControl( 'city', new FormControl( '', [] ) );
// formGroupのaddresses(FormArray)に生成した子コンポーネントを追加する
// ( FormGroup.getはAbstractControlを返すので、as句を使ってダウンキャスとする )
( this.formGroup.get( 'addresses' ) as FormArray ).push( child_form_group );
});
}
// 範囲を確認するValidator
public range = ( min: number, max: number ): ValidatorFn => {
return ( control: AbstractControl ): { outOfRange: any } => {
const value = control.value;
// valueがセットされていない時はエラーなし
if ( value === null || value === undefined ) {
return null;
}
// 値が範囲ないに収まっていることを確認する
const message = ( ( value >= min ) && ( value <= max) ) ? null : min + '~' + max + 'の値を入力してください。';
return { outOfRange: message };
};
}
// formGroupからstrをキーとするFormArrayを返す
public getFormArrayOfString = ( str: string ): FormArray => {
// ( FormGroup.getはAbstractControlを返すので、as句を使ってダウンキャスとする )
return ( this.formGroup.get( str ) as FormArray );
}
}
親コンポーネントのテンプレートに反映する
<!-- formタグを宣言し、対応するFormGroupを渡す -->
<form [formGroup]="formGroup">
<!-- 親コンポーンネントに直接記載したフォーム -->
<!-- formControlNameから自動でFormGroup内が検索され、FormControlが紐付けられる-->
名前: <input [formControlName]="'name'"><br>
<!-- 子コンポーンネントで記載したフォーム -->
<app-children1 [formGroup]="formGroup"></app-children1>
住所: <br>
<!-- 親コンポーンネントで直接記載したフォームの配列 -->
<ng-container *ngFor="let address of addresses; let index = index;" [formArrayName]="'addresses'">
{{ address }}: <br>
<ng-container [formGroupName]="index">
都道府県: <input [formControlName]="'prefecture'">
市区町村: <input [formControlName]="'city'">
<br>
</ng-container>
</ng-container>
<!-- 子コンポーンネントで記載したフォームの配列 -->
電話番号: <br>
<ng-container *ngFor="let phone_number of phone_numbers; let index = index;">
{{ phone_number }}<br>
<app-children2 [formArray]="getFormArrayOfString( 'phone_numbers' )"></app-children2>
</ng-container>
</form>
<br>
formGroup.value | json <br>
<pre>
{{ formGroup.value | json }}
</pre>
子コンポーネント1
@Input() formGroup: FormGroup;
<form [formGroup]="formGroup">
年齢: <input [formControlName]="'age'" type="number">
Validation: {{ formGroup.get( 'age' ).valid }} // 補足部分のvalidation結果hyouzi
</form>
子コンポーネント2
@Input() formArray: FormArray; // 親コンポーネントからFormArrayを受け取る
public childFormGroup: FormGroup; // このコンポーネントのFormGroupを保持する
constructor( private formBuilder: FormBuilder ) { }
ngOnInit() {
// このコンポーネントのFormGroupを初期化する
this.childFormGroup = this.formBuilder.group({});
// FormGroupにコントロールをセットして行く
this.childFormGroup.addControl( 'phone_number1', new FormControl( '', [] ) );
this.childFormGroup.addControl( 'phone_number2', new FormControl( '', [] ) );
// 親コンポーネントのフォームに追加する
this.formArray.push( this.childFormGroup );
}
<form [formGroup]="childFormGroup">
上4桁: <input [formControlName]="'phone_number1'">
下4桁: <input [formControlName]="'phone_number2'">
<br>
</form>
おまけ
Validator
今回の実装例ではage
をAngular標準で提供されるValidators.min
, Validators.max
で実装しました。しかし、標準実装ものはあまり充実しておらず、エラーメッセージ(正確にはエラー時にformにセットされる任意のオブジェクト)が固定のため使いにくいことがあります。ここでは、カスタムValidatorの宣言方法を補足します。
constructor () {
// ....
this.formGroup.addControl( 'age', new FormControl( {},
[ this.range(0, 100) ] // ここをcustom validatorに変更
) );
// ....
}
// custom validatorを宣言
// 範囲を確認するValidator
public range = ( min: number, max: number ): ValidatorFn => {
return ( control: AbstractControl ): { outOfRange: any } => {
const value = control.value;
// valueがセットされていない時はエラーなし
if ( value === null || value === undefined ) {
return null;
}
// 値が範囲ないに収まっていることを確認する
if ( ( value >= min ) && ( value <= max) ) {
return null;
}
const message = min + '~' + max + 'の値を入力してください。';
return { outOfRange: message };
};
}