前回 は、これまで学んできたことの実践を通してAngularに対する知識を深めました。
本記事では、 [(ngModel)]
を利用しないフォーム ( Reactive forms
といいます )の書き方を学んでいきます。
- ReactiveFormの基礎
- [(ngModel)] をFormGroup, FormControlで書き換える
- Formの送信による保存処理
をやっていきます。
この記事のソースコード
https://github.com/seteen/AngularGuides/tree/入門その07
AngularのForm要素の基礎
現状の product-edit.component.ts
は [(ngModel)]
を利用して Form
の一つ一つの要素を product
の各要素と対応付けています。
Angularでは、 Form
に対するこのような関連付けの処理を専用で行うためのクラスが用意されています。
それぞれについて説明していきます。
クラス | 役割 |
---|---|
FormControl |
<input> などの入力要素一つに対応するクラス |
FormGroup |
<form> 要素に対応するクラス |
FormBuilder | FormGroup, FormControl を作成するクラス |
[(ngModel)]
を使った例との対応
現状の product-edit.component.ts
との対応を考えると下記のようになります。
クラス | 役割 | product-edit.component |
---|---|---|
FormControl |
<input> などの入力要素一つに対応するクラス |
product |
FormGroup |
<form> 要素に対応するクラス |
product.name |
FormBuilder | FormGroup, FormControl を作成するクラス | なし |
[(ngModel)]
を FormGroup
, FormControl
で書き換える
まずは、 Typescriptから書き換えていきます。
import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { FormBuilder } from '@angular/forms';
@Component({
selector: 'app-product-edit',
templateUrl: './product-edit.component.html',
styleUrls: ['./product-edit.component.scss']
})
export class ProductEditComponent implements OnInit {
productForm = this.fb.group({ // <= 変更
id: [''],
name: [''],
price: [''],
description: [''],
});
constructor(
private route: ActivatedRoute,
private router: Router,
private fb: FormBuilder, // <= 追加
private productService: ProductService,
) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.productService.get(params['id']).subscribe((product: Product) => {
this.productForm.setValue({ // <= 変更
id: product.id,
name: product.name,
price: product.price,
description: product.description,
});
});
});
}
saveProduct(): void {
this.router.navigate(['/products']);
}
}
解説
constructor部分
FormBuilder
を追加しています。
変数定義部分
product
変数を消して、 productForm
変数を追加しました。
productForm
の型は、 FormGroup
です。 this.fb.group()
で FormGroup
を入れています。
このメソッドの引数は、 Form
の要素として利用する値のキーとその初期値です(配列になっているのは、2つ目移行にValidatorを入れられるようにするためです。これは後ほど説明します)。
つまり、ここでは、 product
と同じ要素(id, name, price, description) を持つ FormGroup
を作っています。
なお、この各 name: ['']
が FormControl
です。
FormBuilderを使うとどの要素がどのクラスなのかわかりにくいので、FormBuilderを使わないで同様の処理ができるコードを下記に書いておきます。
productForm = new FormGroup({
id: new FormControl(''),
name: new FormControl(''),
price: new FormControl(''),
description: new FormControl(''),
});
ngOnInit部分
productService
から product
を取り出した後に、 productForm
に現状の product
の値を入れています。
HTMLで FormGroup
, FormControl
を使う
<div class="container">
<div class="title">商品編集</div>
<form (ngSubmit)="saveProduct()" [formGroup]="productForm">
<div class="edit-form">
<div class="edit-line">
<label>ID</label>
<span>{{ productForm.controls.id.value }}</span>
</div>
<div class="edit-line">
<label>名前</label>
<input type="text" formControlName="name" required>
</div>
<div class="edit-line">
<label>価格</label>
<input type="number" formControlName="price">
</div>
<div class="edit-line">
<label>説明</label>
<input type="text" formControlName="description">
</div>
</div>
<div class="footer">
<span class="button white" [routerLink]="['/products', productForm.controls.id.value]">キャンセル</span>
<button class="button black">保存</button>
</div>
</form>
</div>
<form>
要素に対して [formGroup]
を指定することで、 <form>
に対して productForm
を関連付けています。
また、各 <input>
要素から [(ngModel)]
を消して、 formControlName
を追加して productForm
の各要素と関連付けています。
ID だけは編集するものではないので、 productForm.control.id.value
で取り出してそのまま表示するようにしています。
新しいモジュールを追加する
実は、ここで起動してもまたエラーとなります。
compiler.js:215 Uncaught Error: Template parse errors:
Can't bind to 'formGroup' since it isn't a known property of 'form'. ("<div class="container">
<div class="title">商品編集</div>
<form (ngSubmit)="saveProduct()" [ERROR ->][formGroup]="productForm">
<div class="edit-form">
<div class="edit-line">
これは、また モジュールが足りないという意味です。
その6で紹介した手順で探してみます。
https//angular.io
にアクセスして FormGroup
を探してみます。
ここで module
で検索。 .... ない!
実は、Angularの公式は、このあたりかなりひどく、わかりにくいです。
実は、この Can't bind to 'xxx' since it isn't a known property of 'yyyy'
というエラーですが、 Directive
(HTMLで言うところの 属性(attribute) ) がないというエラーです。
上記画像で調べたものは、 実は Directive
ではなく、 Class
でした。
https://angular.io
で FormGroup
を検索すると、下記のような画面になります。
ここで、 C
となっているものは Class
, D
となっているものが Directive
です。
FormGroup
の下に FormGroupDirective
というものがあります。
実は、こちらを調べないといけません。
FormGroupDirective
の画面に行くと、上記の画面に行きます。
ここに
@Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: { '(submit)': 'onSubmit($event)', '(reset)': 'onReset()' },
exportAs: 'ngForm'
})
と記載されています。
この Directive
の selector
が [formGroup]
になっています。つまり、名前に Directive
とついてしまって混乱しましたが、これが調べたかったものです。
ここで module
を検索してみます。
ありましたね。
この [formGroup]
は ReactiveFormModule
に属しています。
app.module.ts
に追加します。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ProductListComponent } from './product/product-list/product-list.component';
import { ProductDetailComponent } from './product/product-detail/product-detail.component';
import { AppRoutingModule } from './app-routing.module';
import { ProductEditComponent } from './product/product-edit/product-edit.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
ProductListComponent,
ProductDetailComponent,
ProductEditComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
この状態で、動作確認してみましょう。
ng serve
で起動して、 http://localhost:4200/products/1/edit
にアクセスしてみます。
ちゃんと値が入ってますね。 FormGroup
, FormControl
がうまく <form>
要素と関連付けられました。
TIPS: 奥が深いForm要素
今回、3つのクラスを紹介しましたが、Angularには Formに関するクラスが他にも用意されています。
詳しくは Angularの公式サイトで調べてみてください。
https://angular.io/guide/reactive-forms
ここまでのコミット
Formの送信による保存処理
[(ngModel)]
を FormGroup
, FormControl
を用いて置き換えていきました。
現状のこのアプリでは、全ての状態はメモリ上で処理されています。 [(ngModel)]
のときには、双方向バインディングであったがために、
そのまま何もしなければ保存されていました。
今回で [(ngModel)]
を使わなくなったため、編集しても ProductService
が持つ products
を変更する処理がないため、変更が保存されません。
ProductService
に update()
メソッドを追加して、変更を保存するように修正していきます。
import { Injectable } from '@angular/core';
import { Product } from '../models/product';
import { Observable, of } from 'rxjs/index';
@Injectable({
providedIn: 'root'
})
export class ProductService {
products = [
new Product(1, 'Angular入門書「天地創造の章」', 3800, '神は云った。「Angularあれ」。するとAngularが出来た。'),
new Product(2, 'Angularを覚えたら、年収も上がって、女の子にももてて、人生が変わりました!', 410, '年収300万のSEが、Angularと出会う。それは、小さな会社の社畜が始めた、最初の抵抗だった。'),
new Product(3, '異世界転生から始めるAngular生活(1)', 680,
'スパゲッティの沼でデスマーチ真っ最中の田中。過酷な日々からの現実逃避か彼は、異世界に放り出され、そこでAngularの入門書を拾う。現実逃避でさえ、プログラミングをするしかない彼に待ち受けるのは!?'),
];
constructor() { }
list(): Observable<Product[]> {
return of(this.products);
}
get(id: number): Observable<Product> {
return of(this.products[id - 1]);
}
update(product: Product): void { // <= 追加
const index = this.products.findIndex((prd: Product) => prd.id === product.id);
this.products[index] = product;
}
}
import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { FormBuilder } from '@angular/forms';
@Component({
selector: 'app-product-edit',
templateUrl: './product-edit.component.html',
styleUrls: ['./product-edit.component.scss']
})
export class ProductEditComponent implements OnInit {
productForm = this.fb.group({
id: [''],
name: [''],
price: [''],
description: [''],
});
constructor(
private route: ActivatedRoute,
private router: Router,
private fb: FormBuilder,
private productService: ProductService,
) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.productService.get(params['id']).subscribe((product: Product) => {
this.productForm.setValue({
id: product.id,
name: product.name,
price: product.price,
description: product.description,
});
});
});
}
saveProduct(): void {
const { id, name, price, description } = this.productForm.getRawValue(); // <= 追加
this.productService.update(new Product(id, name, price, description)); // <= 追加
this.router.navigate(['/products', this.productForm.controls.id.value]); // <= 変更
}
}
解説
ProductServiceについて
ProductService
に update()
メソッドを追加しました。
update(product: Product): void {
const index = this.products.findIndex((prd: Product) => prd.id === product.id);
this.products[index] = product;
}
処理としては、updateで与えられた product
と同じ id
を持つ index
を探して、 products
の index
番目の要素を入力された product
と入れ替えるというものです。
TIPS: findIndex() メソッド
JavascriptのArrayのメソッドです。Arrayの各要素について、引数のメソッドを実行して、true
が帰ってくる最初の配列のインデックスを返します。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
ProductEditComponentについて
saveProduct()
メソッドを変更しました。
saveProduct(): void {
const { id, name, price, description } = this.productForm.getRawValue(); // <= 追加
this.productService.update(new Product(id, name, price, description)); // <= 追加
this.router.navigate(['/products', this.productForm.controls.id.value]); // <= 変更
}
まず最初の行の
const { id, name, price, description } = this.productForm.getRawValue();
について解説します。
this.productForm.getRawValue()
では、 FormGroup
の FormControl
の値を key: value
の形式でオブジェクトに変換したものを返します。
今回、例えば id
が 3
の product
に対して何も編集せずに実行すると下記を返します。
{id: 3, name: "異世界転生から始めるAngular生活(1)", price: 680, description: "スパゲッティの沼でデスマーチ真っ最中の田中。過酷な日々からの現実逃避か彼は、異世界に放り出され、そこでAngularの入門書を拾う。現実逃避でさえ、プログラミングをするしかない彼に待ち受けるのは!?"}
次に、
const { xxx } = yyy
というのは、 yyy
オブジェクトの xxx
要素を xxx
という変数に入れる、という処理になります。
そのため、 id, name, price, description
という変数に FormGroup
の各値を入れているということになりますね。
次の行では、 ProductService
に新しく作った update()
メソッドを呼び出しています。
最後の行は、特に変更する必要はなかったのですが、保存後に一覧画面に遷移するようになっていたので、詳細画面に遷移するように修正しました。
では、動作確認をしてみましょう。
ちゃんと保存した内容が反映されるようになりましたね。
ここまでのコミット
まとめ
今回は、 [(ngModel)]
を使わない Reactive forms
について学んでいきました。
Reactive forms
は、 [(ngModel)]
に比べて少し冗長ですが、次回のバリデーション部分を含めると、こちらもかなり使い勝手が良くなってきます。
次回は、Angularのバリデーションのやり方を解説していきます。
Angular入門 未経験から1ヶ月でサービス作れるようにする その8. バリデーション
入門記事一覧
「Angular入門 未経験から1ヶ月でサービス作れるようにする」は、記事数が多いため、まとめ記事 を作っています。
https://qiita.com/seteen/items/43908e33e08a39612a07