Angular 入門 - アプリケーションを作りながらモジュールとコンポーネントとサービスについて理解する

# はじめに

* Angular のチュートリアルをやったりドキュメントを読んだりしたので、復習のために自分でアプリケーションを作りながら入門的な内容をまとめていく。

# ドキュメント

* [公式のチュートリアル](https://angular.io/tutorial)
  * これやっておけばなんとなくわかったような気になる
* [公式のアーキテクチャ解説](https://angular.io/guide/architecture)
  * コンポーネントとは？モジュールとは？サービスとは？というのがわかる

# インストール

* Angular プロジェクトのスケルトンを生成してくれる Angular CLI をインストールする

```
npm install -g @angular/cli
```

# プロジェクトの作成

```
ng new myapp
cd myapp
npm install
```

# アプリケーションの起動

* コマンド実行後 `http://localhost:4200/` にブラウザでアクセスすることで起動を確認できる

```
npm start
```

# コンポーネントの作成

* コンポーネントは Angular の管理する UI 要素の単位
* コンポーネントは TypeScript のクラスで内部変数やメソッドを持ち、テンプレートの HTML や CSS を持つことができる
* コンポーネントは入れ子にすることができ、ページとしても UI の要素としても振る舞うことができる
* 今回は ng コマンドでヘッダーパーツのコンポーネントを作成する

## コマンドの実行

* `ng generate` コマンドでコンポーネントを作成する
* コンポーネントの本体の ts ファイルとテストの spec.ts ファイル、html, css が作成される
* app.module.ts の変更箇所については後述

```
$ ng generate component header
installing component
  create src/app/header/header.component.css
  create src/app/header/header.component.html
  create src/app/header/header.component.spec.ts
  create src/app/header/header.component.ts
  update src/app/app.module.ts
```

## コンポーネントの登録

* コンポーネント作成の際に app.module.ts に変更が入ったが、これはアプリケーションのメインモジュールにヘッダーコンポーネントが登録されたということ
* モジュールとは現時点では Angular のフレームワークとしての機能を提供するレイヤーと理解しておけば良い
* 今回はアプリケーション内で利用しているコンポーネントのリストの設定にヘッダーコンポーネントが追加された

```
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index f657163..55587c2 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -2,10 +2,12 @@ import { BrowserModule } from '@angular/platform-browser';
 import { NgModule } from '@angular/core';

 import { AppComponent } from './app.component';
+import { HeaderComponent } from './header/header.component';

 @NgModule({
   declarations: [
-    AppComponent
+    AppComponent,
+    HeaderComponent
   ],
   imports: [
     BrowserModule
```

### @NgModule とは

* @NgModule の箇所は「デコレータ」と呼ばれる TypeScript の機能でクラスやメソッドにメタデータを定義できる
  * Java のアノテーションや C# の属性のようなもの
* @NgModule デコレータを付けたクラスは Angular のモジュールとして振る舞う
* @NgModule デコレータのカッコの中には Angular のモジュールの設定を定義する
* declarations にはアプリケーションで使用されるコンポーネントがリストで定義される

# 作成されたコンポーネントの確認

* 初期化処理として constructor と ngOnInit が定義されている
* constructor は TypeScript の標準のコンストラクタで ngOnInit は Angular のコンポーネントの初期化処理
* constructor には後述の DI のための処理だけを定義して、コンポーネントの初期化処理は ngOnInit に定義する慣習になっている

```ts:header.component.ts
import { Component, OnInit } from '@angular/core';

// @Component デコレータがついたクラスがコンポーネントとして振る舞う
@Component({
  selector: 'app-header', // HTML上では <app-header> として扱うことができる
  templateUrl: './header.component.html', // このコンポーネントに関連するテンプレートの定義
  styleUrls: ['./header.component.css'] // このコンポーネントに関連するスタイルシートの定義
})
export class HeaderComponent implements OnInit {

  // 初期化処理
  constructor() { }

  // OnInit インターフェイスを実装すると ngOnInit が初期化時に実行される
  ngOnInit() {
  }

}
```

# コンポーネントの表示

* app.component.html の行頭にヘッダーのセレクタを記述することでヘッダーコンポーネントを表示することができる

```
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 230f4ed..5b233f3 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,4 +1,5 @@
 <!--The content below is only a placeholder and can be replaced.-->
+<app-header></app-header>
 <div style="text-align:center">
   <h1>
     Welcome to {{title}}!
```

# コンポーネントの値をテンプレートに表示する

## 変数をテンプレートに表示する

* コンポーネントのインスタンス変数はテンプレート上で `{{VAR}}` という記法で表示できる

```
diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html
index 51bfb8c..80a0af3 100644
--- a/src/app/header/header.component.html
+++ b/src/app/header/header.component.html
@@ -1,3 +1,4 @@
 <p>
   header works!
+  header title is {{title}}
 </p>
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index ac77fc6..f430c29 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -7,6 +7,8 @@ import {Component, OnInit} from '@angular/core';
 })
 export class HeaderComponent implements OnInit {

+  title = 'タイトル';
+
   constructor() {
   }
```

## テンプレートでリストを表示する

* テンプレートで `*ngFor` を使うとリストをループで展開することができる
* ng-container 要素はコンパイル後のソースには表示されないので今回のように *ngFor などとセットで利用される

```
diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html
index 80a0af3..348d0c7 100644
--- a/src/app/header/header.component.html
+++ b/src/app/header/header.component.html
@@ -2,3 +2,7 @@
   header works!
   header title is {{title}}
 </p>
+
+<ng-container *ngFor="let b of buttons">
+  <button>{{b}}</button>
+</ng-container>
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index f430c29..cbe156a 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -9,6 +9,12 @@ export class HeaderComponent implements OnInit {

   title = 'タイトル';

+  buttons: string[] = [
+    'button1',
+    'button2',
+    'button3'
+  ];
+
   constructor() {
   }
```

## 変数で表示非表示を切り替える

* `*ngIf` を使うとコンポーネントの変数の値によってテンプレート上の表示非表示を切り替えることができる

```
diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html
index 348d0c7..e09e2ad 100644
--- a/src/app/header/header.component.html
+++ b/src/app/header/header.component.html
@@ -3,6 +3,9 @@
   header title is {{title}}
 </p>

-<ng-container *ngFor="let b of buttons">
-  <button>{{b}}</button>
+<ng-container *ngIf="showButton">
+  <ng-container *ngFor="let b of buttons">
+    <button>{{b}}</button>
+  </ng-container>
 </ng-container>
+
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index cbe156a..5f5301f 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -15,6 +15,8 @@ export class HeaderComponent implements OnInit {
     'button3'
   ];

+  showButton = false;
+
   constructor() {
   }
```

# ルーティング機能を使って表示するコンポーネントを切り替える

* RouterModule を使うことでテンプレート中の `<router-outlet>` 内に任意のコンポーネントを表示することができる
* 今回は About ページを追加してページ全体の表示を切り替えられるようにする
  * ページ全体を切り替えるような実装にするため既存のトップページの内容は IndexComponent に避難して AppComponent では <router-outlet> の表示だけを行う

## Index ページ用のコンポーネントの追加

```
ng g component index
```

* app.component.html を index.component.html にコピーしておく

## About ページ用のコンポーネントの追加

```
ng g component about
```

## ルーターモジュールの作成

* ルーティング設定を AppModule に直接記載してもいいが、今回はルーティング設定を切り出したいのと自分でモジュールを作ってみたいのでこのアプリケーション用のルーターモジュールをつくってみる

```
$ ng generate module app-router
installing module
  create src/app/app-router/app-router.module.ts
  WARNING Module is generated but not provided, it must be provided to be used
```

## ルーターモジュールの設定

* RouterModule が標準のルーターモジュール
* アプリケーション用のルーターモジュールの定数でルーティング設定を用意しておき、RouterModule の import と設定を行う
* RouterModule を export することでこのモジュールを import したモジュール側で RouterModule を改めて import する必要がなくなるので（これだけだとそんなに意味はないが）見通しが良くなり責任が適切に分割できる

```ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {AppComponent} from '../app.component';
import {AboutComponent} from '../about/about.component';

const routes: Routes = [
  {path: '', component: IndexComponent},
  {path: 'about', component: AboutComponent},
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRouterModule {
}
```

## ルーターモジュールをメインモジュールから読み込む

```
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 374a2c6..f306a55 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -4,15 +4,19 @@ import { NgModule } from '@angular/core';
 import { AppComponent } from './app.component';
 import { HeaderComponent } from './header/header.component';
 import { AboutComponent } from './about/about.component';
+import {AppRouterModule} from './app-router/app-router.module';
+import { IndexComponent } from './index/index.component';

 @NgModule({
   declarations: [
     AppComponent,
     HeaderComponent,
     AboutComponent,
     IndexComponent
   ],
   imports: [
-    BrowserModule
+    BrowserModule,
+    AppRouterModule
   ],
   providers: [],
   bootstrap: [AppComponent]
```

## ルートパラメータを使用する

### ルート定義でパラメータを受け取るように設定

* FooComponent で `bar` パラメータを受け取る設定を追加

```ts:AppRouterModule.ts
const routes: Routes = [
  {path: '', component: IndexComponent},
  {path: 'about', component: AboutComponent},
  {path: 'foo/:bar', component: FooComponent},
];
```

### ルートパラメータを受け取る

* FooComponent で `bar` パラメータを受け取る

```ts:FooComponent.ts
import {ActivatedRoute} from '@angular/router';

@Component({...})
export class FooComponent implements OnInit {

  bar: string;

  constructor(private route: ActivatedRoute) {
    route.params.subscribe(params => {
      this.bar = params['bar'];
    });
  }

  ngOnInit() {
  }

}
```

# サービスを作成する

* Angular で言うサービスとは何らかのデータを返したり何らかの処理をするレイヤー
* 公式ドキュメントの例だと logging service, data service, tax calculator, application configuration などとあるので、フレームワークの拡張でもドメインロジックでもなんでもサービスとして扱う様子
* 今回はランダムで運勢を教えてくれるおみくじサービスを作ってみる

```
$ ng g service omikuji
installing service
  create src/app/omikuji.service.spec.ts
  create src/app/omikuji.service.ts
  WARNING Service is generated but not provided, it must be provided to be used
```

```ts:omikuji.service.ts
import {Injectable} from '@angular/core';

@Injectable()
export class OmikujiService {

  box: string[] = [
    '大吉',
    '中吉',
    '吉',
    '小吉'
  ];

  constructor() {
  }

  draw(): string {
    return this.box[Math.floor(Math.random() * this.box.length)];
  }

}
```

# サービスをコンポーネントから使用する

* Dependency Injection を使ってコンポーネントにサービスを割り当てて使用する
* Dependency Injection とは依存性の注入と訳され、Angular ではサービスや任意のクラスを DI の対象として設定しておくと、初期化と割り当てをフレームワークが自動で行ってくれる

## サービスを DI の対象にする

* AppModule の @NgModule デコレータの設定の providers に OmikujiService を登録するとアプリケーション全体で OmikujiService が DI の対象になる

```
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index f306a55..37fdd84 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -6,6 +6,7 @@ import { HeaderComponent } from './header/header.component';
 import { AboutComponent } from './about/about.component';
 import {AppRouterModule} from './app-router/app-router.module';
 import { IndexComponent } from './index/index.component';
+import {OmikujiService} from './omikuji.service';

 @NgModule({
   declarations: [
@@ -18,7 +19,7 @@ import { IndexComponent } from './index/index.component';
     BrowserModule,
     AppRouterModule
   ],
-  providers: [],
+  providers: [OmikujiService],
   bootstrap: [AppComponent]
 })
 export class AppModule { }
```

## サービスをコンポーネントに割り当てて使用する

* コンポーネントのコンストラクタの引数に DI 対象のクラスを指定しておくと初期化時に自動で割り当てられ、コンポーネントから使用できるようになる

```
diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html
index e09e2ad..e868060 100644
--- a/src/app/header/header.component.html
+++ b/src/app/header/header.component.html
@@ -9,3 +9,6 @@
   </ng-container>
 </ng-container>

+<p>
+  {{omikujiResult}}
+</p>
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index 5f5301f..be4283b 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -1,4 +1,5 @@
 import {Component, OnInit} from '@angular/core';
+import {OmikujiService} from '../omikuji.service';

 @Component({
   selector: 'app-header',
@@ -17,10 +18,13 @@ export class HeaderComponent implements OnInit {

   showButton = false;

-  constructor() {
+  omikujiResult = '';
+
+  constructor(public omikuji: OmikujiService) {
   }

   ngOnInit() {
+    this.omikujiResult = this.omikuji.draw();
   }

 }
```

# クリックイベントを処理する

* テンプレートで `(click)="something()"` とするとその要素のクリックイベントで something メソッドを実行する
* 今回はクリックでおみくじを引くように修正する

```
diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html
index e868060..51bf2a2 100644
--- a/src/app/header/header.component.html
+++ b/src/app/header/header.component.html
@@ -9,6 +9,6 @@
   </ng-container>
 </ng-container>

-<p>
-  {{omikujiResult}}
-</p>
+<div>
+  <button (click)="draw()">おみくじを引く</button> {{omikujiResult}}
+</div>
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index be4283b..d5c56e2 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -18,13 +18,15 @@ export class HeaderComponent implements OnInit {

   showButton = false;

-  omikujiResult = '';
+  omikujiResult;

   constructor(public omikuji: OmikujiService) {
   }

   ngOnInit() {
-    this.omikujiResult = this.omikuji.draw();
   }

+  draw() {
+    this.omikujiResult = this.omikuji.draw();
+  }
 }
```