今回は、テンプレートベースの双方向バインディングではなく、Reactive Formを実装していく第一回目になります。
各種バージョンは以下のとおりです。
- Angular CLI: 11.2.8
- Node: 15.10.0
- Angular Material: 11.2.8
TOC
今回の目標とソースコードについて
今回の目標は、
- 入力された値をもとに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
の変更点は以下のとおりです。
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
です。
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 カスタムバリデーション