[Angular/TypeScript(JavaScript)] 非同期処理/待ち合わせ処理のまとめ (Observable/subscribe/forkJoin/Promise/async/await/then)

  • Angular等のTypeScriptベースのフレームワークでフロントエンドを開発する際には、待ち合わせ処理が度々問題となる
  • 外部通信が基本的に非同期処理であるため、返り値を加工するにはひと手間加える必要がある

前提知識

同期/非同期/待ち合わせ処理とは?

プログラムを書く前にどれが最適化か?を判断する必要がある(書き方が変わってくる)

  • 非同期処理

    • 実行完了を待たずに次の処理を並列で開始する
    • メリット
      • 処理の高速化を図れる。
      • 一部の処理でエラーが出ても、他の処理は問題無く動く
      • 処理完了までユーザーを待たせない
    • デメリット
      • 返り値を使って次の処理を行いたい時に問題が発生する
        (返り値無しの状態で次の処理を実行してしまう)
  • 同期処理

    • 処理完了まで他の処理をストップする
    • メリット
      • 処理を順に実行できるため、プログラムを書きやすい
    • デメリット
      • 処理完了までユーザーを待たせる事になる
  • 待ち合わせ処理

    • ある条件が満たされるまで処理を待たせる
      • ex. 返り値が戻ってくるまである処理の開始を待たせる
    • 同期処理とイコールではない
    • 複数の処理群としてみると非同期でOKだが、個々の処理は待ち合わせて順番に実行させたいケースがある
      • ユーザーが処理の完了を待つ必要がない → 非同期でOK
      • データ取得後に加工したい → 待ち合わせ処理が必要
  • TypeScript(JavaScript)においては、以下のような処理が非同期で実行されることを考慮する必要がある

    • API通信
    • データベース通信
    • その他の重い処理全般

TypeScript(JS)の待ち合わせ処理問題とは

上述の非同期で動く特性から、初級者は以下の問題でハマることが多い

  • TypeScript(JS)では、プログラムが書かれた順に動くとは限らない

    • 基本的に非同期処理で動く
      • 非同期処理は重い処理の終了を待たずに、次の処理を進められるので、高速化という意味では有効だが、困るシーンもある
  • よくあるケース

    • 外部APIと通信してデータを取得する場合、その戻り値が帰ってくる前に次の処理に進んでしまう。そのため、以降の処理がデータ無しで行われてしまう
    • 正しく外部にリクエストできている筈が、データ=undefinedと出力されてしまう
      • この辺りはChromeの開発モードで出力順を見ると理解し易い
  • 回避策: 待ち受け処理/同期処理を実現する(返り値が来た後に次の処理を実行させるように書き換える)
    • 書き方は色々ある
      1. Observable, subscibeを活用して待ち合わせ処理を実現する
      2. Observable, forkJoin()を活用して待ち合わせ処理を実現する
      3. aync/awaitを利用して同期処理化する
      4. Promise, then()を活用して同期処理化する

Observable型を使って待ち合わせ処理を実現するパターン

  • Angularでは適したケースであれば、基本的にPromiseよりもObservableが推奨されている

  • service側の返り値をobservable型に定義、component側で.subscribe()で受け取り、その中に処理を書き込む

  • ポイント

    • Observableでも待ち合わせ処理は実現できるが、同期処理はできない
    • 同期処理についてはPromiseを使う
    • ストリームを扱える

1. Observable & subscribe()

  • Service側

    • 返り値をObservable型で定義
  • Component側

    • .subscribe()で返り値を受け取る
  • sample.service.ts

    • Observable型で返り値を返す
      1
      2
      3
      4
      5
      6
      getmethod(){
      return observable = new Observable<number>(observer => {
      // 外部APIとの通信処理

      });
      }
  • sample.component.ts

    • 書式:observable.subscribe()
1
2
3
4
5
6

this.sampleService.getMethod().subscribe(value => {

// 返り値を利用する処理をここに書く

});

2. Observable & forkJoin()

  • forkJoin()を活用すると、Observableの全ての処理の完了を待って次の処理を実行できる

    • ユースケース
      • 複数回外部APIにリクエストを実行して、全てのデータが帰ってきてから加工
  • 書式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 複数の外部リクエストを実行してobservable型で受けとる
    // 配列hogehogeの中身を一つずつ引数として渡して実行する例
    const observable = hogehoge.map(hoge => {
    return this.sampleService.getData(hoge).pipe(catchError(e => observableOf({"error": e})))
    });

    // 待ち合わせ処理
    forkJoin(observables).subscribe( response => {
    // 以降に処理を記載
  • 活用例は以下の記事に記載した


Promise型を使って同期処理化するパターン

3. async/awaitを活用する

  • asyncをつけた関数の返り値はPromise型となる

  • asyncをつけた関数はawaitで待てる

  • await

    • Promiseの値が取り出されるまで待つ
  • async

    • awaitキーワードを使っている関数の頭に付ける必要がある
  • import必要?

  • ポイント

    • 同期処理化したい関数の宣言時:頭にasyncを付ける
    • 同期処理化したい関数の実行時:awaitを付ける
    • awaitはasyncを付けた関数内でしか実行できない

Serviceで外部API通信を実行 ⇒ Component側の変数に受け取る例

  • sample.component.ts
    • awaitを付けた関数の処理を待つ
      1
      2
      3
      4
      5
      6
      let result; // 返り値を受け取る変数
      async ngOnInit() {
      // Service経由で外部APIからデータを受け取る
      this.result = await this.sampleService.getMethod()
      console.log(this.getData);
      }
  • sample.service.ts
    • 関数の頭にasyncを付ける
      1
      2
      async getMethod(){
      }

複数の関数を順に実行する例

1
2
3
4
5
6
7
8
9
10
11
12
13
// プログラムの大筋
async function main() {
const x = await getX()
const y = await getY()
console.log(x + y)
}
async function getX() {
return 1
}
async function getY() {
return 2
}
main()

アロー関数の場合はasyncをつける箇所が異なる

  • Angularの場合は基本的にアロー関数が推奨されているのでこの書き方も覚えた方が良い
1
2
3
4
5
6
7
8
// functionによる関数宣言
async function sampleFunc() {
// 処理内容
}
// アロー関数による関数宣言
const sampleFunc = async() => {
// 処理内容
}

4. Promise, .then()を活用する

参考

関連記事

非同期処理参考

[Angular] E2E Testの自動化

E2Eテスト概要

  • Angularでは以下の二種のテストをngコマンドで実行可能(Protoractor/Karmaというテストを実行するためのフレームワークが標準で準備されている)
    • E2Eテスト
      • End to End(エンドツーエンド)テストの略
      • WEBブラウザー上でのテストを自動化できる
      • 画面毎のテスト
      • テストフレームワーク
        • Protoractor
      • コマンド
        • ng e2e
    • ユニットテスト
      • 関数ごとのテスト
      • テストフレームワーク
        • Karma
      • コマンド
        • ng test

今回は画面毎のテストを自動化したいので、Protoractorを活用してE2Eテストを実践する

  • E2Eテストまでの流れ
      1. 操作したい要素に識別子を付与
      • 要素(Ex. ボタン、入力フォーム)を判別可能にする
      1. テストを定義
      • テストコードを書く
    • マルブラウザ対応の設定をする
    • テストを実行

選択する要素に識別子を付与

E2EテストではAPの画面を自動操作する。
その際にどこを操作するか?(特定のボタン、フォームなど)を判別するために、各Componentのhtmlファイルで各要素に識別子を付与する必要がある。

  • 選択する要素を識別する識別子

    • data属性を使う
    • 書式
      • 以下を操作したい要素に定義していく
        1
        [attr.data-test]="'任意の識別子'"
      • ※要素を識別するために他に使用される属性として、id属性・class属性などもあるが、これらのはデザインの変更などで変更される恐れがあるため勧められていない。
  • 一般的なログイン画面で以下の要素に識別子を与えた例

    • ログインIDの入力欄
    • パスワードの入力欄
    • リクエストを送信するボタン
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<h1>login</h1>
<form>
<div>
<mat-form-field>
<input matInput placeholder="email" [attr.data-test]="'email'"> // ← 要素①: data-*属性で一意となる識別子を振る
</mat-form-field>
</div>
<div>
<mat-form-field>
<input type="password" matInput placeholder="passoword" [attr.data-test]="'password'"> // ← 要素②
</mat-form-field>
</div>
</form>
<div class="p-button-wrapper">
<button mat-raised-button class="p-button" color="accent" (click)="login()" [attr.data-test]="'login-button'">Login</button>  // ← 要素③
</div>

この後の工程で、これらの識別子に対するアクションを定義する。
上記の例であれば、IDとパスの識別子に対して入力値を与えて、ボタンの識別子に対して(click)アクションを実行させることになる。

## テストファイル

  • テストを定義するファイルは以下の二種類
    • \angular-pj\e2e\src\*.e2e-spec.ts
    • \angular-pj\e2e\src*.po.ts

Angular CLIでプロジェクトを始めると、上記のファイルが自動生成されているはず。

Page Object

  • アプリケーションの画面単位で1オブジェクトを定義するPage Objects としてE2Eテストが作成される
  • PageObjects
    • アプリの画面を1つのオブジェクトとして捉えるデザインパターン
  • PageObjectのメリット
    • もし、テストケースに直接セレクタを記述する場合、デザイン変更がにより、セレクタを記述している全テストケースの修正が必要
    • Page Objectsとして、.po.tsに各画面毎の要素選択などの処理を定義することで、.e2e-spec.tsは実際のテストケースを手続き的に書いていける
      • コードの可読性が向上
      • 再利用性が高まる

E2Eテストを実行

テストコードのより詳細な記述方法については別途まとめます

参考

関連記事

その他

[Angular] ng e2e Test 証明書エラーでハマった際の回避策(unable to get local issuer certificate))

AnglarのE2Eテストを会社の開発環境で試みたところ、証明書エラーでハマってしまった際のメモ

Error詳細

  • E2Eテストの実行時に以下の様なエラーが出る

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    \angular-pj> ng e2e
    [21:20:51] I/config_source - curl -oD:\angular-pj\node_modules\protractor\node_modules\webdriver-manager\selenium\chrome-response.xml https://chromedriver.storage.googleapis.com/
    events.js:174
    throw er; // Unhandled 'error' event
    ^

    Error: unable to get local issuer certificate
    at TLSSocket.onConnectSecure (_tls_wrap.js:1058:34)
    at TLSSocket.emit (events.js:198:13)
    at TLSSocket._finishInit (_tls_wrap.js:636:8)
    Emitted 'error' event at:
    at Request.onRequestError (D:\angular-pj\node_modules\request\request.js:877:8)
    at ClientRequest.emit (events.js:198:13)
    at TLSSocket.socketErrorListener (_http_client.js:392:9)
    at TLSSocket.emit (events.js:198:13)
    at emitErrorNT (internal/streams/destroy.js:91:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)
    at process._tickCallback (internal/process/next_tick.js:63:19)
  • webdrive-managerのアップデートにより解決した事例を真似てupdateを試みたものの、同様のエラーで進めない状況

    • Error: unable to get local issuer certificate
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      PS D:\angular-pj>  webdriver-manager update --version.chrome=86.0.4240.75
      webdriver-manager: using global installed version 12.1.7
      [21:24:30] I/config_source - curl -oD:\Users\XXXXXXXXX\AppData\Roaming\npm\node_modules\webdriver-manager\selenium\standalone-response.xml https://selenium-release.storage.googleapis.com/
      [21:24:30] I/config_source - curl -oD:\Users\XXXXXXXXX\AppData\Roaming\npm\node_modules\webdriver-manager\selenium\chrome-response.xml https://chromedriver.storage.googleapis.com/
      [21:24:30] I/config_source - curl -oD:\Users\XXXXXXXXX\AppData\Roaming\npm\node_modules\webdriver-manager\selenium\gecko-response.json https://api.github.com/repos/mozilla/geckodriver/releases
      events.js:174
      throw er; // Unhandled 'error' event
      ^

      Error: unable to get local issuer certificate
      at TLSSocket.onConnectSecure (_tls_wrap.js:1058:34)
      at TLSSocket.emit (events.js:198:13)
      at TLSSocket._finishInit (_tls_wrap.js:636:8)
      Emitted 'error' event at:
      at Request.onRequestError (D:\Users\XXXXXXXXX\AppData\Roaming\npm\node_modules\webdriver-manager\node_modules\request\request.js:877:8)
      at ClientRequest.emit (events.js:198:13)
      at TLSSocket.socketErrorListener (_http_client.js:392:9)
      at TLSSocket.emit (events.js:198:13)
      at emitErrorNT (internal/streams/destroy.js:91:8)
      at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)
      at process._tickCallback (internal/process/next_tick.js:63:19)

Protoractor/ng e2eについて基礎知識

  • Protoractorは内部的にSelenium WebDriverというライブラリを利用しており、入力やボタン操作などの操作の仕組みを標準で備えている

  • Protoractor(Webdriver)は単体では直接ブラウザーを操作できない

    • 内部的には、中計サーバーであるSelenium Serverを介して対象にアクセスする
      protoractor
  • directConnect

    • 有効化した場合、ChromeとFirefoxはSeleniumServerを立ち上げずにアクセスできる
  • angular-pj\node_modules\selenium-webdriver>

    • pj配下にseleniumは元から入っているように見受けられる

証明書エラー解決策: 証明書検証の回避

  • オプションにより証明書の検証を回避できる
    • –ignore_ssl
      1
      2
      angular-pj> .\node_modules\.bin\webdriver-manager update --proxy=XXXX --ignore_ssl
      angular-pj> ng e2e --no-webdriver-update

環境情報

  • package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"name": "XXXXXXXXX",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~8.2.13",
"@angular/cdk": "~8.2.3",
"@angular/common": "~8.2.13",
"@angular/compiler": "~8.2.13",
"@angular/core": "~8.2.13",
"@angular/forms": "~8.2.13",
"@angular/material": "^8.2.3",
"@angular/platform-browser": "~8.2.13",
"@angular/platform-browser-dynamic": "~8.2.13",
"@angular/router": "~8.2.13",
"hammerjs": "^2.0.8",
"jquery": "^3.4.1",
"ng-material-treetable": "^0.5.5",
"rxjs": "~6.4.0",
"tslib": "^1.14.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.803.29",
"@angular/cli": "^8.3.29",
"@angular/compiler-cli": "~8.2.13",
"@angular/language-service": "~8.2.13",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^8.10.64",
"aws-sdk": "^2.768.0",
"codelyzer": "^5.2.2",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.5.4",
"protractor": "^5.4.4",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.5.3"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
ng --version
> ng --version

_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/


Angular CLI: 8.3.29
Node: 10.16.3
OS: win32 x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package Version
-----------------------------------------------------------
@angular-devkit/architect 0.803.29
@angular-devkit/build-angular 0.803.29
@angular-devkit/build-optimizer 0.803.29
@angular-devkit/build-webpack 0.803.29
@angular-devkit/core 8.3.29
@angular-devkit/schematics 8.3.29
@angular/cdk 8.2.3
@angular/cli 8.3.29
@angular/material 8.2.3
@ngtools/webpack 8.3.29
@schematics/angular 8.3.29
@schematics/update 0.803.29
rxjs 6.4.0
typescript 3.5.3
webpack 4.39.2
npm
npm -v
6.9.0

その他

参考

関連記事

その他

[Typescript(JavaScript)] JSONデータの構文チェック & エラーハンドリング

WEBアプリにおいて、読み込んだJSONファイルの中身が破損していないかチェックする際のメモ。

JSON構文のチェック

Javascript(Typescript)においては、JSON.parse()によってチェック可能

  • MDN/JSON.parse()
    • 読み込んだデータがJSON出なければ、エラーが発生する

try catch構文でエラーハンドリング

  • catch{} にエラー発生時の処理を書けば良い
    1
    2
    3
    4
    5
    6
    try{
    JSON.parse(this.filesData[0]);
    } catch(e) {
    // ここにエラー時の処理を書く
    alert("JSONデータが不正です");
    }

参考

関連記事

その他

[Angular] (mouseover)/(mouseout)イベントで各ボタンの説明を表示

Angularにおけるマウスオーバーイベントについてのメモ

概要

Angularでは(click)イベントと同様に、要素に(mouseover)イベントを定めることで、マウスオーバー時に実行するメソッドを容易に指定できる

  • 書式
    1
    <div (mouseover)="sampleMethod()">

実装例

各ボタンの説明をマウスオーバー時のみ表示する

  • 実現方法

    • 説明を表示したい要素に以下のイベントを設定
      • (mouseover)
      • (mouseout)
    • イベント発生時に状態値を変更
    • ngIfで状態値に応じて説明を表示
  • 実装例

    • [Angular mat-tree] treeの全展開機能機能の実装で実装したボタンのホバー時に説明を表示するように改修
    • (mouseover)時に状態値:showExpandExplanationの値を変更 → ngIfを設定した要素が表示される
      1
      2
      3
      4
      5
      <button mat-icon-button (click)="treeControl.expandAll()" (mouseover)="showExpandExplanation=true" (mouseout)="showExpandExplanation=false"><mat-icon>unfold_more</mat-icon></button><!--全展開ボタン-->
      <div *ngIf="showExpandExplanation">全展開ボタン</div>

      <button mat-icon-button (click)="treeControl.collapseAll()" (mouseover)="showCollapseExplanation=true" (mouseout)="showCollapseExplanation=false"><mat-icon>unfold_less</mat-icon></button><!--全閉ボタン-->
      <div *ngIf="showCollapseExplanation">折り畳みボタン</div>

参考

関連記事

[Angular mat-tree] treeの全展開機能機能の実装

概要

  • 実装したい機能

    • 複数のtreeを一括で展開/折り畳みする機能
  • 実現方法

    • mat-treeにはデフォルトで全展開に利用可能なメソッドが用意されているため、それらを利用する
  • 全展開

    • expandAll() メソッド
  • 全閉

    • collapseAll() メソッド
  • 全展開機能の書式

    1
    tree.treeControl.expandAll();

全展開/全閉ボタンの実装

1
2
3
4
5
<button (click)="tree.treeControl.collapseAll()">collapse All</button>
<button (click)="tree.treeControl.expandAll()">expand All</button>
<mat-tree #tree [dataSource]="dataSource" [treeControl]="treeControl">
...
<mat-tree>
  • mat-iconを利用した例
    1
    2
    <button mat-icon-button (click)="treeControl.expandAll()"><mat-icon>unfold_more</mat-icon></button><!--全展開ボタン-->
    <button mat-icon-button (click)="treeControl.collapseAll()"><mat-icon>unfold_less</mat-icon></button><!--全閉ボタン-->

参考

関連記事

その他

[Angular] データをファイルで切り出しimportする

Angular公式のCoding Guideを読む中で知ったのですが、データをComponentのtsファイル内で宣言するのはアンチパターンで、極力ファイルとして外だしするのがベストプラクティスだそうです。データファイルをComponent側から参照するまでのメモです

想定する状況

  • 元データ
    • componentのtsファイル内でハッシュを宣言しているが、他のcomponentからも参照できるようにファイルとして外だしする
    • 以下はcheckbox一覧として使用するデータの例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
regionsData = [ // 個々のselectedプロパティでチェック状態を保持
{ value: 'us-east-1', selected: false},
{ value: 'us-east-2', selected: false},
{ value: 'us-west-1', selected: false},
{ value: 'us-west-2', selected: false},
{ value: 'ca-central-1', selected: false},
{ value: 'eu-central-1', selected: false},
{ value: 'eu-west-1', selected: false},
{ value: 'eu-west-2', selected: false},
{ value: 'eu-west-3', selected: false},
{ value: 'eu-north-1', selected: false},
{ value: 'ap-northeast-1', selected: false},
{ value: 'ap-northeast-2', selected: false},
{ value: 'ap-northeast-3', selected: false},
{ value: 'ap-southeast-1', selected: false},
{ value: 'ap-southeast-2', selected: false},
{ value: 'ap-south-1', selected: false},
{ value: 'me-south-1', selected: false},
{ value: 'sa-east-1', selected: false},
];

ファイルにデータを切り出す

  • ファイルを作成
    • componentと同様のディレクトリに格納
  • regions-data.ts
    • exportで他のcomponentからもアクセス可能な変数として宣言
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      export const regionsData = [
      { value: 'us-east-1', selected: false},
      { value: 'us-east-2', selected: false},
      { value: 'us-west-1', selected: false},
      { value: 'us-west-2', selected: false},
      { value: 'ca-central-1', selected: false},
      { value: 'eu-central-1', selected: false},
      { value: 'eu-west-1', selected: false},
      { value: 'eu-west-2', selected: false},
      { value: 'eu-west-3', selected: false},
      { value: 'eu-north-1', selected: false},
      { value: 'ap-northeast-1', selected: false},
      { value: 'ap-northeast-2', selected: false},
      { value: 'ap-northeast-3', selected: false},
      { value: 'ap-southeast-1', selected: false},
      { value: 'ap-southeast-2', selected: false},
      { value: 'ap-south-1', selected: false},
      { value: 'me-south-1', selected: false},
      { value: 'sa-east-1', selected: false},
      ];

Componentからファイルをimport

  • sample.component.ts
    • ファイルからimport
      1
      2
      3
      4
      5
      // ファイルをimport
      import { regionsData } from './regions-data';
      export class SampleComponent implements OnInit {
      // component内で扱う変数にimportしたデータを格納
      regionsData = regionsData;

参考

関連記事

[Angular Service入門] ロジックを切り出し、複数Componentで再利用可能にする

Angularでは基本的にComponent単位で開発を行いますが、複数のComponentで使い回す機能については”Service”という形で切り出すのが一般的です。
初めは各Componentのtsファイルに機能をそのまま書いてしまうと思います。そこから切り出す過程と注入(Injection)の手順や、Serviceについて解説します。

AngularにおけるServiceという概念

About Angular Service

  • 機能をサービスクラス(tsファイル)として切り出し、複数のComponentから使用させることが可能

    • Serviceから各Componentに”依存性注入(Dependency Injection, DI)”とを行うことでServiceの中の関数を使用できる
  • 一般的なAngularの開発

    • Component側にはビュー(見栄え)に関するロジックのみを書く
    • アプリ固有のビジネスロジックはService側に全て切り出し、Componentはそれを呼ぶだけ
      • こうすることで、各ファイルがコンパクトになり可読性が大きく向上する

ロジックのサービス分割手順 日時取得機能の例

想定する状況

  • Componentのtsファイルに、以下の様な日時を取得してフォーマットを整形するロジックを書いていたとする
    • 他のComponentでもこの機能を使い回せるように、Serviceとして切り出す
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// --日時データを取得--
// Dateオブジェクトの作成
const now = new Date();
// 各日時要素を取得
const year = String(now.getFullYear()); // 年
// 1月=0と出るため+
const month = String( ("00" + (now.getMonth() + 1)).slice(-2)); // 月
const date = String( ("00" + now.getDate()).slice(-2)); // 日
const hour = String( ("00" + now.getHours()).slice(-2)); // 時
const min = String( ("00" + now.getMinutes()).slice(-2)); // 分
const sec = String( ("00" + now.getSeconds()).slice(-2)); // 秒
// YYYYMMDDHHMMSS形式にまとめる
const time = year + month + date + hour + min + sec;
return time;
  • まずは関数として切り出してみる
    • Component内で関数として切り出せる機能=Service内の関数として切り出し可能
    • 以下のgetTime()がServiceからの関数呼び出しに変わるイメージ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cost t = getTime();

...中略...

getTime() { // ----ファイル名のprefixに付ける日時データを取得---
// Dateオブジェクトの作成
const now = new Date();
// 各日時要素を取得
const year = String(now.getFullYear()); // 年
// 1月=0と出るため+
const month = String( ("00" + (now.getMonth() + 1)).slice(-2)); // 月
const date = String( ("00" + now.getDate()).slice(-2)); // 日
const hour = String( ("00" + now.getHours()).slice(-2)); // 時
const min = String( ("00" + now.getMinutes()).slice(-2)); // 分
const sec = String( ("00" + now.getSeconds()).slice(-2)); // 秒
// まとめる YYYYMMDDHHMMSS
const time = year + month + date + hour + min + sec;
return time;
}

Serviceを作成

  • ng gコマンドで雛形を自動生成可能
    • ~~.spec.tsはテスト用のファイルなので一旦スルーしてOK
      1
      2
      3
      \src\app\service> ng g service "get-time"
      CREATE src/app/service/get-time.service.spec.ts (339 bytes)
      CREATE src/app/service/get-time.service.ts (136 bytes)

Serviceの実装

  • 先ほどの関数をService側に転記
  • Component側のgetTime()関数は消す

get-time.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class GetTimeService {
constructor() { }
// 日時を取得整形する関数
getTime() {
// Dateオブジェクトの作成
const now = new Date();
// 各日時要素を取得
const year = String(now.getFullYear()); // 年
// 1月=0と出るため+
const month = String( ("00" + (now.getMonth() + 1)).slice(-2)); // 月
const date = String( ("00" + now.getDate()).slice(-2)); // 日
const hour = String( ("00" + now.getHours()).slice(-2)); // 時
const min = String( ("00" + now.getMinutes()).slice(-2)); // 分
const sec = String( ("00" + now.getSeconds()).slice(-2)); // 秒
// まとめる YYYYMMDDHHMMSS
const time = year + month + date + hour + min + sec;
return time;
}
}

Serviceの注入(DI)

Component側でServiceの関数を利用可能にする。以下はcomponentのtsファイルの話

  • serviceをimport
    1
    import { GetTimeService } from '../../service/get-time.service'; // get time logic
  • 依存性注入
    • ここで宣言したtimeServiceという変数にServiceの中身が入るイメージ
    • ”timeService.使いたい関数名”でService内の関数を呼び出せる
      1
      2
      3
      constructor(
      private timeService: GetTimeService,
      ) {

      ComponentからServiceの関数を使用する

  • constructreで宣言したservice名.関数名で実行
    1
    2
    3
    4
    // 日時取得&整形ロジックの利用
    const time = this.timeService.getTime();
    // 結果を確認
    alart(time);

参考

関連記事

[Angular ReactiveForm x mat-input] 一つの入力欄のエラーメッセージを出し分ける

  • やりたいこと
    • バリデーションで引っかかった内容毎にエラーメッセージを出し分けて、何を直すべきか?をユーザにリアルタイムで教えたい
    • 入力値に不備がある状態で、ユーザにリクエストをさせない、できない仕組みを作る
  • 同一のフォームで複数のバリデーションルール毎にエラーメッセージを切り替えるには、テンプレート駆動フォームではなく”リアクティブフォーム”の利用が必要

Angularのフォーム テンプレート駆動/Reactive(モデル駆動)

  • フォームの種類
    • テンプレート駆動フォーム
      • フォームの検証ルールをテンプレートとなるhtmlファイルに記載
      • 入力コントロール(Ex. <input>)に属性を付けると、それに対応したFormControlオブジェクトやFormGroupオブジェクトが生成される
      • バリデーション
        • 属性で指定(Ex. required)することで、該当するオブジェクトに適応される
      • 仕組み
        • コンポ―ネントから、オブジェクトにアクセス
    • Reactive(モデル駆動)フォーム
      • テンプレート側で
      • ts側に記載
      • メリット
        • 柔軟なバリデーション、入力されたデータの複雑な制御を実現可能
      • デメリット
        • コードが冗長になりがち
      • 使いどころ
        • 同一のフォーム内で複数のバリデーションルール毎にエラーメッセージを切り替えたいケース
          • Ex. 未入力 or 文字数の不足 or 禁止された入力値の型(大文字禁止なのか等)をユーザにリアルタイムで伝えることでUXの向上を図れる
      • 仕組み
        • コンポーネントにあらかじめFormControlオブジェクトやFormGroupオブジェクトを作っておく
        • テンプレートの入力コントロール(Ex. <input> 要素)からそれらのオブジェクトを参照

実装手順

  • sampleというコンポーネントがあるものとする​
    • sample.component.html
    • sample.component.ts

      モジュールの有効化

  1. app.component.module.tsでReactiveFormsモジュールをimport
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Reactive form
    import { ReactiveFormsModule } from '@angular/forms'
    // テンプレート駆動型と同様にFormsModuleも必要

    ~略~

    importts:[

    FormsModule,
    ReactiveFormsModule // 追加
    ]
  2. 対象のコンポーネントでも必要なモジュールをimport
  • sample.component.ts
    1
    import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms'
  • FormBuilder
    • 必須ではないが、これを利用すると短く楽に記述できる

      実装例

  • Formbuilderを利用する事で完結に記述した例
  • ID x Password型のシンプルなフォームは基本以下でよさそう
    • 時間を作れればSchematicsで自動生成できるようにしておきます

      #### sample.component.ts
  • Formbuilderをconstructorで注入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 1. import module
    import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms'

    export class SampleComponent implements OnInit{

    // 2.1. FormGroup参照用の変数を宣言
    credentialForm:FormGroup;

    // 2.2. FormControl参照用の変数を宣言
    id = new FormControl("", [
    Validators.required,
    Validators.minLength(20)
    ]);
    pass = new FormControl("", [
    Validators.required,
    Validators.minLength(40)
    ]);

    // 3. Formbuilderの注入 FormBuilderオブジェクトを生成
    constructor(private fb: FormBuilder) {
    // 4. FormBuilderオブジェクトを使ってFormGroupとFormControlを作成
    this.credentialForm = this.fb.group({
    // formControlと初期値、バリデート条件を列挙
    id: this.id,
    pass: this.pass
    })
    }

    // リクエスト実行時に入力値を参照するサンプル
    submit(){
    // 入力値の参照
    id = this.id.value.id;
    pass = this.pass.value.pass;
    this.sampleService(id, pass); // 認証機能を持つServiceに送る
    }
    }
  • 入力値の参照
    • this.formControlname.valueで取得可能になっている
      1
      2
      this.id.value;
      this.pass.value;

      sample.component.html

  • 入力コントロールとfonrControlオブジェクトの連携、バリデーションに応じたエラーメッセージの表示
    • maxlengthはhtml側に設定すべき
      • ts側でも検知はできたが、規定した時数以上入力を不可にすることはできなかった為
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        <!-- 1. formGroupの設定 -->
        <form [FormGroup] = "credentialForm">

        <mat-form-field>
        <mat-label>accessKeyId</mat-label>

        <!-- 2. FormControlの結びつけ -->
        <input matInput
        formControlName="id"
        placeholder="Ex. XXXXX..."
        maxlength="20">

        <!--&&条件を付けないとエラー解消後もメッセージが残ってしまう為注意-->
        <!-- 3.1. エラーメッセージ 空欄,未入力時 -->
        <mat-error *ngIf="id.invalid && id.invalid.required">入力必須</mat-error>
        <!-- 3.2. エラーメッセージ 文字数不足時 -->
        <materror *ngIf="id.invalid && id.errors.minlength">字数不足</mat-error>

        <!-- 3. FormControlの結びつけ -->
        <mat-form-field>
        <mat-label>Password</mat-label>
        <input matInput
        formControlName="pass"
        placeholder="Ex. FdOI0..."
        maxlength="40"
        [type]="hide ? 'password' : 'text'"
        >
        <!---pattern="[a-zA-Z0-9!-/:-@¥[-`{-~]*"--->
        <!--目のアイコンの押下で入力値の表示を切り替え--->
        <button mat-icon-button
        matSuffix
        (click)="hide = !hide"
        [attr.aria-label]="'Hide password'"
        [attr.aria-pressed]="hide"
        >
        <mat-icon>{{hide ? 'visibility_off' : 'visibility'}}</mat-icon>
        </button>
        <mat-error *ngIf="pass.errors.required">入力必須</mat-error>
        <mat-error *ngIf="pass.errors.minlength">字数不足</mat-error>
        </mat-form-field>

        </form>

  • 送信ボタンの無効化
    • 一つでもバリデーションエラーがあればdisabledで無効化
    • formGroup名.invalid
      • フォームグループ内で一つでもバリデーションエラーがあればtrue
        1
        2
        3
        4
        <button 
        (click)="submit()"
        [disabled]="credentialForm.invalid"
        >送信</button>
  • 他のフォームのバリデーションチェックも条件に加える場合
    • ||でor条件を利用する
      1
      2
      3
      4
      <button mat-raised-button
      (click)="submit()"
      [disabled]="credentialForm.invalid || checkValidation"
      >Get Data</button>

備考

その他

[Angular mat-input] passwordの表示/非表示(*)を切り替えるアイコンを実装

やりたいこと: パスワードのマスク(***と出る)を目アイコンでON/OFF

  • type=passwordで入力値が**で表示される

    • 覗き見を防止するための基本的な仕様だが、入力値の確認も可能にしたい

      Passwordフォームイメージ
  • やり方

    • html側
      • type=変数
        • 条件分岐で ‘password’ : ‘text’に切り替わる
      • 変数の初期値はpassword
      • 入力欄に目アイコンのボタンを追加
        • mat-iconのvisibility、visibility_offを使用
        • matSuffixでbuttonを入力欄の右端に
      • 押下時に入力値が見えるようにする
    • ts側
      • 条件分岐用に変数を定義
        1
        hide = true;

        ‐ Angularにおける変数の条件分岐の書き方
    • 以下は変数hideの値がtrueかfalseかで、?以降の:で区切られた値が採用されるという意味
      1
      [type]="hide ? 'password' : 'text'"

      実装例

  • htmlの実装

    • input内のtype属性
      • 変数hideの値による条件分岐でpassword or textに切り替わる
      • つまり、表示/非表示(*)が切り替わる
    • buttonを作成
      • mat-iconで目のアイコン(visibility, visibility_off)に
      • mat-suffixで右端に
      • (click)時にhideの値を変更
        • (click)=”hide = !hide”
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
                <mat-form-field class="mat-input-margin2">
          <mat-label>secretAccessKey</mat-label>
          <input matInput
          id="secretAccessKey"
          skey="secretAccessKey"
          [(ngModel)]="skey"
          minlength="40"
          maxlength="40"
          pattern="[a-zA-Z0-9!-/:-@¥[-`{-~]*"
          placeholder="Ex. FdOI0..."

          [type]="hide ? 'password' : 'text'"

          required
          #secretAccessKey="ngModel"
          >
          <!--目のアイコンの押下で入力値の表示を切り替え--->
          <button mat-icon-button
          matSuffix
          (click)="hide = !hide"
          [attr.aria-label]="'Hide password'"
          [attr.aria-pressed]="hide"
          >
          <mat-icon>{{hide ? 'visibility_off' : 'visibility'}}</mat-icon>
          </button>

          <mat-error>入力必須</mat-error>

          </mat-form-field>
  • 公式のコードサンプル(stackblitz)

参考

​関連記事

その他

@EventEmitter @Input @Output @ViewChild ACM AMP API Gateway AR AR.js AR.js Studio AWS AWS Amplify AWS Budgets AWS Cost Explorer AWS SDK for JavaScript AddThis Adobe XD Alexa Amazon CloudWatch Amazon Honycode Amazon SNS Android Angular Angular Material AngularFire AppSync Augury C CDN CI/CD Cloud Craft Cloud9 CloudFormation CloudFront CloudTrail Cognito Cost Anomaly Detection Cubase ai Cubasis LE DTM Disqus DynamoDB Elgato HD 60S Firebase Firebase Hosting Former2 Github Github Actions Github.com GithubEnterprise GithubPages Google Chrome Google Cloud Shell GraphQL Hexo Hosting IAM Ionic JSON JavaScript LadioCast Logging LowCode MFA MS Authentication MacBook Pro 16 Mind Node NETDUETTO Netflix Party Netlify Network NoCode Observable PO PdM Promise RPA ReactiveForm Recognition Route53 S3 SAM(Serverless Application Model) SAR SSL SYNCROOM Schematics ScrumInc Serverless Service Siri Sitemap Spark AR Steinberg UR44C Teams Touch Cast Studio Treetable TypeScript UI UI Bakery WAF WAFv2 WEB Page Dev WEB会議 WebAR WebSocketAPI Webhook Windows Power Automate Wireshark aot async/await aws config cloud9 display.land draw.io e2e test filter() forkJoin() google search console hexo-generator-amp iOS iPad Pro iPhone icarus map() mat input mat tree mat-checkbox mat-input mat-selection-list mmhmm ngFor plantUML popIn Aladdin2 then() vscode ”global is not defined” らくがきAR アジャイル アジャイル開発 クロスプラットフォーム ショートカット スクラム スクラム開発 テレワーク ファイル操作 ブラウザ型IDE プロダクトオーナー プロダクトマネージャー プロトタイピング リモートセッション 共同開発 双方向データバインディング 待ち合わせ処理 認定スクラムマスター 静的WEB Hosting 静的WEBサイトHosting
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×