Angular Reactive Form その1 FormGroupを作る

今回は、テンプレートベースの双方向バインディングではなく、Reactive Formを実装していく第一回目になります。

注意
本連載は、基本的に前回までの作業が終わっていることを前提としています。最初から試したい方は以前の記事も参考にしてください。また、今回から作成するWebページはESPPなどで必要な情報を入れると確定申告に必要な情報をだしてくれるものを目指しますが、確定申告に必要な情報を保証するようなものではないので、了承の上使用ください。
Angular Material + Flex Layout その5 Flex Layoutでメインコンポーネントの見た目を完成

各種バージョンは以下のとおりです。

  • Angular CLI: 11.2.8
  • Node: 15.10.0
  • Angular Material: 11.2.8

今回の目標とソースコードについて

今回の目標は、FormGroupを作りフォームの各要素がちゃんと取得できているところまでを確認します。下記の画像のように入力した値がページの下部に随時反映されるようにします。

参考 今回のソースコード

Reactive Formsのメリット

Angularには、テンプレートベースのフォームとリアクティブフォームがあります。どちらもユーザからのインプットを受け取って画面の見た目を変えたりできます。テンプレートベースのフォームは、Typescript内で宣言した変数を直接htmlで指定するため、コードの容易さに加え直感的です。しかしながら、入力した内容のバリデーションを実施したり、入力された内容に合わせて他のフォームの内容を変えたりと動的なフォームをつくるのには向いていません。そのような用途なために、リアクティブフォームが用意されており、ユーザの入力内容に合わせて動的にページを変えていくことができます。どちらも長所と短所があり時と場合によって使い分けるのが重要です。ざっくりですが、2つの長所を下記にまとめました。

  • テンプレートベースのフォーム
    • 使い方が簡単
    • Typescriptでの記述は最小限ですむ
  • リアクティブフォーム
    • 柔軟なフォームを作成可能
    • 入力されたデータに応じて処理が記述しやすい
    • Typescriptで多くの処理を記述する必要がある

Reactive Formsモジュールのインポート

Reactive Formを使うために、モジュールをインポートします。追加内容は以下のとおりです。

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ReactiveFormsModule
  ],
})

前回までのソースに加えると最終的にapp.module.tsは下記のとおりです。

app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FlexLayoutModule } from '@angular/flex-layout';

import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from './shared/material.module';
import { FooterComponent } from './shared/footer/footer.component';
import { HeaderComponent } from './shared/header/header.component';
import { MainComponent } from './main/main.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    FooterComponent,
    HeaderComponent,
    MainComponent,
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FlexLayoutModule,
    MaterialModule,
    FormsModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

これによりまずはコンポーネント内でFormGroupを宣言する下地は完了です。

FormGroupの作成

続いてFormGroupを作っていきます。ここで今後使うオブジェクトの整理をします。

  • FormControl
    • 1つのフォームの値やその入力のバリデーションを管理するオブジェクトです。一つのフォームに対して一つのFormContorlを作成します。
  • FormGroup
    • FormControlのグループを定義したもの。
  • FormArray
    • FormControl、FormGroup、FormArrayなどを配列として定義したもの。
  • FormBuilder
    • FormControl、FormGroup、FormArrayを作るのに必要なツールを提供するクラスです。

Formなんとかというオブジェクトがたくさんあり混乱しますが、FormBuilderでWebページ上のフォームの内容を管理するFormGroupを作るとまずは考えていただければ大丈夫です。

ちなみに、FormGroupをつくるときに、FormBuilderを使わずに作成する方法もあり、下記2つの方法があります。

  • 一つのフォームごとにFormControlを作成し、FormGroupに追加していく方法
  • FormBuliderでまるっと最初に宣言していく方法

基本的には、後者のFormBuilderを使う方法がシンプルなので、今回もそちらを使っていきます。

まず、今回ページ内のフォームを管理するFormGroupを作成します。追加したコードの切り抜きは以下のとおりです。

import { FormBuilder, FormGroup, Validators } from "@angular/forms";

export class MainComponent implements OnInit {

  esppForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.esppForm = this.fb.group({
      name: '',
      purchaseDate: ['', Validators.required],
      quantity: [0, Validators.required],
      marketPrice: [0, Validators.required],
      purchasePrice: [0, Validators.required]
    })
  }
}

変更点は以下のとおりです。

  • 必要なモジュールをインポート
  • コンストラクターにFormBuilderを追加
  • FormBuilderを使ってFormGroupを作成

各フォームを宣言する際に初期値やバリデーターを指定することができます。

  • nameは初期値に空文字を割り当てています。
  • quantityは初期値に0を指定し、必須項目というバリデータを入力しています。

カスタムなバリデータを指定したり、フォームを無効化するといったことも可能です。

その結果、最終的なmain.component.tsは以下の通りです。

main.component.ts
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";

export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

const ELEMENT_DATA: PeriodicElement[] = [
  { position: 1, name: "Hydrogen", weight: 1.0079, symbol: "H" },
  { position: 2, name: "Helium", weight: 4.0026, symbol: "He" },
  { position: 3, name: "Lithium", weight: 6.941, symbol: "Li" },
  { position: 4, name: "Beryllium", weight: 9.0122, symbol: "Be" },
  { position: 5, name: "Boron", weight: 10.811, symbol: "B" },
  { position: 6, name: "Carbon", weight: 12.0107, symbol: "C" },
  { position: 7, name: "Nitrogen", weight: 14.0067, symbol: "N" },
  { position: 8, name: "Oxygen", weight: 15.9994, symbol: "O" },
  { position: 9, name: "Fluorine", weight: 18.9984, symbol: "F" },
  { position: 10, name: "Neon", weight: 20.1797, symbol: "Ne" },
];

@Component({
  selector: "app-main",
  templateUrl: "./main.component.html",
  styleUrls: ["./main.component.scss"],
})
export class MainComponent implements OnInit {
  yen = 0;

  esppForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.esppForm = this.fb.group({
      name: ['', Validators.required],
      purchaseDate: ['', Validators.required],
      quantity: [0, Validators.required],
      marketPrice: [0, Validators.required],
      purchasePrice: [0, Validators.required]
    })
  }

  displayedColumns: string[] = ["position", "name", "weight", "symbol"];
  dataSource = ELEMENT_DATA;

  ngOnInit(): void {
  }
  dollar(): number {
    return this.yen / 105;
  }

}

続いて、このFormGroup内の各FormControlをHTML内のフォームに紐付けていきます。変更内容は以下のとおりです。

    <form novalidate [formGroup]="esppForm">
              <mat-form-field class="espp-form">
                <mat-label>銘柄</mat-label>
                <input matInput formControlName="name"/>
              </mat-form-field>
              <mat-form-field class="espp-form">
                <mat-label>購入日</mat-label>
                <input matInput [matDatepicker]="picker" formControlName="purchaseDate"/>
                <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
                <mat-datepicker #picker></mat-datepicker>
              </mat-form-field>
              <mat-form-field class="espp-form">
                <mat-label>株数</mat-label>
                <input matInput formControlName="quantity"/>
              </mat-form-field>
              <mat-form-field class="espp-form">
                <mat-label>購入日価格</mat-label>
                <input matInput formControlName="marketPrice"/>
              </mat-form-field>
              <mat-form-field class="espp-form">
                <mat-label>購入価格</mat-label>
                <input matInput formControlName="purchasePrice"/>
              </mat-form-field>

大きく2つが変更点になります

  • formディレクティブの中でformGroupをコンポーネント内で作ったesppFormに紐付け
  • 各inputディレクティブにてformControlNameを使ってそれぞれ値を紐付け

これにより各フォーム内に入力された値がesppFormというFormGroupで管理されるようになります。今回は入力された内容がwebページに表示されるように下記の変更も加えました。

    Value: {{ esppForm.value | json }}<br />

これらを全て組み合わせるとmain.component.htmlは以下の通りになります。

main.component.html
<div fxLayout="column">
  <div fxFlex>
    <form novalidate (ngSubmit)="add()" [formGroup]="esppForm">
      <div class="main-container" fxLayout="row" fxLayout.lt-sm="column" fxFlex>    
        <div class="form-container" fxFlex="60" fxFlex.lt-sm="grow" fxLayout="column">      
          <div class="form-row" fxLayout="row">
            <div class="form-box" fxFlex="50">
              <mat-form-field class="espp-form">
                <mat-label>銘柄</mat-label>
                <input matInput formControlName="name"/>
              </mat-form-field>
            </div>
            <div class="form-box" fxFlex="50">
              <span></span>
            </div>
          </div>
          <div class="form-row" fxLayout="row">
            <div class="form-box" fxFlex="50">
              <mat-form-field class="espp-form">
                <mat-label>購入日</mat-label>
                <input matInput [matDatepicker]="picker" formControlName="purchaseDate"/>
                <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
                <mat-datepicker #picker></mat-datepicker>
              </mat-form-field>
            </div>
            <div class="form-box" fxFlex="50">
              <mat-form-field class="espp-form">
                <mat-label>株数</mat-label>
                <input matInput formControlName="quantity"/>
              </mat-form-field>
            </div>
          </div>
          <div class="form-row" fxLayout="row">
            <div class="form-box" fxFlex="50">
              <mat-form-field class="espp-form">
                <mat-label>購入日価格</mat-label>
                <input matInput formControlName="marketPrice"/>
              </mat-form-field>
            </div>
            <div class="form-box" fxFlex="50">
              <mat-form-field class="espp-form">
                <mat-label>購入価格</mat-label>
                <input matInput formControlName="purchasePrice"/>
              </mat-form-field>
            </div>
          </div>
        </div>
        <div class="result-container" fxFlex="40" fxFlex.lt-sm="grow" fxLayout="column">
          <div class="result-box" fxFlex>為替レート1ドルXYZ円で計算し、利益はです。</div>
          <div class="result-box" fxFlex><button mat-raised-button color="primary">Primary</button></div>
        </div>
      </div>  
    </form>
  </div>
  <div fxFlex>
      <table mat-table [dataSource]="dataSource" class="mat-elevation-z8 result-table">
        <!--- Note that these columns can be defined in any order.
            The actual rendered columns are set as a property on the row definition" -->
        <!-- Position Column -->
        <ng-container matColumnDef="position">
          <th mat-header-cell *matHeaderCellDef>No.</th>
          <td mat-cell *matCellDef="let element">{{ element.position }}</td>
        </ng-container>
  
        <!-- Name Column -->
        <ng-container matColumnDef="name">
          <th mat-header-cell *matHeaderCellDef>Name</th>
          <td mat-cell *matCellDef="let element">{{ element.name }}</td>
        </ng-container>
  
        <!-- Weight Column -->
        <ng-container matColumnDef="weight">
          <th mat-header-cell *matHeaderCellDef>Weight</th>
          <td mat-cell *matCellDef="let element">{{ element.weight }}</td>
        </ng-container>
  
        <!-- Symbol Column -->
        <ng-container matColumnDef="symbol">
          <th mat-header-cell *matHeaderCellDef>Symbol</th>
          <td mat-cell *matCellDef="let element">{{ element.symbol }}</td>
        </ng-container>
  
        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
      </table>
  </div>
  <div fxFlex>
    Value: {{ esppForm.value | json }}<br />
  </div>
</div>

これにより、入力された値がFormGroupで管理され、その値がページ下部に表示されるようになります。

まとめ

ReactiveFormの一歩として、モジュールのインポートとFormGroupを作るところまで行いました。次回以降で、入力された内容によって画面の表示を変えたり、入力内容のバリデーションを行っていきたいと思います。今回のソースコードです。実際にコンポーネントのコードを変更して動きをみてみてください。

参考 今回のソースコード Angular Reactive Form その2 入力内容に合わせて処理を行う

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA