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

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

More than 1 year has passed since last update.

この投稿は、Angular公式チュートリアルの 3. Master/Detail を和訳した記事です。
職場で行うAngular勉強会を円滑に進めることを目的としています。

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

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

【チュートリアル一覧】
1. Introduction : 和訳ページ / 原文
2. The Hero Editor : 和訳ページ / 原文
3. Master/Detail : このページです / 原文
4. Multiple Components : 和訳ページ / 原文
5. Services : 和訳ページ作成中 / 原文
6. Routing : 和訳ページ作成中 / 原文
7. HTTP : 和訳ページ作成中 / 原文


Master/Detail

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

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

前回どこで終わったか

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

スクリーンショット 2017-10-28 15.14.21.png

アプリをコンパイル・実行し続ける

ターミナル画面で以下のコマンドを入力します。

npm start

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

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

ヒーローを表示する

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

ヒーローを作成する

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

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プロパティを作成し、データバインディング用にヒーロー配列を公開します。

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ディレクティブは、AppComponenthero配列を繰り返し処理し、その配列のヒーローごとにこのテンプレートのインスタンスをレンダリングします。

let hero部分は、テンプレート入力変数としてheroを識別します。これは、各繰り返しの現在のヒーロー項目を保持します。 この変数をテンプレート内で参照すると、現在のheroのプロパティにアクセスできます。

ngForとテンプレート入力変数の詳細については、Displaying DataページのShowing an array property with *ngForTemplate 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は選択されたヒーローです。

次に、クリックイベントにバインドされたAppComponentselectedHeroプロパティを通じて、MasterDetailに接続します。

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

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

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

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

イベントバインディングの詳細については、User Inputページおよび、Template SyntaxページのEvent bindingを参照してください。

選択したヒーローを表示するクリックハンドラを追加する

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

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

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

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

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ディレクティブを追加し、その値としてAppComponentselectedHeroプロパティを設定します。

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に入れ、ネストされたバインディングを評価します。

ngIfngForの詳細については、Structural Directivesページと、Template SyntaxページのBuilt-in directivesを参照してください。

選択されたヒーローにスタイルを適用する

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

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

heroes-list-selected.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ページでの説明を参照してください。

<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

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

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アプリは選択可能なヒーローのリストを表示します。
  • あなたはヒーローを選択し、ヒーローの詳細を表示する機能を追加しました。
  • コンポーネントのテンプレートで組み込みディレクティブngIfngForを使用する方法を学びました。

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

次のステップ

Tour of Heroesアプリの機能を拡張しましたが、まだ完成ではありません。
アプリは巨大な1つのコンポーネントだけであってはいけません。
次のページでは、アプリケーションをサブコンポーネントに分割して連携させます。

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
ユーザーは見つかりませんでした