Angular Reactive Form その2 入力内容に合わせて処理を行う

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

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

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

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

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

今回の目標は、

  • 入力された値をもとに1ドル105円計算で、ESPP購入時の利益(給与所得)を表示する

です。

実際のところ双方向バインディングを使えばReactive Formsは今回のケースだと必要ないのですが、練習と思って簡単な実装をしていきます。

参考 今回のソースコード

フォームの入力をsubscribeする

フォームに入力されたことを契機として、処理を行いようにコンポーネントを実装します。

基本的にsubscribeに必要なことは以下のコードです。

export class MainComponent implements OnInit, OnDestroy {
  ngOnInit(): void {
    const quantityControl = this.esppForm.get("quantity");
    if (quantityControl) {
      quantityControl.valueChanges.subscribe(() => this.updateProfit());
    }
}

監視したいフォームをFormGroupのgetで取得し、valueChanges.subscribe()内に処理内容を記述します。今回はupdateProfit()という関数を呼び出しています。ちなみに、下記の通り1行で記述すると

this.esppForm.get("quantity").valueChanges.subscribe(() => this.updateProfit());

TS2531 Object is possibly 'null'で怒られるため、上記のように一度変数に入れて、ifでnullチェックしています。

フォームの入力が完了して少し待つ

このままだと、入力のたびに関数が呼ばれます。例えば、150とフォームに入力すると。

1
15
150

のようにフォームが変わるたびに呼ばれるため、ユーザの入力が完了していない状態で関数が呼ばれ続けます。それを防ぐために、入力が終了し、ある一定時間入力がない場合に処理を呼ぶようにできます。

先程のコードにdebounceTimeを追加して、以下のようにします。今回は 0.5秒待って処理を行うようにしました。

import { debounceTime } from "rxjs/operators";
export class MainComponent implements OnInit, OnDestroy {
  ngOnInit(): void {
    const quantityControl = this.esppForm.get("quantity");
    if (quantityControl) {
      quantityControl.valueChanges
        .pipe(debounceTime(500))
        .subscribe(() => this.updateProfit())
    }
}

OnDestroyでフォームの入力値をunsubscribe

仕上げとしてこのページから移動した際にこのsubscribeをリセットする処理を追加します。今回のようなシングルページのアプリケーションであれば、不要です。しかし、複数ページのアプリケーションを作成することを考えてみてください。別のページに移動したにもかかわらず、サブスクライブしたままだと不要なリソースを消費します。そののようなことを防ぐために、OnDestroy時にunsubscribeするようにします。

import { Component, OnDestroy, OnInit } from "@angular/core";
import { Subscription } from "rxjs";
import { debounceTime } from "rxjs/operators";

export class MainComponent implements OnInit, OnDestroy {
  private subscriptions = new Subscription();

  ngOnInit(): void {
    const quantityControl = this.esppForm.get("quantity");
    if (quantityControl) {
      this.subscriptions.add(
        quantityControl.valueChanges
          .pipe(debounceTime(500))
          .subscribe(() => this.updateProfit())
      );
    }
  }
  updateProfit(): void {
  }
  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();
  }
}

これでフォームの内容を監視して、処理を追加するところまでは完了です。

変更点とまとめ

今回アプリケーションに加えた変更点は以下のとおりです。

  • 株数、購入価格、購入日価格の入力をsubscribe
  • それらのどれかが変更された場合には、updateProfit()で利益を計算
  • 利益profiltを双方向バインディング
  • ngOnDestory()にてunsubscribe
  • 株数、購入価格、購入日価格の入力を数字に制限

まずは、main.component.htmlの変更点は以下のとおりです。

main.component.html
diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html
index 8d42c11..6b195be 100644
--- a/src/app/main/main.component.html
+++ b/src/app/main/main.component.html
@@ -26,7 +26,7 @@
             <div class="form-box" fxFlex="50">
               <mat-form-field class="espp-form">
                 <mat-label>株数</mat-label>
-                <input matInput formControlName="quantity"/>
+                <input matInput type="number" formControlName="quantity"/>
               </mat-form-field>
             </div>
           </div>
@@ -34,19 +34,19 @@
             <div class="form-box" fxFlex="50">
               <mat-form-field class="espp-form">
                 <mat-label>購入日価格</mat-label>
-                <input matInput formControlName="marketPrice"/>
+                <input matInput type="number" 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"/>
+                <input matInput type="number" 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>為替レート1ドル105円で計算し、利益は{{ profit | currency: 'JPY': '': '0.0-0' }}円です。</div>
           <div class="result-box" fxFlex><button mat-raised-button color="primary">Primary</button></div>
         </div>
       </div>

html側には今回は変更はほとんどありません。利益の表示とフォームを数字に制限したところが変更内容です。
続いてmain.component.tsです。

main.component.ts
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts
index 59b70d2..a3e324d 100644
--- a/src/app/main/main.component.ts
+++ b/src/app/main/main.component.ts
@@ -1,5 +1,7 @@
-import { Component, OnInit } from "@angular/core";
+import { Component, OnDestroy, OnInit } from "@angular/core";
 import { FormBuilder, FormGroup, Validators } from "@angular/forms";
+import { Subscription } from "rxjs";
+import { debounceTime } from "rxjs/operators";

 export interface PeriodicElement {
   name: string;
@@ -26,11 +28,13 @@ const ELEMENT_DATA: PeriodicElement[] = [
   templateUrl: "./main.component.html",
   styleUrls: ["./main.component.scss"],
 })
-export class MainComponent implements OnInit {
+export class MainComponent implements OnInit, OnDestroy {
   yen = 0;
-
+  profit = 0;
   esppForm: FormGroup;

+  private subscriptions = new Subscription();
+
   constructor(private fb: FormBuilder) {
     this.esppForm = this.fb.group({
       name: "",
@@ -44,8 +48,39 @@ export class MainComponent implements OnInit {
   displayedColumns: string[] = ["position", "name", "weight", "symbol"];
   dataSource = ELEMENT_DATA;

-  ngOnInit(): void {}
-  dollar(): number {
-    return this.yen / 105;
+  ngOnInit(): void {
+    const quantityControl = this.esppForm.get("quantity");
+    if (quantityControl) {
+      this.subscriptions.add(
+        quantityControl.valueChanges
+          .pipe(debounceTime(500))
+          .subscribe(() => this.updateProfit())
+      );
+    }
+    const marketPriceControl = this.esppForm.get("marketPrice");
+    if (marketPriceControl) {
+      this.subscriptions.add(
+        marketPriceControl.valueChanges
+          .pipe(debounceTime(500))
+          .subscribe(() => this.updateProfit())
+      );
+    }
+    const purchasePriceControl = this.esppForm.get("purchasePrice");
+    if (purchasePriceControl) {
+      this.subscriptions.add(
+        purchasePriceControl.valueChanges
+          .pipe(debounceTime(500))
+          .subscribe(() => this.updateProfit())
+      );
+    }
+  }
+  updateProfit(): void {
+    const quantity = this.esppForm.get("quantity")?.value || 0;
+    const marketPrice = this.esppForm.get("marketPrice")?.value || 0;
+    const purchasePrice = this.esppForm.get("purchasePrice")?.value || 0;
+    this.profit = quantity * (marketPrice - purchasePrice) * 105;
+  }
+  ngOnDestroy(): void {
+    this.subscriptions.unsubscribe();
   }
 }

フォームの入力を監視して、処理を行いコードが追加されています。

この状態でアプリケーションを起動し、数字を入力すると計算結果が表示されることがわかると思います。

ReactiveFormの強みでもある入力内容に合わせた動的処理を行いました。本来ならばフォームの見た目を変更したりと、もっと様々なことができるのですが、次回以降にみていきたいと思います。

次回は、入力内容のバリデーションです。

参考 今回のソースコード Angular Reactive Forms その3 カスタムバリデーション

コメントを残す

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

CAPTCHA