Angular 勉強したくなったら 6.0 が出ていたので触ってみた

# はじめに

この記事では Angular GETTING START と TUTORIAL を確認して学んだことを記載します。

TUTORIAL は実例を基にして基礎的なコーディング方法が学べますが、概念的な説明が不足している感があるため、FUNDAMENTALS を適宜確認する必要があると思われます。

尚、既に1カ月近く経過してしまっていますが '18/05/03 に Angular 6 が出たので使うことにしました。

# Angular とは

Angular は HTML と TypeScript で記述されたアプリケーションをビルドするためのプラットフォーム/フレームワークです。Angular 自身も TypeScript で記述されています。

# Angular を実行するための事前準備

Node.js 8以上, npm 5.x をインストールします。

Windows であれば nodist、Mac であれば nodebrew を使うと便利です。
Docker であれば node(official) を使うとよいでしょう。

## Windows ユーザ向け事前準備方法

- nodist をインストールする
    - https://github.com/marcelklehr/nodist/releases
- Node.js, npm をインストールして global バージョンを設定する
    - `$ nodist + 8`
    - `$ nodist global 8`
- Node.js, npm のバージョンを確認する
    - `$ node -v`
    - `$ npm -v`

<font color="gray">※プロンプトが`>`ではないのはコピペした時にうっかりリダイレクトしてしまわないためです。</font>

# Angular CLI をインストールする

まずは Angular CLI をインストールします。

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

Angular CLI は Angular アプリケーションを作成したり、Angular アプリケーションのパーツとなるコンポーネントやサービスを CLI コマンドで生成する機能があります。

# Angular の Hello world アプリケーションを作成する

アプリケーションとして動作するために必要なファイルは `ng new` コマンド一発でもろもろインストールが出来ます。

```sh:アプリケーションを作成する
$ ng new my-app # my-app は適当なアプリケーション名に変える
```

アプリケーション名は次の規則に従う必要があります。
(規則は網羅できているわけではないと思います。一度エラーとなったため備忘録として記載しています。)

- 使える文字は、アルファベットとハイフン `-` である
- アルファベットで始まる

`ng new` は、アプリケーション名のディレクトリを CWD 直下に作成し、その中にプロジェクトの初期ファイルを作成します。

README.md, package.json 等も作成されますが、既にある場合はコマンド実行が進まないので消しておくとよいでしょう。

### ng new で作成されるファイル

`ng new` を実行すると Angular を動作させるために必要なファイルが作成されますが、その他にも、テストに使う Karma の設定ファイル、Lint ツールの TSLint 用の設定ファイル、エディタ整形ルールの EditorConfig 用設定ファイル等が作成されます。

作成されるファイルは [Angular > GETTING STARTED#project-file-review](https://angular.io/guide/quickstart#project-file-review) に説明されてますが、次のとおりです。

```bash:作成されるファイル
src/
  app/
    app.module.ts         # AppModule の定義
                          # -> Angular にアプリケーションのコンパイル方法と起動方法を伝える役割
    app.component.html    # AppComponent のビューテンプレート(ビューテンプレート内はAppComponentコンポーネントの変数が参照できる)
    app.component.spec.ts # AppComponent のユニットテスト
    app.component.ts      # AppComponent のコンポーネント定義
    app.component.css     # AppComponent のスタイルシート
  assets/                 # アプリケーションビルド時にコピーする画像を配置するディレクトリ
    .gitkeep              # Git 用の空ディレクトリをバージョン管理配下に置くためのファイル
  environments/        
    environment.prod.ts   # 本番環境でビルドするための設定
    environment.ts        # 本番以外の環境でビルドするための設定
  favicon.ico             # ブラウザのお気に入り用アイコン
  index.html              # root AppModule のビューファイル
  main.ts                 # アプリケーションのエントリポイント
  polyfills.ts            # Pollyfill設定(ブラウザ毎のサポートレベルを統一するためのもの)
  test.ts                 # ユニットテストのエントリポイント (内部で .spec.ts ファイルを読み込む)
  styles.css              # App 全体の CSS 設定を指定する
  browserslist            # 他の Front-end ツール間でターゲットブラウザを共有するためのファイル
  karma.conf.js           # Karma 用設定ファイル
  tsconfig.app.json       # TypeScript の App 用設定ファイル
  tsconfig.spec.json      # TypeScript のユニットテスト用設定ファイル
  tslint.json             # TSLint の設定ファイル
e2e/                   
  src/                 
    app.e2e-spec.ts       # End-to-End(e2e) テストのエントリポイント
    app.po.ts             # app.e2e-spec.ts から読み込まれるファイル
  protractor.conf.js      # e2e テストの設定ファイル(内部で src/app.e2e-spec.ts を読み込む)
  tsconfig.e2e.json       # TypeScript の e2e テスト用設定ファイル
README.md                 # プロジェクトの README
angular.json              # AngularCLI の設定ファイル
package.json              # npm の設定ファイル
tsconfig.json             # TypeScript の設定ファイル(IDE用)
tslint.json               # TSLint の設定ファイル
.editorconfig             # Editorconfig の設定ファイル
.gitignore                # Git の管理対象外を記述ファイル
```

# アプリケーション開発環境の実行方法 (ng serve)

`ng new` は依存パッケージのインストールも行うため、`ng new` を実行し終わると `ng serve` コマンドにより Angular アプリケーションを起動することが出来ます。

```sh:アプリケーション実行方法
$ cd my-app        # 作成したアプリケーション名のディレクトリが作成されている
$ ng serve --open  # --open を指定するとブラウザで URL が開かれます
```

これで http://localhost:4200/ にアクセスすると Angular のロゴと「Welcome to app!!」が表示されます。

Docker を使っている場合は `$ docker run -it -v -p 4200:4200 node:8 bash` のように TCP 4200 番を publish しておき、 `$ ng serve --host 0.0.0.0 --open` で Angular App を起動するとよいでしょう。
するとブラウザで http://${DockerホストのIPアドレス}:4200/ にアクセスできます。

尚、`ng serve` を実行中にソースコードを編集すると自動でリロードされます。
アプリケーションを開発する際、 `ng serve` で実行してコードを変更することを繰り返すことになります。

# Angular アプリケーションの概要

アプリケーションが起動できたので編集を進めたいと思うでしょうが、その前に少し Fundamentals に記述された概念的な話をします。(理解している人は読み飛ばしてください)

Angular アプリケーションは `NgModule` と呼ばれる**モジュール**によって構成されます。モジュールとは**コンポーネント**をコンパイルするためのコンテキストを定義するものであり、モジュールには起動する際の起点となる**ルートモジュール**が必ず含まれ、その他は**フィーチャモジュール**と呼ばれます。
※ ここでのモジュールは JavaScript(ES2015) のモジュールとは異なります

コンポーネントはビュー（ビューテンプレートやスタイル）や、HTML をレンダリングするために必要となる処理を記述するものです。

その他、特定の HTML をレンダリングするためだけに必要な処理「以外」は**サービス**として定義することができます。サービスはコンポーネントを初期化する際に注入（injection）することができ、コンポーネントモジュールからサービスを利用することが出来るようになります。

※ サービスとコンポーネントを分ける理由は再利用性を高めるためです。(Dependency Injection (DI) と呼ばれる)
※ サービスはサービスプロバイダによりコンポーネントに注入されます

ここで、Tutorial にある Hero 名を記録して表示・検索する Web アプリケーションの中から、Hero 一覧ページを表示する heroes.component.ts を例にとって、ルートモジュールとコンポーネントに記載される内容を紹介します。

angular-cli にコンポーネントを生成する機能があるため使うとよいでしょう。generate オプションを使って以下のように生成できます。また、ルートモジュールに組み込むところまで行ってくれます。

```sh:HeroesComponentを生成する
$ ng generate component Heroes
CREATE src/app/heroes/heroes.component.css (0 bytes)
CREATE src/app/heroes/heroes.component.html (25 bytes)
CREATE src/app/heroes/heroes.component.spec.ts (628 bytes)
CREATE src/app/heroes/heroes.component.ts (269 bytes)
UPDATE src/app.module.ts (396 bytes)
```

コンポーネントを追加しただけでは、まだブラウザでアクセスした時に表示することはできません。ルーティング設定を追加する必要があります。ルーティング設定には @angular/router を使います。

ルーティング設定を行うにはモジュールの中で `RouterModule::forRoot` を使います。
例中では取り合えず利用するためにルートモジュールに直接定義して利用しますが、ベストプラクティスとして Tutorial にあるようにモジュールとして作成するのがよいでしょう。
(参考: https://angular.io/tutorial/toh-pt5#add-the-approutingmodule)

ルーティング設定が終わると、コンポーネントに `<a routerLink="/heroes">Heroes</a>` を追加すると、作成したコンポーネントに対するリンクが作成できるようになります。このリンクを使ってページ遷移した場合、URL のパスが変わりブラウザをリロードしても遷移したページが読み込まれ、ページを遷移した履歴がブラウザに残るため、戻る・進むボタンでページ遷移が出来ます。

## ルートモジュールの記述例

```ts:ルートモジュール例(src/app/app.module.ts)
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';                // inputのデータバインディングに使う FormsModule を読み込む
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';      // Router ライブラリパッケージを読み込む

import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component'; // Heroesコンポーネントを読み込む

const appRoutes: Routes = [                                  // パスを指す Routes 型インスタンスを作成
  { path: 'heroes', component: HeroesComponent },            // `/heroes` にアクセスした際に HeroesComponent を
                                                             //   読み込み、`<router-outlet />` を
                                                             //   置き換えて View を表示する
];

@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent,           // Heroes コンポーネントをモジュールリストに追加する
  ],
  imports: [
    BrowserModule,
    FormsModule,               // ngModel ディレクティブを使うために FormsModule を指定する
    RouterModule.forRoot(      // Routerモジュールを作成する(provider込み)
      appRoutes,               //   パスを指す Routes 型の引数を設定
      { enableTracing: true }  //   デバッグ用出力を有効にする
    ),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

ルートモジュールには次の特徴があります。

- 慣例としてクラス名を AppModule とし、ファイル名を `src/app/app.module.ts` として定義する
- @NgModuel アノテーションを使い、`declarations` として利用するコンポーネント等、アプリケーションの全体像を指定する
    - Angular はこのモジュールに記載された内容から、アプリケーションをビルドするために必要な構成を読み取る

### `@NgModule` デコレータで定義する内容

- declarations
    - NgModule に関連する directives/pipes のリストを指定する
    - コンポーネントは **必ず唯１つ**の NgModule に紐く必要がある
- imports
    - このモジュールのテンプレートで使う directives/pipes のリストを指定する
    - リストには ModuleWithProviders もまた含まれる
- providers
    - サービス(注入可能(injectable) なオブジェクト)のリストを指定する
- bootstrap
    - モジュールが bootstrap される際に bootstrap されるコンポーネントのリストを指定する    

詳細は https://angular.io/api/core/NgModule を参照のこと。

## ルートコンポーネントのテンプレート記述例

```html:ルートコンポーネントのテンプレート例(src/app/app.component.html)
<base href="/" />
<nav>
  <li>
    <h2><a routerLink="/heroes" routerLinkActive="active">Heroes</a></h2>
  </li>
</nav>
<router-outlet></router-outlet>
```

## コンポーネントの記述例

```ts:コンポーネント例(src/app/heroes/heroes.component.ts)
import { Component, OnInit } from '@angular/core';      // Componentデコレータと OnInitインタフェースの読み込み

import { Hero } from '../hero';                         // Heroモデル定義の読み込み
import { HeroService } from '../hero.service';          // Heroサービスの読み込み

@Component({                                            // @Componentデコレータを使ってコンポーネントを定義
  selector: 'app-heroes',                               //   - コンポーネントが指すHTMLのタグ名を<app-heroes>とする
  templateUrl: './heroes.component.html',               //   - ビューテンプレートとして./heroes.component.htmlを使う
  styleUrls: ['./heroes.component.css']                 //   - スタイルシートとして./heroes.component.cssを使う
})
export class HeroesComponent implements OnInit {        // OnInitインタフェースを継承してHeroesComponentクラスを定義して、exportする

  heroes: Hero[];                                       // Heroモデル配列型のheroes変数を定義
  selectedHero: Hero;                                   // リスト内の選択中の Hero

  constructor(                                          // コンストラクタの定義
    private heroService: HeroService                    // HeroServiceサービスを注入(インスタンスをheroServiceプロパティに代入する)
  ) { }

  ngOnInit() {                                          // lifecycle hook 関数の ngOnInit を実装
    this.getHeroes();
  }

  /* Hero リストを取得する */
  getHeroes(): void {
    this.heroService.getHeroes()                        // heroServiceのgetHeroesメソッドを呼び出し heroes プロパティに代入する
      .subscribe((heroes) => { this.heroes = heroes; });
  }

  /* Hero を選択する */
  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }

  /* Hero リストに Hero を追加する */
  add(name: string): void {
    name = name.trim();
    if (!name) { return; }
    let newHero = new Hero();
    newHero.id = Math.max.apply(null, this.heroes.map(h => h.id)) + 1;
    newHero.name = name;
    this.heroes.push(newHero);                          // リストに Hero を追加 (プロパティ変数を操作しているだけなのでリロードすると消える）
  }

  /* Hero リストから Hero を削除する */
  delete(hero: Hero): void {
    this.heroes = this.heroes.filter(h => h !== hero);  // リストから指定したIDのHeroを削除
  }
}
```

```ts:モデル例(src/app/hero.ts)
export class Hero {
  id: number;
  name: string;
}
```

```css:スタイルシート例(src/app/heroes/heroes.component.css)
button {
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
  font-family: Arial;
}
  : <snip>
```

## テンプレートの例

テンプレートは HTML と Angular Markup を組み合わせたものです。

Angular Markup には**テンプレートディレクティブ**と**バインディングマークアップ**、そして**パイプ**があります。これらは HTML を表示する前に Angular によって処理されるため、表示結果にロジックやデータバインディングを組み込むことが出来ます。

```html:テンプレート例(src/app/heroes/heroes.component.html)
<h2>My Heroes</h2>
<div>
  <label>Hero name:
    <input #heroName />                                        <!-- input ボックスを用意し、入力した値を heroName で扱う -->
  </label>
  <!-- (click) passes input value to add() and then clears the input -->
  <button (click)="add(heroName.value); heroName.value=''">    <!-- click 時にコンポーネントの add メソッドを呼び出す -->
    add
  </button>
</div>
<ul class="heroes">
  <li *ngFor="let hero of heroes">                             <!-- *ngFor で指定した変数 heroes から要素を1つずつ取り出して
                                                                     hero に代入して li とその中の html を繰り返し表示する -->
が参照できる -->
    <span class="badge">{{hero.id}}</span> {{hero.name}}
    <button class="delete" title="delete hero"
      (click)="delete(hero)">x</button>                        <!-- click 時にコンポーネントの delete メソッドを呼び出す -->
  </li>
</ul>
<hr />
<h2>Hero Detail</h2>
<div *ngIf="selectedHero !== null">                            <!-- selectedHero が null ではない場合だけ div とその中の html を表示する -->
  <div>
    <label>Hero name: {{selectedHero.name | uppercase }}</label> <!-- 名前を uppercase パイプにより全て大文字にして表示する -->
  </div>
  <div>ID: {{ selectedHero.id }}</div>                            <!-- ID を表示 -->
  <div>Edit name: <input [(ngModel)]="selectedHero.name" /></div> <!-- selectedHero.name をデータバインディングして即時編集する -->
</dvi>
```

バインディングマークアップには2種類のタイプが存在し、それぞれ次の役割がある。
Angular は2方向(読み書き)データバインディングをサポートしています。

- Event binding
    - ユーザのインプットに応じてアプリケーションデータを更新して応答する
- Property binding
    - アプリケーションデータを HTML に埋め込む

## サービスの例

```ts:サービス例(src/app/hero.service.ts)
import { Injectable } from '@angular/core';
 
import { Observable, of } from 'rxjs';
 
import { Hero } from './hero';
import { HEROES } from './mock-heroes';  // 簡略のためモックデータを読み込む。サーバからデータを取得するのが一般的。

@Injectable({
  providedIn: 'root',                    // root プロバイダによって生成されるよう定義する
})
export class HeroService {
  
  getHeroes(): Observable<Hero[]> {
    return of(HEROES);
  }
}
```

```ts:データのモック(src/app/mock-heroes.ts)
import { Hero } from './hero';

export const HEROES: Hero[] = [
  { id: 11, name: 'Mr. Nice' },
  { id: 12, name: 'Narco' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];
```

# Angular アプリケーション表示例

上記で示した例を記述して Angular を起動すると次の表示結果となる。

|`/`の表示結果|`/heroes`の表示結果|
| --- | --- |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/206493/0d2fb588-b6aa-8834-1133-fa2a71dfcf11.png)|![image.png](https://qiita-image-store.s3.amazonaws.com/0/206493/5821462b-df3b-2638-5ba0-ea32337a49d1.png)|

# 本番へのデプロイ

本番環境で動作させるためには ng build によりファイルをビルドした後、ビルドさせたファイルをコンテンツルートとして設定したHTTPサーバを起動する。

ng serve との違いはビルドサイズが小さいなるよう minify 等が行われる点である。

```sh:本番用html/jsファイルをビルドする
$ ng build --prod
```

上記コマンドが成功すると `dist/my-app` (my-app はアプリケーション名) にファイルがビルドされる。(js ファイルは minify されたりする)

ファイルを適宜デプロイ先に展開した後、apache/nginx 等の HTTP サーバを起動する。
Docker を使っている場合は `$ docker run -p 80:80 -v /usr/local/src/my-app/dist/my-app:/usr/share/nginx/html:ro -d nginx` 等とすればよい。
(例は /usr/local/src/my-app にアプリケーションルートがある場合)

以上で `http://${DockerホストのIPアドレス}` でデプロイしたアプリケーションにアクセスすることが出来る。


# まとめ

- Angular CLI を使って初期アプリケーションを作成することが出来る
- コンポーネントを使ってアプリケーションデータを表示することが出来る
    - テンプレート内では `{{}}` を使って変数が使える
- Angular CLI を使ってコンポーネントを生成することが出来る
- ngModel ディレクティブにより双方向データバインディングが行える
    - ngModel ディレクティブを使うためには FormsModule を読み込む必要がある
- ユーザが Hero を選択した場合だけ詳細情報を表示させた
    - 選択操作はイベントバインディング `(click)` を使った
    - 条件に一致した場合のみ表示させるために `*ngIf` を使った
- 配列を1つずつ取り出しながら全ての Hero リストを表示するために `*ngFor` を使った
- HeroService をサービスプロバイダとして root プロバイダによって生成されるよう定義して、HeroComponent に注入を行った
- コンポーネントの ngOnInit ライフサイクルをフックして Hero リストを取得した
- Angular router を使って異なるコンポーネント間を移動した
    - 移動した履歴はブラウザの進む・戻るで再現できる
    - `<router-outlet>` タグがパスに関連付けられたコンポーネントの表示結果に置き換えられた
    - a タグに `routerLink` ディレクティブをつけてリンクを作成した
- Hero リストを追加・削除・編集可能にした
- 本番環境へデプロイするために `ng build --prod` コマンドを使った