Help us understand the problem. What is going on with this article?

今日からはじめるAngular

この記事では、Web アプリケーション開発のための JavaScript フレームワークであるAngularについて説明します。

Angular とは

Angular(アンギュラー)は Google 社によって開発されているクライアントサイド向けJavaScriptフレームワークです。
フルスタック(全部乗せ)なフレームワークを指向しており、Web アプリケーション開発に必要な機能のほぼ全てをサポートしています。
Angular CLI(Command Line Interface)を使って、プロジェクトの生成からビルド、デプロイまで実行することができます。

現在(2019 年 7 月)の最新バージョンは v8.0.0(メジャーバージョン)です。Google は半年ごとのバージョンアップを宣言しており、2019 年 10 月に v9.x、2020 年 5 月に v10.x のアップデートが予定されています。
Angular の開発初期(v1.x)では「AngularJS」というプロダクト名で開発されていましたが、v2.x にアップデートされたときにアーキテクチャの抜本的な見直しが図られ、名前も「Angular」に改められました。
AngularJS では JavaScript をベースとしたフレームワークでしたが、Angular では TypeScript ベースに改められ、文法も刷新されたため、AngularJS と Angular には互換性がありません。
※ネット上には AngularJS 時代の情報がたくさん転がっています。惑わされないように。。。

Angular は Node.js 環境下で開発を行います。これは Angular の開発サポート機能自体が TypeScript(をコンパイルした JavaScript)で記述されているためです。
また、Angular 自体は npm を使用してインストールすることができます。
Node.js は v8.x 以上、npm は v5.x 以上が必要です。バージョンが古いとエラーになるため注意が必要です。

インストール

Angular のインストールはすなわち Angular CLI のインストールです。
Angular CLI を利用することで、Angular プロジェクトを生成し、フレームワークの様々な恩恵を享受することができるようになります。

npm install @angular/cli -save

プロジェクトの生成

Angular プロジェクトを生成するには、Angular CLI から以下のコマンドを実行します。
コマンドを実行したカレントディレクトリに新しいサブディレクトリが作られ、その中にアプリケーションを構成するソースが格納されます。
このとき、Angular が依存する外部ライブラリも自動でインストールされます。

ng new <プロジェクト名>

※「ng」は Angular CLI を呼び出すコマンドですが、Angular を示すワードとして色々なところで頻出します。

angular-project.PNG

生成されたプロジェクトフォルダには、Angular プロジェクトであることを示す angular.json に加え、TypeScript プロジェクトであることを示す tsconfig.json などが含まれます。

Angular 自身のモジュールや外部ライブラリなどは node_modules に格納されます。
実際にアプリケーションのソースとなるのは src フォルダです。

/
├ node_modules/
└ src/
 ├ app/
 │ ├ app.component.html
 │ ├ app.component.css
 │ ├ app.component.ts
 │ ├ app.component.spec.ts
 │ └ app.module.ts
 ├ assets/
 ├ environments/
 ├ dist/
 ├ index.html
 ├ main.ts
 ├ styles.css
 └ etc...
要素 機能
node_modules/ npm パッケージを格納するフォルダ。
src/ プロジェクトの資源を格納するフォルダ。
src/index.html メイン HTML ページ。
src/main.ts アプリケーションのルートスクリプト。
src/styles.css メインスタイルシート。
src/app/ アプリケーションのソースを格納するフォルダ。
src/app/app.component.html index.html から読み込まれるルートコンポーネント。
src/app/app.component.css ルートコンポーネントのスタイルシート。
src/app/app.component.ts ルートコンポーネントのスクリプト。
src/app/app.component.spec.ts ルートコンポーネントのユニットテストスクリプト。
src/app/app.module.ts root モジュールのスクリプト。
assets/ 画像ファイル、外部ファイル等、コンパイルせずに使う資源を格納するフォルダ。
environments/ 本番用、開発用等のビルド設定を格納するフォルダ。
dist/ コンパイル済みプロジェクトを格納するフォルダ。

アプリケーションの実行

開発中の動作確認も Angular CLI からコマンドで簡単に行うことができます。
Angular プロジェクトのルートフォルダで実行する必要があります。

ng serve --open

--openオプションを付けることで、ビルド完了後に自動でブラウザを開いてくれます。
ng serve の内部では webpack の開発用仮想サーバ機能(webpack-dev-server)を利用しており、ホットデプロイ(ソースの変更を検知して自動で再読み込みしてくれる機能)をサポートしているため、ソースの修正結果がそのままブラウザへ反映されます。

アプリケーションのビルド

Angular ソースを実際にアプリケーションとして運用する際には、コンパイルを行う必要があります。
以下のコマンドを実行すると、内部的に webpack の処理が走り、コンパイルおよびバンドルされたソースが生成されます。
Angular プロジェクトのルートフォルダで実行する必要があります。

ng build --configuration=production

--configurationオプションでは、ビルドを行う環境設定を指定することができます。
一般的には、production、staging、development などでそれぞれ環境設定を定義しておきます。
production ビルドに限り、--prodのショートハンドで指定することができます。
ビルド環境やソースの出力先などの設定については、angular.json 内で定義することができます。

コンポーネント

Angular はコンポーネント指向のフレームワークです。
コンポーネントとは、特定の画面であったり、共通の部品であったり、何かしら「ある機能を持った一定のひとかたまり」のことを言います。
別々のコンポーネントを適宜組み合わせて画面を構成するのが Angular の Web アプリケーションです。
(例えば、ヘッダコンポーネントとナビゲーションコンポーネントとメインコンテンツコンポーネントを組み合わせて画面を構成するなど)
コンポーネントを作成するには、Angular CLI で以下のコマンドを実行します。
生成されるコンポーネントのソースは、同名のフォルダにまとめられた状態で作られます。

ng generate component hoge

コマンドを実行すると、ソースコードが生成されます。

CREATE src/app/hoge/hoge.component.html (19 bytes)
CREATE src/app/hoge/hoge.component.spec.ts (614 bytes)
CREATE src/app/hoge/hoge.component.ts (262 bytes)
CREATE src/app/hoge/hoge.component.css (0 bytes)
UPDATE src/app/app.module.ts (467 bytes)

「ng generate」はソースファイルを作るだけでなく、新しく作り出したコンポーネントをモジュールに登録してくれます。
Angular がコンポーネントを取り扱うためには、どこかのモジュールでコンポーネントを宣言しておく必要があります。
Angular におけるモジュールについては後の章で説明します。

 └ app/
  └ hoge/
   └ hoge.component.html
   └ hoge.component.css
   └ hoge.component.ts
   └ hoge.component.spec.ts

Angular は、Model-View-ViewModel からなるMVVMパターンを踏襲したフレームワークです。

  • Model  アプリケーションのコアとなるドメインロジック(ユーザからは見えない)
  • View  ブラウザ上でユーザと対話するロジック(ユーザから見える)
  • ViewModel   Model と View の橋渡しを行うロジック

各コンポーネント単位は View-ViewModel レイヤーを担います。
生成されたファイルのうち、.html ファイルおよび.css ファイルが View、.ts ファイルが ViewModel にあたります。

コンポーネントの利用

生成したコンポーネントをアプリケーションに組み込むには、いくつか方法があります。
基本的なものは、以下の 3 種類です。

  • HTML(ビュー)の中でコンポーネントセレクタを記述する
  • ルーティングで遷移する
  • アプリケーションのスクリプトで動的にコンポーネントを生成する

コンポーネントセレクタを記述する場合

Angular コンポーネントの TypeScript クラスファイル(.ts ファイル)には、@Componentデコレータを記述し、その中にコンポーネントのメタ情報(コンポーネント自身の情報)を記述します。

@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css']
})
要素 備考
selector コンポーネントセレクタ(Angular の View 上でそのコンポーネントを示す名前)
templateUrl コンポーネントの View 構造を記述したファイル(HTML)
styleUrls コンポーネントの View スタイルを記述したファイル(CSS)

templateUrl、styleUrls は「template」「style」として HTML や CSS を.ts ファイル上に直接記述することもできます。
しかし、責務の分割の観点から、なるべく避けた方がよいでしょう。

上記の例のコンポーネントは、Angular の View において app-hoge タグを記述して利用することができます。
当然ながら、app-hoge タグは HTML 標準ではなく、本来ブラウザが解釈できるタグではありません。
あくまで Angular が独自に HTML を拡張したタグであり、実際のアプリケーションでは Angular によってブラウザが解釈できる形に変換されます。

<app-hoge></app-hoge>

ルーティングで遷移する場合

通常、Web ページ上のハイパーリンクをクリックなどすることで、別の Web ページに遷移できます。
同時に、ブラウザのヒストリにページの履歴が記録され、「戻る」ボタン、「進む」ボタンで移動することができます。

(Angular に限らずですが)SPA においては、「別の Web ページに遷移」という概念をほとんど持ちません。
リクエストされた URL に対応するコンポーネントを、現在表示しているコンポーネントと入れ替えることで別の Web ページに遷移したように見せています。
同時に、JavaScript でブラウザのヒストリを書き込んでいくことで、通常の画面遷移と同じように「戻る」ボタン、「進む」ボタンを使うことができます。
これをルーティングといい、ルーティングを行うためのモジュールをルータといいます。

Angular Router はフレームワークの中でも重要な機能であるため、後の章で説明します。

動的にコンポーネントを生成する場合

スクリプトを記述することで動的にコンポーネントを生成することもできます。
(たとえば、ボタンを押したときに開くダイアログなどのユースケースが考えられます。)
コンポーネントの生成には、以下の 3 つのクラスを利用します。

  • ComponentFactoryResolver コンポーネントクラスから ComponentFactory を特定するクラス
  • ComponentFactory コンポーネントを生成するクラス
  • ViewContainerRef  View への参照を提供するクラス

コンポーネントクラスをブラウザで見える形に変換するのは ComponentFacroty の役目です。
しかしながら、Angular アプリケーションには無数のコンポーネントが含まれており、対応する ComponentFactory インスタンスも無数に存在しています。
その数多くの ComponentFactory インスタンスの中から、ほしいコンポーネントに対応した Factory を見つけ出すのが ComponentFactoryResolver の役目です。
そして、ComponentFactory によって生成されるコンポーネントは、View にセットされることでブラウザ上に現れます。
View へアクセスするためのメソッドを提供するのが ViewContainerRef です。

実際のコンポーネントを生成する際の流れは以下のようになります。

  1. ComponentFactoryResolver のresolveComponentFactoryメソッドにコンポーネントクラスを渡して、対応する ComponentFactory を得る
  2. ViewContainerRef のcreateComponentメソッドに ComponentFactory を渡して、View にコンポーネントを生成する

Factory 自体に触ることはありません。
ViewContainerRef 自体にコンポーネントを生成するメソッドがあり、そのまま Factory を渡してしまえば ViewContainerRef がすべてよしなにやってくれるのです。

ただし、動的なコンポーネントの生成はスクリプトロジックがやや煩雑になりがちです。
特に制約がなければ、Angular Material の MatDialogModule を利用するのがよいでしょう。

サービス

Angular は、特定のロジックをサービスとしてコンポーネントと切り分けることができます。
サービスとして別クラスに取り出した機能は、複数のコンポーネントから呼び出して共有することができます。
これにより、コンポーネントの責務を最小限にすることができ、保守性が向上します。
また、ビジネスロジックが特定のコンポーネントに結びつくことを防ぎ、再利用性を高めることができます。

Angular は依存性注入(DI:Dependency Injection)をサポートする DI コンテナでもあります。
フレームワークにインジェクタという機構を持っており、サービスの利用者(コンポーネントクラスまたはサービスクラス)の要求に応じて、外部からサービスを注入(インジェクション)できる仕組みとなっています。
サービスの利用者は、使いたいサービスをコンストラクタで宣言しておけば、クラスの中でサービスをインスタンス化せずにサービスの機能を利用することができます。

サービスを作成するには、Angular CLI で以下のコマンドを実行します。

ng generate service hoge
CREATE src/app/hoge.service.spec.ts (323 bytes)
CREATE src/app/hoge.service.ts (133 bytes)
 └ app/
  └ hoge.service.ts

サービスの利用

サービスを利用するには、まずサービスを注入するインジェクタにサービスの作成方法を教える必要があります。
インジェクタに教えるサービスの作成方法は、providersメタ情報としてサービスごとに定義します。
下記の例では、import した HogeService を@Component デコレータの providers で指定することで、インジェクタが HogeService をインスタンス化して注入できるようになります。

src/app/hoge/hoge.component.ts

import { Component } from '@angular/core';
import { HogeService } from '../hoge.service';

@Component({
  selector: 'app-hoge',
  templateUrl: './hoge.component.html',
  styleUrls: ['./hoge.component.css'],
  providers: [HogeService]
})
export class HogeComponent {
  constructor(private hoge: HogeService) {
    this.hoge.out();
  }
}

src/app/hoge.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class HogeService {
  public out(): void {
    console.log('this is HogeService.');
  }
}

インジェクタはアプリケーションの階層ごとに存在します。
つまり、providers メタ情報をどこで定義するかによってサービスを利用できるスコープが変化します。
上記の例の場合はコンポーネントレベルで定義しているので、HogeService の作成方法を知っているインジェクタはこのコンポーネントのインジェクタのみとなり、他のコンポーネントは HogeService を利用することができません。

サービスを利用するレベルのインジェクタがサービスを注入できない場合、先祖のレベルのインジェクタへ遡って注入できるサービスを探します。
providers をアプリケーションのルートレベルで定義しておけば、そのアプリケーションのすべてのコンポーネントがサービスを利用できます。
特別な理由がない限り、ルートレベルで定義しておくのがよいでしょう。
providers をルートレベルにするには、以下のいずれかで可能です。

  • root モジュール(app.module.ts)の providers にサービスを記述する
  • サービスの@Injectable デコレータの providedIn メタ情報に「root」と記述する

providers で指定できるサービス作成方法にはいくつかの種類があります。

  • useClass 指定したクラスをインスタンス化(new)して注入する
  • useValue 指定したインスタンスやオブジェクトを注入する
  • useFactory 指定した関数の戻り値を注入する

上記の例で示したproviders: [HogeService]は、以下の構文のシュガーシンタックスです。

providers: [{ provide: HogeService, useClass: HogeService }];

provideプロパティはサービスインスタンスを識別する DI トークン(名前)です。
これにより、HogeService を指定するだけで、HogeService のインスタンスを特定して注入することができるようになります。

この通り、provider の正体は名前サービス作成方法をセットで保持するオブジェクトです。
インジェクタはこれらの provider オブジェクトを頼りにサービスを作成し、作成されたサービス(インスタンスまたはオブジェクト)はそのインジェクタ内で共有されます。

バインディング

Angular では、View(.html ファイル)と ViewModel(.ts ファイル)が連携してコンポーネントを構成しています。
View と ViewModel の間で情報をやりとりする仕組みを、バインディングといいます。
バインディングには以下の 3 種類があります。

  • イベントバインディング  View から ViewModel へ情報を渡す
  • プロパティバインディング  ViewModel から View へ情報を渡す
  • 双方向バインディング  View と ViewModel で同期する

イベントバインディング

イベントバインディングは、View で発生した何らかのイベントを ViewModel に伝えることができます。
HTML の onClick、onLoad などをイメージすると理解しやすいでしょう。
HTML ボタンの click イベントにバインドするには、以下のように記述します。

<button (click)="hogeFnc()">Click Me!</button>

イベントバインディングによって、Web ページの操作からコンポーネントの関数を実行することができます。
関数に引数を渡す際は、テンプレート参照変数を使うことができます。
View の要素に対して#のプレフィクスとともに任意の名前をつけることで、要素の値にアクセスできるようになります。

<button (click)="hogeFnc(hogeForm.value)">Click Me!</button>
<input type="text" #hogeForm />
public hogeFnc(txt: string): void {
console.log(`hogeForm has value : ${txt}`);
}

プロパティバインディング

プロパティバインディングでは、ViewModel が持つ情報を View に埋め込むことができます。
HTML テキストフォームの value 属性にバインドするには、以下のように記述します。

<input type="text" [value]="hoge" />

上記の例では、ViewModel がもつ hoge 変数をテキストフォームの value にセットしています。
イベントバインディングとは括弧の形が異なることに注意してください。

  • イベントバインディング  :View から ViewModel へ  ⇒  ()丸括弧
  • プロパティバインディング :ViewModel から View へ  ⇒  []角括弧

上記の例では、テキストフォームの値を変更しても ViewModel の hoge 変数の値は変化しません。
テキストフォームの値はあくまでも「hoge 変数の値をセットされただけ」であり、テキストフォームの変更を hoge 変数にフィードバックしないためです。
変更を hoge 変数へフィードバックするには、双方向バインディングを使用します。(後述)

なお、どこかの HTML 要素にバインディングせずに、単純に変数の中身を出力したい場合は、以下のように記述できます。

{{ hoge }}

このように、{{ }}で書く構文をInterpolation(補間)といいます。
{{ }}の中には、ただ変数を出力するだけではなく、式や関数を記述することもできます。

また、プロパティバインディングを利用して View のスタイルを変更することができます。
style 属性にバインドするスタイルバインディングと class 属性にバインドするクラスバインディングです。
スタイルバインディングでは、[style.CSS プロパティ名]を記述し、対応する値をセットします。
クラスバインディングでは、[class.CSS クラス名]を記述し、クラスを有効にするか無効にするかを boolean でセットします。

<p [style.color]="hogeStyle">hoge works!</p>
<p [class.hogeblue]="hogeClass">hoge works!</p>
public hogeStyle: string = 'red';
public hogeClass: boolean = true;
.hogeblue {
  color: blue;
}

スタイルバインディングの場合、ViewModel にスタイル情報を持つ必要があります。
これは責務の分離の観点から好ましい状態ではない(本来、スタイルは CSS に集約するべき)ため、特別な理由がない限りクラスバインディングを利用するのがよいでしょう。

双方向バインディング

イベントバインディングとプロパティバインディングを組み合わせることで、View と ViewModel の同期を取ることができます。
以下は最も簡単な双方向バインディングの例です。

<input type="text" #hogeForm [value]="hoge" (input)="hoge = hogeForm.value" />

テキストフォームに hoge 変数の値がセットされ、テキストフォームの値が変更されればそのまま hoge 変数の値となります。
この方法でも双方向バインディングは実現できているのですが、あまりにも冗長です。
そのため、一般的にはNgModelディレクティブを使用します。
Angular には、フォームの操作を簡単に行うためのFormsModuleモジュールが用意されており、NgModel ディレクティブは FormsModule モジュールの一機能です。
NgModel ディレクティブを記述することで、フォームの要素をFormControlインスタンスとして扱うことができます。

NgModel ディレクティブを使用するには、FormsModule モジュールをインポートし、View に NgModel ディレクティブを記述します。

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

@NgModule({
declarations: [
 省略 
],
imports: [
FormsModule,
 省略 
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
<input type="text" [(ngModel)]="hoge" />

HTML の記述を[(ngModel)]とし hoge 変数をセットすることで、このフォームの FormControl インスタンスは hoge 変数と結びつき、変更を即座に同期します。
イベントバインディングの記法は()丸括弧、プロパティバインディングの記法は[]角括弧でしたが、これはそれらを組み合わせた形[()]となっています。(バナナボックス構文といいます)

ディレクティブ

Angular が解釈できる独自のキーワードを HTML に記述することで、View の出力結果を変化させることができます。
この仕組みをディレクティブといいます。
ディレクティブには大きく分けて 2 種類あります。

  • 構造ディレクティブ  View の構造(レイアウト)そのものを変化させる
  • 属性ディレクティブ  View の属性を操作して見た目や振る舞いを変化させる

構造ディレクティブ

よく利用するのは、条件に応じて DOM 要素を追加・削除するNgIfと、ループ処理で DOM 要素を生成していくNgForです。

<div *ngIf="hoge === fuga">あいうえお</div>
<div *ngFor="let item of ['AAAA','BBBB','CCCC']">{{ item }}</div>

NgIf の場合、指定した条件式(または boolean の変数)が true なら DOM 要素を生成し、false なら DOM 要素を生成しません。
一般的に、特定の要素を消す方法としては、CSS のdisplay: none;などがありますが、CSS ではDOM要素を生成したうえで非表示にします。
NgIf は DOM 要素の生成自体を行わないため、パフォーマンス面でメリットがあります。

NgFor の場合、指定した Iterable な値をループして DOM 要素を生成します。
基本的には for-of 構文で記述し、ループで取り出した値を仮変数に入れて利用します。
NgFor は、規定回数ループさせる(「let i=0; i < 10; i++」のような)処理はできないため、必ず Iterable な値を渡す必要があります。(ForEach と考えるとよいです)

属性ディレクティブ

よく利用するのは、CSS クラスを一括で適用するNgClassと、CSS スタイルを一括で適用するNgStyleです。

<div [ngClass]="hogeClass">あいうえお</div>
.classA {
  color: red;
}
.classB {
  font-weight: bold;
}
.classC {
  background-color: yellow;
}
import { Component } from '@angular/core';

@Component({
  selector: 'app-hoge',
  templateUrl: './hoge.component.html',
  styleUrls: ['./hoge.component.css']
})
export class HogeComponent {
  public hogeClass = {
    classA: true,
    classB: true,
    classC: true
  };
}

NgClass では、CSS クラス名と一致する Key-Value を含んだオブジェクトを記述します。
Value が true ならクラスが適用され、false なら適用されません。

<div [ngStyle]="hogeStyle">あいうえお</div>
import { Component } from '@angular/core';

@Component({
  selector: 'app-hoge',
  templateUrl: './hoge.component.html',
  styleUrls: ['./hoge.component.css']
})
export class HogeComponent {
  public hogeStyle = {
    color: 'blue',
    'text-decoration': 'underline',
    'font-size': '20pt'
  };
}

NgStyle では、CSS プロパティ名と一致する Key-Value を含んだオブジェクトを記述します。
Value に指定した値に応じてスタイルが適用されます。
NgStyle の場合、ViewModel にスタイル情報を持つ必要があります。
これは責務の分離の観点から好ましい状態ではない(本来、スタイルは CSS に集約するべき)ため、特別な理由がない限り NgClass を利用するのがよいでしょう。

カスタムディレクティブ

Angular に標準で実装されている組込ディレクティブのほか、自分でディレクティブを定義することもできます。
コンポーネントやサービスを作るときと同様、Angular CLI から generate できます。

ng generate directive directive/mydirective
CREATE src/app/directive/mydirective.directive.spec.ts (244 bytes)
CREATE src/app/directive/mydirective.directive.ts (151 bytes)
UPDATE src/app/app.module.ts (689 bytes)
<div appMydirective>あいうえお</div>
import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: [appMydirective]
})
export class MydirectiveDirective implements OnInit {
  constructor(private elRef: ElementRef) {}

  ngOnInit() {
    const el = this.elRef.nativeElement;
    el.style.backgroundColor = 'green';
    el.style.color = 'yellow';
  }
}

上記の例では、ディレクティブセレクタをappMydirectiveとしています。
要素中にappMydirectiveと記述することでディレクティブの効力が発揮され、要素のスタイルが変化します。
ディレクティブが適用された要素を参照するために、ElementRef クラスを使用しています。

パイプ

Angular では、View において表示内容を整形するための単純な関数(入力 → 整形 → 出力)の仕組みを持っており、これをパイプといいます。
一般的なコマンドラインインターフェースにおけるパイプライン演算子と同じように、|でつなげて関数を呼び出すことで、データの加工をすることができます。
以下は代表的な組込パイプの例です。

<p>{{ 'This is Pipe' | uppercase }}</p>
<p>{{ 'This is Pipe' | lowercase }}</p>
<!-- 0文字目から4文字抽出 -->
<p>{{ 'This is Pipe' | slice:0:4 }}</p>
<p>{{ 10000 | currency }}</p>
<p>{{ 10000 | currency:'JPY' }}</p>
<p>{{ 10000 | number }}</p>
<!-- 整数桁10桁以上、小数点以下2桁 -->
<p>{{ 10000 | number:'10.2' }}</p>
<p>{{ 0.5 | percent }}</p>
<p>{{ dateObj | date }}</p>
<p>{{ dateObj | date:'yyyy年 MM月 dd日' }}</p>

※dateObj:日付型オブジェクト

カスタムパイプ

ディレクティブと同様に、パイプも自作することが可能です。
コンポーネントやサービスを作るときと同様、Angular CLI から generate が可能です。

ng generate pipe pipe/mypipe
CREATE src/app/pipe/mypipe.pipe.spec.ts (187 bytes)
CREATE src/app/pipe/mypipe.pipe.ts (205 bytes)
UPDATE src/app/app.module.ts (828 bytes)
{{ '吾輩は猫である。名前はまだ無い。' | mypipe }}
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'mypipe'
})
export class MypipePipe implements PipeTransform {
  transform(value: string): string {
    if (typeof value === 'string') {
      return value.replace('', '');
    } else {
      return value;
    }
  }
}

上記の例では、パイプセレクタをmypipeとしています。
mypipeにデータをパイプすることで、渡した文字列中のキーワードが置換されます。
パイプとしてデータの変換を行う正体はtransformメソッドです。
Angular は、PipeTransform インタフェースとして実装された transform メソッドを呼び出すことでデータの変換を行います。
なお、パイプ処理は View の中で事あるごとに実行されるため、重い処理を行うのは避けた方がよいでしょう。
また、パイプの中の処理では、意図した型の引数が渡ってきているか必ずチェックしておくことが望ましいです。

ルータ

Angular ではコンポーネントを組み合わせて画面を構成しますが、何らかのアクション(ユーザのボタンタップ、function 起動等)に合わせて表示するコンポーネントを入れ替えることで、画面遷移を表現できます。
この機能をルーティングと呼び、ルーティングはRouterModuleモジュールによって提供されます。

ルーティングを行うには以下のように準備を行う必要があります。

  1. base 要素を記述する
  2. AppRoutingModule モジュールを作成する
  3. ルートを設定する
  4. ルータアウトレットを記述する
  5. 遷移を行うトリガを記述する

base 要素を記述する

まず、アプリケーションの基点となるページがどこにあるかを示すために、<base>タグを index.html の head タグに記述します。
href 属性を「/」とすることで、このページがアプリケーションのルートであることをルータへ示します。
なお、Angular CLI でアプリケーションの雛型を生成(ng new)した場合は、自動で index.html に書き込まれます。

<base href="/" />

AppRoutingModule モジュールを作成する

ルーティング機能を利用するために、Angular の RouterModule をインポートする必要があります。
root モジュールの中でそのままインポートすることもできますが、一般的には、ルーティング専用のモジュールを用意して root モジュールにインポートします。
RouterModule には初期設定作業を行う必要があるため、モジュールを分割することで root モジュールのコードが煩雑になるのを防ぎます。
モジュール名は任意の名前を付けられますが、慣例的に「AppRoutingModule」とするのが推奨されています。

ng generate module app-routing --flat --module=app
  • --flat 固有フォルダを作らず、src/app 直下に作成する
  • --module 指定したモジュールのインポートに自動的に追加する
CREATE src/app/app-routing.module.ts (194 bytes)
UPDATE src/app/app.module.ts (1119 bytes)

ルーティングモジュールはアプリケーション全体で利用するモジュールであるため、AppModule(root モジュール)と同じフォルダに置くことが多いです。
また、上記の例では generate したモジュールをそのまま AppModule にインポートさせています。
module オプションを使用しない場合は、手作業でインポートする必要があります。

ルートを設定する

Angular に組み込まれている RouterModule モジュールは、そのままでは使えません。
ルーティングを行うための地図となるルート(Route)を教え込む必要があります。
generate したばかりの AppRoutingModule モジュールは何も書かれていない空の状態です。
RouterModule モジュールをインポートし、ルーティングモジュールとしての機能を果たすよう設定をしていきます。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; // RouterModuleとRoutes型をインポート

import { HogeComponent } from './hoge/hoge.component'; // サンプル用コンポーネント #1
import { FugaComponent } from './fuga/fuga.component'; // サンプル用コンポーネント #2

const routes: Routes = [
  // https://example.jp/hoge にアクセスされたらHogeComponentを表示する
  { path: 'hoge', component: HogeComponent },
  // https://example.jp/fuga にアクセスされたらFugaComponentを表示する
  { path: 'fuga', component: FugaComponent },
  // 上記以外のURLにアクセスされたら https://example.jp/ に遷移する
  { path: '**', redirectTo: '/' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)], // ルートを読み込む
  exports: [RouterModule]
})
export class AppRoutingModule {}

まず、RouterModuleモジュールとRoutes型をインポートします。
Routes 型は Route 型(ルートの 1 エントリ)の配列です。
Route 型に指定できるプロパティには、一例として以下のようなものがあります。

プロパティ 内容
path URL と比較する文字列。
**を指定するとワイルドカードになる。
pathMatch URL の比較ルール。
・prefix(前方一致):デフォルト
・full(完全一致)
component path が一致した場合に表示するコンポーネント
redirectTo path が一致した場合にリダイレクトする URL
canActivate アクティブ化可能か判定するガード(後述)の配列
loadChildren 遅延ロードを行うためのコールバック

ルートは配列に指定された順番の通りに優先順位が決まります。
ワイルドカードを使用する際は、順番を誤るとすべてのアクセスがワイルドカードに拾われてしまうため、必ず最後に記述します。
作成したルート(routes 定数)を RouterModule モジュールの forRoot メソッドで読み込むことで、ルータとして機能させることができます。

ルータアウトレットを記述する

ルータによって入れ替わるコンポーネントのプレースホルダとなるものが、router-outletディレクティブです。
View の任意の場所に記述することで、URL に対応したコンポーネントがルータによってバインドされます。

<router-outlet></router-outlet>

遷移を行うトリガを記述する

通常、Web 画面上のリンクやボタンを押下することで画面遷移を行います。
これらの遷移トリガを定義するには、以下のような方法があります。

  • routerLinkディレクティブを記述する
  • Routernavigateメソッドを実行する

routerLink ディレクティブを使用する場合は、View の a タグまたは button タグにディレクティブを追加します。

<div>
  <a routerLink="hoge">Go To Hoge</a>
  <button routerLink="hoge">Go To Hoge</button>
</div>
<div>
  <a routerLink="fuga">Go To Fuga</a>
  <button routerLink="fuga">Go To Fuga</button>
</div>
<router-outlet></router-outlet>

上記の例では、リンクを押してもボタンを押しても、それぞれ「/hoge」「/fuga」に遷移します。
(下部の router-outlet に「/hoge」「/fuga」の内容が表示されます。)

スクリプトで何かしらの処理を行ったうえで遷移したい場合は、以下のように navigate メソッドを使用します。

<div>
  <input type="text" #password />
  <button (click)="goNext(password.value)">Go</button>
</div>
<router-outlet></router-outlet>
import { Component } from '@angular/core';
import { Router } from '@angular/router'; // 遷移を行うためのRouterをインポート

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'ngtest';

  constructor(private router: Router) {}

  goNext(pass: string): void {
    if (pass === 'piyo') {
      this.router.navigate(['hoge']); // 入力されたパスワードが「piyo」なら「/hoge」に遷移
    } else {
      console.log('NG! パスワードが誤っています');
    }
  }
}

ガード

ルータによって、ユーザはアプリケーション内のコンポーネントを自由に切り替えて参照することができるようになりました。
しかしながら、一般的なアプリケーションでは、特定のユーザにのみ参照を許可するような機能があります。(たとえばログイン機能などは最たる例です)
そのような特別な機能へのルーティングを制御するための方法として、Angular はガードという機能を持っています。
ガードは、遷移先が利用可能かどうか判定するための関数といえます。NG の場合はルータは遷移をキャンセルします。

ng generate guard guard/auth
import { Injectable } from '@angular/core';
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
} from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    // ここに何か処理を書く
    return true;
  }
}

上記の例では、ルータは CanActivate インタフェースのcanActivateメソッドを呼び出すことで判定を行います。
作成したガードは、Route オブジェクトの中で指定します。
これにより、該当するパスへ遷移しようとしたときにガードのチェックが実行され、遷移の可否を判定します。

const routes: Routes = [
{ path: "hoge", component: HogeComponent, canActivate: [AuthGuard] }, // AuthGuardのチェックがOKなら参照可能
{ path: "fuga", component: FugaComponent, canActivate: [AuthGuard, UserGuard, DateGuard, ....]}, // すべてのガードのチェックがOKなら参照可能
{ path: "piyo", component: PiyoComponent }, // ガードのチェックなしで参照可能
{ path: "**", redirectTo: "/" }
];

チェックを行うガードは配列で指定します。
複数のガードを連ねた場合、すべてのガードの判定が OK だったら参照可能になります。
チェックの順番は配列で指定した順番となり、途中のガードで NG になった場合でもすべてのガードのチェックが実行されます。

HTTP クライアント

Angular では、サーバと非同期通信を行うための HTTP クライアントが提供されています。
HTTP クライアントを利用するには、HttpClientModuleをインポートします。
一般的には、HTTP クライアントはアプリケーション全体で必要になる機能であるため、root モジュールでインポートします。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    BrowserModule,
    // BrowserModule のあとに HttpClientModule をインポート
    HttpClientModule
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

HttpClientModule をインポートすることで、HttpClientクラスが利用できるようになります。
実際の HTTP アクセスはこの HttpClient クラスのメソッドで行います。

なお、コンポーネントで HttpClient を使用するのは避けた方がよいとされています。
コンポーネントで HttpClient を使用してしまうと、コンポーネントの責務が肥大化しやすく管理が難しくなります。
一般的には、コンポーネントから独立した(カプセル化された)サービスクラスを用意して、HttpClient を使用します。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

const URL: string = 'https://hogehoge.jp/api/backend-fuga';

@Injectable()
export class ServerAccessService {
  constructor(private http: HttpClient) {}

  getRequest(): Observable<string> {
    return this.http.get(URL);
  }

  postRequest(param: string): Observable<string> {
    const httpOptions = {
      'Content-Type': 'text/plain; charset=UTF-8'
    };
    return this.http.post(URL, param, httpOptions);
  }
}

非同期で行われる通信のレスポンスを捕捉するために、HttpClient のメソッドの戻り値はObservable型の値となっています。
これをsubscribe(購読)することで、サーバからのレスポンスをアプリケーションが受け取ることができます。

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  private data: string;

  constructor(private accessService: ServerAccessService) {}

  getData() {
    this.accessService.get().subscribe(
      (data: string) => (this.data = data), // 通信成功時
      error => console.log('error!!') //通信失敗時
    );
  }
}

subscribe の代わりに、View でasyncパイプを利用することもできます。
この場合、サーバからのレスポンスを受け取ると、Angular が自動でデータを読み出して View にバインドしてくれます。
subscribe の場合はどこかでunsubscribe(購読解除)する必要があるのに対し、async パイプの場合は、コンポーネントの破棄時に Angular が自動で unsubscribe してくれます。
ただし、async パイプは複雑なデータ構造を持つレスポンスを扱うには不向きです。
また、レスポンスを受信するまではnullとされ、View 上に何も表示されないことに注意が必要です。

@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
private data$: Observable<string>; // Observable変数であることを示すために、慣例的に「$」を末尾に付与します

constructor(private accessService: ServerAccessService) {}

getData() {
this data$ = this.accessService.get();
}
}
<div>
  <h1>サーバから受信した値</h1>
  {{ data$ | async }}
</div>

RxJS

RxJSは、非同期処理やイベント処理などの時間とともに変化する(いつ発生するかわからない)データの処理を簡潔に記述できるようにするライブラリです。
Angular はコア機能の依存ライブラリに RxJS を利用しており、Angular アプリケーションにおいても RxJS の考え方は避けて通れないものとなっています。

RxJS では、重要な概念として以下の 3 つが登場します。

名前 内容
Observable(観測可能なもの) 遅延プッシュされる複数の値のコレクション。
Observer(観測者) プッシュされた値の処理方法を定義したオブジェクト。
以下の 3 つの関数を持つ。
・next(成功時の処理)
・error(エラー時の処理)
・complete(完了時の処理)
Subscribe(購読) Observable と Observer を紐づけ、
Observable の観測を開始すること。

Producer(生産者)が断続的に提供するデータに反応して、Consumer(消費者)が何らかの処理を行う設計思想のことを、リアクティブプログラミングといいます。
RxJS は「Reactive Extensions Library for JavaScript」というように、このリアクティブプログラミングの考え方に即しています。
上記の表においては、Observable が Producer の役割、Observer が Consumer の役割にあたります。

Observable 型のデータに対してsubscribeメソッドを通じて Observer を提供し、Observer が持つ処理方法に則ってプッシュされた値を処理します。
下記の例では、「1」「2」「3」の値を順にプッシュして完了する Observable に対して、別々の処理を行う Observer をそれぞれ観測させています。

import { Observable } from 'rxjs';

const observable = new Observable(observer => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
});

const observer1 = {
  next(data) {
    console.log('subscription1 がデータを受信しました: ' + data);
  },
  error(err) {
    console.error('subscription1 でエラーが発生しました: ' + err);
  },
  complete() {
    console.log('subscription1 の購読が完了しました。');
  }
};
const observer2 = {
  next(data) {
    console.log('subscription2 got new value: ' + data);
  },
  error(err) {
    console.error('subscription2 got new error: ' + err);
  },
  complete() {
    console.log('subscription2 finished.');
  }
};

const subscription1 = observable.subscribe(observer1);
const subscription2 = observable.subscribe(observer2);
subscription1 がデータを受信しました: 1main.js:591:25
subscription1 がデータを受信しました: 2main.js:591:25
subscription1 がデータを受信しました: 3main.js:591:25
subscription1 の購読が完了しました。main.js:597:25
subscription2 got new value: 1 main.js:602:25
subscription2 got new value: 2 main.js:602:25
subscription2 got new value: 3 main.js:602:25
subscription2 finished. main.js:608:25

以下のように subscribe メソッドに直接 Observer を記述することもできます。
Observer オブジェクトを引数とする以外に、関数を 1 ~ 3 個渡すことでも subscribe が可能です。
その場合、引数として渡した順番に沿って next、error、complete と見なされます。
また、next 以外のメソッドは任意であるため、指定しなくても動作します。

const subscription1 = observable.subscribe({
  next(data) {
    console.log('subscription1 がデータを受信しました: ' + data);
  },
  error(err) {
    console.error('subscription1 でエラーが発生しました: ' + err);
  },
  complete() {
    console.log('subscription1 の購読が完了しました。');
  }
});
const subscription2 = observable.subscribe(
  data => console.log('subscription2 がデータを受信しました: ' + data),
  err => console.log('subscription2 でエラーが発生しました: ' + err),
  () => console.log('subscription2 の購読が完了しました。')
);
const subscription3 = observable.subscribe(data =>
  console.log('subscription3 がデータを受信しました: ' + data)
);
subscription1 がデータを受信しました: 1main.js:591:25
subscription1 がデータを受信しました: 2main.js:591:25
subscription1 がデータを受信しました: 3main.js:591:25
subscription1 の購読が完了しました。main.js:597:25
subscription2 がデータを受信しました: 1main.js:600:68
subscription2 がデータを受信しました: 2main.js:600:68
subscription2 がデータを受信しました: 3main.js:600:68
subscription2 の購読が完了しました。main.js:600:182
subscription3 がデータを受信しました: 1main.js:601:68
subscription3 がデータを受信しました: 2main.js:601:68
subscription3 がデータを受信しました: 3main.js:601:68

complete メソッドまたは Error メソッドが呼ばれた時点で観測は終了しますが、complete メソッドを呼ばずに、観測されている限りデータを提供し続ける Observable もありえます。
その場合は、データの利用者側が任意のタイミングで観測を打ち切る必要があります。
観測を打ち切るには、Subscriptionオブジェクトのunsubscribeメソッドを実行します。
Subscription は観測行為そのものを表すオブジェクトです。(雑誌の購読契約みたいなものとイメージするとよいでしょう)
subscribe メソッドで観測を開始した際、その戻り値として返却されます。

// 観測の開始
const subscription = observable.subscribe({
  next(data) {
    // 何かの処理
  },
  error(err) {
    // 何かの処理
  },
  complete() {
    // 何かの処理
  }
});

// 観測の終了
subscription.unsubscribe();

RxJS の特長として、断続的に提供されるデータを配列のように扱えることがあります。
たとえば、「1」「2」「3」を順番に提供する Observable があった場合、あたかも[1, 2, 3]という配列であるかのように操作を加えることができます。
このデータ操作機能のことを、オペレータといいます。

以下の例では、 Observable が提供するデータのうち、奇数のみを取り出し、さらに 100 倍するような操作をしています。

import { Observable, pipe } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const observable = new Observable<number>(observer => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
});

const sub = observable
  .pipe(
    filter(data => data % 2 !== 0), // 奇数のみ取り出す
    map(data => data * 100) // 100倍する
  )
  .subscribe(data => console.log(data));
100 main.js:591:232
300 main.js:591:232

上記の例のうち、filtermapがオペレータです。
複数のオペレータを連ねるにはpipeメソッドを使用します。

Angular Material

近年、Google が推奨しているデザイン手法がマテリアルデザインです。
「紙」と「インク」のイメージで構成された部品に反射光や影の効果を施すことで、より自然に「物体が存在する」ことの表現を目指したデザイン手法です。

このマテリアルデザインの原則に則って作られた UI 部品(ボタンやフォーム等)ライブラリが、Angular Materialです。
Angular の開発チームによって作成されたライブラリであり、公式の Angular ファミリーです。
そのため、Angular フレームワーク が View(.html ファイル)に作用する処理を阻害せず、高い親和性をもって動作する UI 部品を提供してくれます。

Angular Material は外部の npm パッケージとして公開されています。
npm install のほか、ng addコマンドでもインストールが可能です。
ng add では、以下のような関連する初期設定まで簡潔に行えるため、公式では ng add を使うよう推奨されています。

  • 依存ライブラリ(Component Dev Kit、Angular Animations)のインストール
  • カラーテーマの設定
  • HammerJS(ジェスチャー認識ライブラリ)のインストール
  • BrowserAnimationsModule のインポート
  • index.html への Roboto フォントの追加
  • index.html へのマテリアルデザインアイコンフォントの追加
  • root CSS の調整
  • body の margin を 0 に
  • html と body の height を 100%に
  • デフォルトフォントの設定
ng add @angular/material

Angular Material に適用するカラーセットのことをテーマといいます。
テーマは自作することも可能ですが、Angular Material では組込テーマとして以下の 4 種類を用意しています。

  • deeppurple-amber.css
  • indigo-pink.css
  • pink-bluegrey.css
  • purple-green.css

使いたいテーマは、root CSS でインポートするほか、Angular.json のビルド設定(グローバルスタイル)に加えることでアプリケーションに適用できます。
(ng add でインストールした場合は後者の方法です。)

"build": {
 省略 
"styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.css"
],
 省略 
}

Angular Material の UI 部品(ボタンやチェックボックス等)はすべてモジュール化されているため、アプリケーション内で使用するにはまずモジュールをインポートする必要があります。
root モジュールにインポートしておくとアプリケーションの全体で利用可能になるため、特別な理由がない限り root モジュールにインポートしておくのがよいでしょう。

import { MatButtonModule, MatCheckboxModule } from '@angular/material';

@NgModule({
 省略 
imports: [
MatButtonModule,
MatCheckboxModule
],
 省略 
})

インポートした UI 部品を View に記述することで、Angular Material の UI 部品 を画面上に配置することができます。
以下は代表的な部品の例です。
もちろんこれらの部品に対しても、ディレクティブを使ったりバインディングしたりすることができます。

<div>
  <button mat-raised-button>Button</button>
</div>

<div>
  <mat-form-field>
    <input matInput type="text" />
  </mat-form-field>
</div>

<div>
  <mat-checkbox>Checkbox label</mat-checkbox>
</div>

<div>
  <mat-radio-group>
    <mat-radio-button>Radio A</mat-radio-button>
    <mat-radio-button>Radio B</mat-radio-button>
    <mat-radio-button>Radio C</mat-radio-button>
  </mat-radio-group>
</div>

<div>
  <mat-icon>home</mat-icon>
</div>

angular-material.PNG

スライドトグルやスライダーなど、ジェスチャー操作が可能な UI 部品を使用する場合は、HammerJSライブラリをインポートする必要があります。
ng add で Angular Material をインストールした場合は、HammerJS をインストールするかどうかも併せて確認されます。
ng add で HammerJS のインストールを拒否した場合や、npm install で Angular Material をインストールした場合などは、別途 npm からインストールすることで利用できます。

npm install --save hammerjs

インストールができたら、アプリケーションのエントリポイント(main.ts)でインポートします。
これにより、ジェスチャー操作を伴う UI 部品を利用できるようになります。

import 'hammerjs';

テスト

Angular では、以下の自動テストをサポートしています。

  • Jasmineテストフレームワーク + Karmaテストランナーによる自動ユニットテスト
  • Protractorテストフレームワークによる自動 e2e(End-to-End)テスト

ユニットテスト

ユニットテストは Angular に組み込まれているJasmineKarmaによって実行されます。
テストコードを解釈し、実行するのは Jasmine の機能です。
Karma は、Jasmine の機能を利用してブラウザ上でテストを行い、結果をレポートします。
また、Karma が実行されている間、ソースコードの変更を検知して都度テストを再実行してくれます。

自動ユニットテストは Angular CLI のコマンドによって実行することができます。
以下のコマンドを実行するとテストが開始され、プロジェクト内に含まれる.spec.tsファイルに定義されたテストが実行されます。

ng test
$ ng test
Browserslist: caniuse-lite is outdated. Please run next command `npm update`
 30% building 15/15 modules 0 active22 11 2019 15:36:30.973:WARN [karma]: No captured browser, open http://localhost:9876/
22 11 2019 15:36:31.026:INFO [karma-server]: Karma v4.1.0 server started at http://0.0.0.0:9876/
22 11 2019 15:36:31.027:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
 30% building 17/17 modules 0 active22 11 2019 15:36:31.089:INFO [launcher]: Starting browser Chrome
22 11 2019 15:36:35.035:WARN [karma]: No captured browser, open http://localhost:9876/
22 11 2019 15:36:35.132:INFO [Chrome 78.0.3904 (Windows 10.0.0)]: Connected on socket WxxEvvEsAy5bM4a2AAAA with id 10021485
Chrome 78.0.3904 (Windows 10.0.0): Executed 12 of 12 SUCCESS (2.265 secs / 2.225 secs)
TOTAL: 12 SUCCESS
TOTAL: 12 SUCCESS
TOTAL: 12 SUCCESS

=============================== Coverage summary ===============================
Statements : 100% ( 41/41 )
Branches : 100% ( 2/2 )
Functions: 100% ( 17/17 )
Lines: 100% ( 32/32 )
================================================================================

angular-karma.PNG

テストのコードカバレッジを出力するには、ng test コマンドに--code-coverageオプションを付与するか、Angular.json において以下のように設定します。

"test": {
 省略 
"codeCoverage": true
 省略 
}

コードカバレッジを出力した場合、プロジェクト内にcoverageフォルダが作成され、HTML 形式でレポートが作成されます。
また、上記の例のようにコンソールにもCoverage summaryが表示されます。

angular-karma2.PNG

テストコード(.spec.ts)の構成は以下のようになっています。

  • import
  • describe
  • beforeEach
  • it
  • expect
  • afterEach

import では、テストに必要なモジュールやテスト対象のクラスを読み込みます。
describe は一区切りのテストシナリオを表します。describe の中に describe をネストすることもできます。
beforeEach、it、afterEach は describe の中に任意の数だけ記述します。

実際のテストケースを表すのは it です。
beforeEach、afterEach は、各 it の前後で毎回実施する初期化処理・終了処理などを記述します。
it の中では、想定結果の検証処理である expect を必ず記述します。
expect は任意の数だけ書くことができ、複数書いた場合、そのうちのひとつでも失敗すればその it は NG になります。

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';

describe('C-1: ルートコンポーネントのテスト', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule],
      declarations: [AppComponent]
    }).compileComponents();
  }));

  it('C-1-1: コンポーネントが生成できること', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`C-1-2: タイトルが「test-sample」であること`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('test-sample');
  });

  it('C-1-3: H1見出しのテキストが「Welcome to test-sample!」であること', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain(
      'Welcome to test-sample!'
    );
  });
});

expect に続くtoBeTruthytoEqualtoContainなどはMatcherと呼ばれる関数です。
expect の引数である検証対象と、Matcher の引数である比較対象を照らし合わせ、条件を満たしていれば expect は成功します。

Angular のユニットテストにおいては、TestBedというテスト用の仮想 NgModule を利用します。
TestBed では、通常の NgModule と同様に declarations、imports、providers などを設定でき、モックサービスやモックコンポーネントを使用する場合はそれらを TestBed に読み込みます。

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TweetListComponent } from './tweet-list.component';
import { Component, Input } from '@angular/core';
import { By } from '@angular/platform-browser';

@Component({
  selector: 'app-tweet',
  template: '<div>Stub Component</div>'
})
class StubTweetComponent {
  @Input() tweet: any;
}

describe('C-3: ツイートリストコンポーネントのテスト', () => {
  let component: TweetListComponent;
  let fixture: ComponentFixture<TweetListComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [TweetListComponent, StubTweetComponent]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TweetListComponent);
    component = fixture.componentInstance;
    const stubValue = [
      {
        user: 'test 1',
        content: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`,
        retweet: 12345,
        like: 67890
      }
    ];
    component.tweetList = stubValue;
    fixture.detectChanges();
  });

  it('C-3-1: ツイートリストがすべて表示されていること', () => {
    const element: HTMLElement = fixture.debugElement.nativeElement;
    const tweetComponents = element.querySelectorAll('app-tweet');
    expect(tweetComponents.length).toBe(3);
  });

  it('C-3-2: リツイートイベントに応じてRT数が増加すること', () => {
    let beforeCount: number;
    const spy = spyOn(component, 'retweet').and.callThrough();
    component.tweetList.forEach(tweet => {
      beforeCount = tweet.retweet;
      fixture.debugElement
        .query(By.css('.tweet'))
        .triggerEventHandler('retweet', tweet);
      expect(spy).toHaveBeenCalled();
      expect(tweet.retweet).toBe(beforeCount + 1);
    });
  });
});

Jasmine には、特定のオブジェクトメソッドや関数の実行を検知して、任意の動きに差し替えるスパイという機能があります。
上記の例(テストケース C-3-2)では、TweetListComponent インスタンスのretweetメソッドをスパイし、callThrough(スパイから本来のメソッドを呼び出す)しています。
これにより、expect においてスパイオブジェクトのtoHaveBeenCalledを検査することで、そのメソッドが呼び出されたかどうかをチェックしています。
上記の例はただスパイを経由しているだけですが、あらかじめ定めた戻り値をスパイから返させることで、モックやスタブを実現することができます。

e2e テスト

実際にアプリケーションをビルドしてブラウザ上で行うテストをe2e(End-to-End)テストといいます。
ユニットテストではモックやスタブを用意してテスト対象の動作のみにフォーカスしたテストを行いますが、e2e テストではサーバサイドを含めた全体的な動きを検査することが目的です。(総合テスト段階に相当します)
e2e テストにおいてはProtractorフレームワークを利用しますが、Protractor は内部的に Jasmine のテスト機能を利用しています。
したがって、テストコードの文法などはユニットテストと同じように書くことができます。

自動 e2e テストは Angular CLI のコマンドによって実行することができます。
以下のコマンドを実行すると、e2eフォルダ内のapp.e2e-spec.tsファイル(ファイルは変更可能です)に定義されたテストコードを実行し、結果をレポートします。

ng e2e
$ ng e2e
[15:09:32] I/config_source - curl -oC:\Users\user\Desktop\DEV\test-sample\node_modules\protractor\node_modules\webdriver-manager\selenium\chrome-response.xml https://chromedriver.storage.googleapis.com/
[15:09:33] I/update - chromedriver: file exists C:\Users\user\Desktop\DEV\test-sample\node_modules\protractor\node_modules\webdriver-manager\selenium\chromedriver_78.0.3904.105.zip
[15:09:33] I/update - chromedriver: unzipping chromedriver_78.0.3904.105.zip
[15:09:34] I/update - chromedriver: chromedriver_78.0.3904.105.exe up to date
Browserslist: caniuse-lite is outdated. Please run next command `npm update`

Date: 2019-11-22T06:09:51.188Z
Hash: 13114b74aad04ac5cd11
Time: 8615ms
chunk {main} main.js, main.js.map (main) 28 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 248 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 16.7 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.94 MB [initial] [rendered]
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
i 「wdm」: Compiled successfully.
[15:09:53] I/launcher - Running 1 instances of WebDriver
[15:09:53] I/direct - Using ChromeDriver directly...

DevTools listening on ws://127.0.0.1:55696/devtools/browser/05988235-51e7-497a-920a-346cd5940efc
Jasmine started

workspace-project App
√ should display welcome message

Executed 1 of 1 spec SUCCESS in 1 sec.
[15:10:00] I/launcher - 0 instance(s) of WebDriver still running
[15:10:00] I/launcher - chrome #01 passed
aqlwah
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした