[和訳] 3. Master/Detail (Angular公式チュートリアル)

この投稿は、Angular公式チュートリアルの [3. Master/Detail](https://v4.angular.io/tutorial/toh-pt2) を和訳した記事です。
職場で行うAngular勉強会を円滑に進めることを目的としています。

筆者自身もAngularについては初級者で、英語の知識も乏しいため、記事内容への指摘がありましたら是非コメントをお願いします。

**なお、この記事はAngular v4に対応したチュートリアルの和訳記事です。**
**最新版のチュートリアルではありませんのでご注意ください。**

【チュートリアル一覧】
_1. Introduction_ : [和訳ページ](https://qiita.com/Imamotty/items/75f2974d95acc15359fd) / [原文](https://v4.angular.io/tutorial)
_2. The Hero Editor_ : [和訳ページ](https://qiita.com/Imamotty/items/5b9290fb674b6feda58c) / [原文](https://v4.angular.io/tutorial/toh-pt1)
**_3. Master/Detail_ : このページです / [原文](https://v4.angular.io/tutorial/toh-pt2)**
_4. Multiple Components_ : [和訳ページ](https://qiita.com/Imamotty/items/796338e1e3224eb9ba34) / [原文](https://v4.angular.io/tutorial/toh-pt3)
_5. Services_ : 和訳ページ作成中 / [原文](https://v4.angular.io/tutorial/toh-pt4)
_6. Routing_ : 和訳ページ作成中 / [原文](https://v4.angular.io/tutorial/toh-pt5)
_7. HTTP_ : 和訳ページ作成中 / [原文](https://v4.angular.io/tutorial/toh-pt6)

***
# Master/Detail
このページでは、`Tour of Heroes`アプリをヒーローのリストを表示するよう発展させて、
ユーザーがヒーローを選択してヒーローの詳細を表示できるようにします。

このページが完成したら、アプリはこの例のようになります。
https://v4.angular.io/generated/live-examples/toh-pt2/eplnkr.html

## 前回どこで終わったか
`Tour of Heroes`チュートリアルのこのページを続行する前に、
前回の`The Hero Editor`ページの後に次のファイル構成になっていることを確認してください。
あなたのファイル構成が一致しない場合は、そのページに戻ってあなたが逃したものを見つけ出してください。

<img width="695" alt="スクリーンショット 2017-10-28 15.14.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/26577/663d55bc-5152-8ad0-707f-32263b5876c6.png">

## アプリをコンパイル・実行し続ける
ターミナル画面で以下のコマンドを入力します。

```bash
npm start
```

このコマンドは、「watchモード」でTypeScriptコンパイラを実行し、コードが変更されたときに自動的に再コンパイルします。
また、ブラウザでアプリを同時に起動し、コードが変更されたときにブラウザを更新します。

ブラウザを再コンパイルまたは更新するために一時停止することなく、
_Tour of Heroes_ を構築し続けることができます。

## ヒーローを表示する
ヒーローのリストを表示するために、ヒーローをビューのテンプレートに追加します。

### ヒーローを作成する
10人のヒーローの配列を作成します。

```TypeScript:src/app/app.component.ts(hero_array)
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' }
];
```
`HEROES`配列は、前のページで定義された`Hero`型のクラスです。
最終的にこのアプリはウェブサービスからヒーローのリストを取得しますが、
今のところはモックのヒーローを表示できるようにします。

### ヒーローを公開する
`AppComponent`にpublicプロパティを作成し、データバインディング用にヒーロー配列を公開します。

```TypeScript:app.component.ts(hero_array_property)
heroes = HEROES;
```

`heroes`プロパティの型は、TypeScriptが`HEROES`配列から推論するため、定義されていません。

> 最終的にヒーロー名はデータサービスから取得するため、
> ヒーローデータはクラス実装から分離されます。

### テンプレートにヒーロー名を表示する
順序付けられていないリストにヒーロー名を表示するために、タイトルの下、ヒーロー詳細の上に次のHTMLを挿入します。

```app.component.ts(heroes_template)
<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <!-- each hero goes here -->
  </li>
</ul>
```

これでテンプレートにヒーローの名前を入力できます。

### ngForでヒーローを一覧表示する
目標は、コンポーネントのヒーローの配列をテンプレートにバインドし、それらを反復処理して個々に表示することです。

組込みディレクティブ`*ngFor`を追加して、`<li>`タグを変更します。

```app.component.ts(ngFor)
<li *ngFor="let hero of heroes">
```

> `ngFor`への接頭辞`*`は、この構文の重要な部分です。 `<li>`要素とその子要素がマスターテンプレートを構成することを示します。
>
> `ngFor`ディレクティブは、`AppComponent`の`hero`配列を繰り返し処理し、その配列のヒーローごとにこのテンプレートのインスタンスをレンダリングします。
>
> `let hero`部分は、テンプレート入力変数として`hero`を識別します。これは、各繰り返しの現在のヒーロー項目を保持します。 この変数をテンプレート内で参照すると、現在の`hero`のプロパティにアクセスできます。
>
> `ngFor`とテンプレート入力変数の詳細については、[Displaying Data](https://v4.angular.io/guide/displaying-data)ページの[Showing an array property with *ngFor](https://v4.angular.io/guide/displaying-data#ngFor)と[Template Syntax](https://v4.angular.io/guide/template-syntax)ページの[ngFor](https://v4.angular.io/guide/template-syntax#ngFor)を参照してください。

`<li>`タグ内で、テンプレート変数`hero`を使用してヒーローのプロパティを表示するコンテンツを追加します。

```app.component.ts(ngFor_template)
<li *ngFor="let hero of heroes">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
```

ブラウザが更新されると、ヒーローのリストが表示されます。

### ヒーローのスタイル
ユーザーは、どのヒーローにカーソルを合わせているのか、どのヒーローを選択しているのかを視覚的に確認する必要があります。

コンポーネントにスタイルを追加するには、`@Component`デコレータの`styles`プロパティに次のCSSを設定します。

```src/app/app.component.ts(styles)
styles: [`
  .selected {
    background-color: #CFD8DC !important;
    color: white;
  }
  .heroes {
    margin: 0 0 2em 0;
    list-style-type: none;
    padding: 0;
    width: 15em;
  }
  .heroes li {
    cursor: pointer;
    position: relative;
    left: 0;
    background-color: #EEE;
    margin: .5em;
    padding: .3em 0;
    height: 1.6em;
    border-radius: 4px;
  }
  .heroes li.selected:hover {
    background-color: #BBD8DC !important;
    color: white;
  }
  .heroes li:hover {
    color: #607D8B;
    background-color: #DDD;
    left: .1em;
  }
  .heroes .text {
    position: relative;
    top: -3px;
  }
  .heroes .badge {
    display: inline-block;
    font-size: small;
    color: white;
    padding: 0.8em 0.7em 0 0.7em;
    background-color: #607D8B;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -4px;
    height: 1.8em;
    margin-right: .8em;
    border-radius: 4px 0 0 4px;
  }
`]
```

複数行の文字列にはバックックォート記法を使用することを忘れないでください。

これらのスタイルを追加すると、ファイルがかなり長くなります。 
後のページでは、スタイルを別のファイルに移動します。

コンポーネントにスタイルを割り当てると、そのコンポーネントはそのコンポーネントに適用されます。 
これらのスタイルは`AppComponent`にのみ適用され、外側のHTMLには影響しません。

ヒーローを表示するためのテンプレートは、次のようになっているはずです。

```src/app/app.component.ts(styled_heroes)
<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>
```

## ヒーローを選ぶ
このアプリでは、詳細ビューにヒーローのリストと単一のヒーローが表示されるようになりました。
しかし、リストと詳細ビューは接続されていません。
ユーザーがリストからヒーローを選択すると、選択したヒーローが詳細ビューに表示されるべきです。
このUIパターンは**_Master/Detail_**と呼ばれます。 
この場合、_Master_はヒーローリストであり、_Detail_は選択されたヒーローです。

次に、クリックイベントにバインドされた`AppComponent`の`selectedHero`プロパティを通じて、_Master_を_Detail_に接続します。

### クリックイベントを処理する
`<li>`要素へのクリックイベントのバインディングを次のように追加します。

```app.component.ts(template抜粋)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
  ...
</li>
```

丸括弧`(click)`は`<li>`要素のクリックイベントをターゲットとして識別します。
`onSelect（hero）`は`AppComponent`の`onSelect（）`メソッドを呼び出し、テンプレート入力変数`hero`を引数として渡します。
これは`ngFor`ディレクティブで以前定義したのと同じ`hero`変数です。

> イベントバインディングの詳細については、[User Input](https://v4.angular.io/guide/user-input)ページおよび、[Template Syntax](https://v4.angular.io/guide/template-syntax)ページの[Event binding](https://v4.angular.io/guide/template-syntax#event-binding)を参照してください。

### 選択したヒーローを表示するクリックハンドラを追加する
`hero`プロパティはもう必要ありません。
なぜなら、あなたはもう1人のヒーローだけを表示しておらず、ヒーローのリストを表示しているからです。
しかし、ユーザーはヒーローのリストをクリックすることでヒーローの1人を選択することができます。
`hero`プロパティを以下の単純な`selectedHero`プロパティで置き換えましょう。

```TypeScript:src/app/app.component.ts(selectedHero)
selectedHero: Hero;
```

ヒーローの名前は、ユーザーがヒーローを選択する前にはすべて選択解除する必要があります。
そのため、`selectedHero`は`hero`と同じように初期化することはありません。

`selectedHero`プロパティにユーザーがクリックした`<li>`要素の`hero`を設定する`onSelect()`メソッドを追加します。

```TypeScript:src/app/app.component.ts(onSelect)
onSelect(hero: Hero): void {
  this.selectedHero = hero;
}
```

テンプレートはまだ古いヒーローのプロパティを参照しています。
代わりに、次のように新しい`selectedHero`プロパティにバインドします。

```app.component.ts(template抜粋)
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
    <label>name: </label>
    <input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
```

### ngIfで空の詳細ビューを非表示にする
アプリが読み込まれると、`selectedHero`は未定義です。
選択されたヒーローは、ユーザーがヒーローの名前をクリックすると初期化されます。
Angularは未定義の`selectedHero`プロパティを表示できず、ブラウザのコンソールに次のエラーがスローされます。

```
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
```

テンプレートには`selectedHero.name`が表示されますが、
選択されたヒーローが存在するまで、ヒーローの詳細をDOMから外しておく必要があります。

テンプレートのヒーロー詳細コンテンツHTMLを`<div>`で囲みます。
その後、Angular組み込みの`ngIf`ディレクティブを追加し、その値として`AppComponent`の`selectedHero`プロパティを設定します。

```src/app/app.component.ts(ngIf)
<div *ngIf="selectedHero">
  <h2>{{selectedHero.name}} details!</h2>
  <div><label>id: </label>{{selectedHero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="selectedHero.name" placeholder="name"/>
  </div>
</div>
```

> `ngIf`の前にアスタリスク`*`を忘れないでください。

アプリはもうエラーをスローしなくなり、ブラウザにはヒーロー名のリストが再び表示されます。

選択されたヒーローが存在しない場合、`ngIf`ディレクティブはヒーローの詳細HTMLをDOMから削除します。 
ヒーローの詳細要素も心配されたバインディングもありません。

ユーザーがヒーローを選択すると、`selectedHero`が定義され、`ngIf`はヒーローの詳細コンテンツをDOMに入れ、ネストされたバインディングを評価します。

> `ngIf`と`ngFor`の詳細については、[Structural Directives](https://v4.angular.io/guide/structural-directives)ページと、[Template Syntax](https://v4.angular.io/guide/template-syntax)ページの[Built-in directives](https://v4.angular.io/guide/template-syntax#directives)を参照してください。

### 選択されたヒーローにスタイルを適用する
選択されたヒーローの詳細はリストの下に表示されますが、
リストの中から選択されたヒーローを見つけることは困難です。

上記で追加した`styles`メタデータには、`selected`という名前のカスタムCSSクラスがあります。
選択したヒーローをより目立たせるために、ユーザーがヒーロー名をクリックすると、この`selected`クラスを`<li>`に適用させます。
たとえば、ユーザーが「Magneta」をクリックすると、次のように独特で繊細な背景色でレンダリングされるはずです。

![heroes-list-selected.png](https://qiita-image-store.s3.amazonaws.com/0/26577/3f2bd234-4da2-74fa-1710-493a5406ec1c.png)

テンプレートで、次の`[class.selected]`バインディングを`<li>`に追加します。

```app.component.ts(setting_the_CSS_class)
[class.selected]="hero === selectedHero"
```

`hero === selectedHero`という式が`true`の場合、Angularは`selected`クラスを追加します。
式が`false`の場合、Angularは選択した`selected`クラスを削除します。

> クラスバインディングの詳細については、[Template Syntax](https://v4.angular.io/guide/template-syntax#ngClass)ページでの説明を参照してください。

`<li>`の最終版は次のようになります。

```app.component.ts(styling_each_hero)
<li *ngFor="let hero of heroes"
  [class.selected]="hero === selectedHero"
  (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
```

"Magneta"をクリックすると、リストは次のようになります。

![heroes-list-1.png](https://qiita-image-store.s3.amazonaws.com/0/26577/562063aa-e73c-1e2c-0ea5-c827174fc1d0.png)

`app.component.ts`全体は以下のようになります。

```TypeScript:src/app/app.component.ts
import { Component } from '@angular/core';

export class Hero {
  id: number;
  name: string;
}

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' }
];

@Component({
  selector: 'my-app',
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
      <li *ngFor="let hero of heroes"
        [class.selected]="hero === selectedHero"
        (click)="onSelect(hero)">
        <span class="badge">{{hero.id}}</span> {{hero.name}}
      </li>
    </ul>
    <div *ngIf="selectedHero">
      <h2>{{selectedHero.name}} details!</h2>
      <div><label>id: </label>{{selectedHero.id}}</div>
      <div>
        <label>name: </label>
        <input [(ngModel)]="selectedHero.name" placeholder="name"/>
      </div>
    </div>
  `,
  styles: [`
    .selected {
      background-color: #CFD8DC !important;
      color: white;
    }
    .heroes {
      margin: 0 0 2em 0;
      list-style-type: none;
      padding: 0;
      width: 15em;
    }
    .heroes li {
      cursor: pointer;
      position: relative;
      left: 0;
      background-color: #EEE;
      margin: .5em;
      padding: .3em 0;
      height: 1.6em;
      border-radius: 4px;
    }
    .heroes li.selected:hover {
      background-color: #BBD8DC !important;
      color: white;
    }
    .heroes li:hover {
      color: #607D8B;
      background-color: #DDD;
      left: .1em;
    }
    .heroes .text {
      position: relative;
      top: -3px;
    }
    .heroes .badge {
      display: inline-block;
      font-size: small;
      color: white;
      padding: 0.8em 0.7em 0 0.7em;
      background-color: #607D8B;
      line-height: 1em;
      position: relative;
      left: -1px;
      top: -4px;
      height: 1.8em;
      margin-right: .8em;
      border-radius: 4px 0 0 4px;
    }
  `]
})
export class AppComponent {
  title = 'Tour of Heroes';
  heroes = HEROES;
  selectedHero: Hero;

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }
}
```

## まとめ
このチュートリアルであなたは以下のことを達成しました。

* `Tour of Heroes`アプリは選択可能なヒーローのリストを表示します。
* あなたはヒーローを選択し、ヒーローの詳細を表示する機能を追加しました。
* コンポーネントのテンプレートで組み込みディレクティブ`ngIf`と`ngFor`を使用する方法を学びました。

あなたのアプリは、今このようになっているでしょう。
https://v4.angular.io/generated/live-examples/toh-pt2/eplnkr.html

## 次のステップ
`Tour of Heroes`アプリの機能を拡張しましたが、まだ完成ではありません。
アプリは巨大な1つのコンポーネントだけであってはいけません。
[次のページ](https://qiita.com/Imamotty/items/796338e1e3224eb9ba34)では、アプリケーションをサブコンポーネントに分割して連携させます。