Angular入門 未経験から1ヶ月でサービス作れるようにする その7. Angularのフォーム2 (Reactive forms)

[前回](https://qiita.com/seteen/items/7df296c342cdd3045717) は、これまで学んできたことの実践を通してAngularに対する知識を深めました。

本記事では、 `[(ngModel)]` を利用しないフォーム ( `Reactive forms` といいます )の書き方を学んでいきます。

- ReactiveFormの基礎
- [(ngModel)] をFormGroup, FormControlで書き換える
- Formの送信による保存処理

をやっていきます。

## この記事のソースコード
[https://github.com/seteen/AngularGuides/tree/入門その07](https://github.com/seteen/AngularGuides/tree/%E5%85%A5%E9%96%80%E3%81%9D%E3%81%AE07)

## 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から書き換えていきます。

``` src/app/product/product-edit/product-edit.component.ts
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を使わないで同様の処理ができるコードを下記に書いておきます。

``` .ts
productForm = new FormGroup({
  id: new FormControl(''),
  name: new FormControl(''),
  price: new FormControl(''),
  description: new FormControl(''),
});
```

#### ngOnInit部分

`productService` から `product` を取り出した後に、 `productForm` に現状の `product` の値を入れています。


### HTMLで `FormGroup` , `FormControl` を使う

``` src/app/product/product-edit/product-edit.component.html
<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` を探してみます。

<img width="1238" alt="001.png" src="https://qiita-image-store.s3.amazonaws.com/0/67424/bcb77e89-2570-f87f-1eea-14907b65d6f2.png">

ここで `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` を検索すると、下記のような画面になります。

<img width="1235" alt="002.png" src="https://qiita-image-store.s3.amazonaws.com/0/67424/b5b54eff-c539-ead2-3557-2b95e70069ab.png">


ここで、 `C` となっているものは `Class` , `D` となっているものが `Directive` です。

`FormGroup` の下に `FormGroupDirective` というものがあります。

実は、こちらを調べないといけません。

<img width="1224" alt="003.png" src="https://qiita-image-store.s3.amazonaws.com/0/67424/9a8402b9-da32-d9cf-a144-ac764c8d5af8.png">


`FormGroupDirective` の画面に行くと、上記の画面に行きます。

ここに

``` .ts
@Directive({
    selector: '[formGroup]',
    providers: [formDirectiveProvider],
    host: { '(submit)': 'onSubmit($event)', '(reset)': 'onReset()' },
    exportAs: 'ngForm'
})
```

と記載されています。

この `Directive` の `selector` が `[formGroup]` になっています。つまり、名前に `Directive` とついてしまって混乱しましたが、これが調べたかったものです。

ここで `module` を検索してみます。

<img width="686" alt="004.png" src="https://qiita-image-store.s3.amazonaws.com/0/67424/450e6b8a-325f-56aa-a57b-1105aa56c13d.png">


ありましたね。

この `[formGroup]` は `ReactiveFormModule` に属しています。

`app.module.ts` に追加します。

``` src/app/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` にアクセスしてみます。

![005.png](https://qiita-image-store.s3.amazonaws.com/0/67424/27b0e75c-9950-9975-709d-667e3fdb459d.png)


ちゃんと値が入ってますね。 `FormGroup` , `FormControl` がうまく `<form>` 要素と関連付けられました。

> TIPS: 奥が深いForm要素
> 今回、３つのクラスを紹介しましたが、Angularには Formに関するクラスが他にも用意されています。
> 詳しくは Angularの公式サイトで調べてみてください。
> https://angular.io/guide/reactive-forms

#### ここまでのコミット

https://github.com/seteen/AngularGuides/commit/a194b5d404828f7a76f68d488983fd8dd251cd9a

## Formの送信による保存処理

`[(ngModel)]` を `FormGroup` , `FormControl` を用いて置き換えていきました。

現状のこのアプリでは、全ての状態はメモリ上で処理されています。 `[(ngModel)]` のときには、双方向バインディングであったがために、
そのまま何もしなければ保存されていました。

今回で `[(ngModel)]` を使わなくなったため、編集しても `ProductService` が持つ `products` を変更する処理がないため、変更が保存されません。

`ProductService` に `update()` メソッドを追加して、変更を保存するように修正していきます。

``` src/app/shared/services/product.service.ts
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;
  }
}
```

``` src/app/product/product-edit/product-edit.component.ts
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()` メソッドを追加しました。

``` .ts
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()` メソッドを変更しました。

``` .ts
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]); // <= 変更
}
```

まず最初の行の

``` .ts
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()` メソッドを呼び出しています。

最後の行は、特に変更する必要はなかったのですが、保存後に一覧画面に遷移するようになっていたので、詳細画面に遷移するように修正しました。

では、動作確認をしてみましょう。

![006.gif](https://qiita-image-store.s3.amazonaws.com/0/67424/e01ccc95-3830-47f2-8646-75f90588279c.gif)


ちゃんと保存した内容が反映されるようになりましたね。

#### ここまでのコミット

https://github.com/seteen/AngularGuides/commit/09075ffc2dfef79d8ac4463c3200dccfabb5e9ed

## まとめ
今回は、 `[(ngModel)]` を使わない `Reactive forms` について学んでいきました。
`Reactive forms` は、 `[(ngModel)]` に比べて少し冗長ですが、次回のバリデーション部分を含めると、こちらもかなり使い勝手が良くなってきます。

次回は、Angularのバリデーションのやり方を解説していきます。

[Angular入門 未経験から1ヶ月でサービス作れるようにする その8. バリデーション](https://qiita.com/seteen/items/d1525383aaf37322b107)

## 入門記事一覧
「Angular入門 未経験から1ヶ月でサービス作れるようにする」は、記事数が多いため、[まとめ記事](https://qiita.com/seteen/items/43908e33e08a39612a07) を作っています。
https://qiita.com/seteen/items/43908e33e08a39612a07