Skip to content

Instantly share code, notes, and snippets.

@dolpen
Last active April 25, 2016 06:31
Show Gist options
  • Save dolpen/80ab78d2d6733d108bc7aed8da7a4c84 to your computer and use it in GitHub Desktop.
Save dolpen/80ab78d2d6733d108bc7aed8da7a4c84 to your computer and use it in GitHub Desktop.

#一覧画面と詳細画面

ヒーローの一覧について、一覧と詳細の画面を作る。

たくさんのヒーロー達

我々にはたくさんのヒーローが必要である。 このアプリケーションを、ヒーローの一覧を表示できるように拡張し、ユーザーがヒーローを選択できるように、 そして選択されたヒーローの詳細情報を表示できるようにしよう。

今回作るもののデモ

まずリストの表示に必要なことを整理してみよう。 まずヒーローの一覧データが必要だ。 一覧データをテンプレートに流し込んで表示したいので、その手段も必要である。

前回までのあらすじ

パート2を続ける前に、まずパート1が終わった時のフォルダ構造を確認してみよう。 もし以下のようになっていないなら、パート1に戻ってどこが足りないのかを探してみてほしい。

angular2-tour-of-heroes
├app
│├app.component.ts
│└main.ts
├node_modules ...
├typings ...
├index.html
├package.json
├styles.css
├tsconfig.json
└typings.json

npm start で TypeScript の自動コンパイルとアプリケーションのリロードを有効にしておこう。 このコマンドは TypeScript のコンパイラを watch mode で動かし、サーバーを動作させる。 アプリケーションをブラウザで起動し、開発中にアプリケーションを動作させたまま更新を反映させることができる。

ヒーロー達を表示する

ヒーロー達(のデータ)を作る

app.component.ts の最下部に、10人のヒーローの**配列(Array / List)**を作ろう。

var 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" }
];

HEROS という配列は、パート1で定義した Hero 型の配列であり、ヒーローたちの配列を作るためのものである。 (本来は)このヒーローのリスト(配列)はウェブサービスから取得することを目指したいが、 まず最初のステップとして、モック(ダミー)のヒーロー達を表示してみよう。

ヒーローを露出させる

AppComponent のプロパティを作って、ヒーロー達をバインディングできるように露出させよう。

heroes = HEROES;

heroes の型を定義する必要はない。 TypeScript がHEROES から推測できるのだ。 (注釈: HEROESHero[] 型を持っているので、それを代入される heroes もまた Hero[] 型に違いない、ということを TypeScript のコンパイラが判断する)

このコンポーネントクラス( AppComponent )にヒーロー達が定義された。 しかし最終的に我々はデータサービスからヒーロー達のデータを得るだろうことを知っている。 我々の最終目的(別のウェブサービスなどからデータを受け取ること)を分かっていれば、コンポーネントクラスの実装の中にヒーロー達のデータを書くことなく、別の場所に最初から分離しておくという形はとても理にかなっているのだ。

ヒーロー達をテンプレート内に表示する

コンポーネント( AppComponent )は heros というプロパティを持っている。 これら表示するようなリストを作ってみよう。次に記載する HTML の断片をタイトルと詳細の間に入れてみよう。

<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <!-- ここにヒーロー1人分の情報が入る -->
  </li>
</ul>

これで、ヒーローの情報を埋めることができるテンプレートになった。

ngForディレクティブでヒーロー達を一覧する

コンポーネント内のヒーローの配列データを、反復して1人ずつテンプレートに流し込み、個別で表示させたい。 ここで Angular の助けが必要になる。順を追ってやっていこう。

まず、 Angular の標準ディレクティブである *ngFor を使って、<li> の中身を変えてみよう。

<li *ngFor="#hero of heroes">

ここで ngFor の前についているアスタリスク( * )は、文法上非常に重要な記号である。

ngFor の前についているアスタリスク( * )は、 li 要素、およびその子要素(タグの中身全部)が繰り返すテンプレートを構成していることを示す。
この例において、 ngForAppComponent.heroes に入っているヒーローの配列を繰り返し処理し、テンプレートの実体(インスタンス)を複製する。
その後ろのダブルクォートで囲まれた文字列( "#hero of heroes" )は「ヒーローそれぞれに対応したテンプレートの実体から使えるように、 hero というローカル変数を作って、対応するヒーローのデータを割り当てなさい」という意味である。
#hero の前についた # 記号はローカル変数を定義する構文である。(繰り返される)テンプレート内では hero という名前で各ヒーローのデータが参照可能になる。

ngFor やテンプレート内ローカル変数については、データの表示およびテンプレート構文の章で、より深く学べるだろう。

これで個別のヒーローの情報を <li> の内側に( hero をテンプレート変数として使うことで)表示できる。

<li *ngFor="#hero of heroes">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

ブラウザを更新したら、ヒーローの一覧が見えるだろう。

ヒーロー達(の表示)を装飾する

現状ヒーローたちはかなりシンプルに、そっけなく表示されている。 私たちは一覧されているヒーローにマウスオーバーした(hover)時や選択された(selected)時に、それが分かるように視覚的な振る舞いを付けたい。

@Component の宣言に styles プロパティを追加し、以下のCSSのクラスを設定することで、コンポーネントに見た目を追加してみよう。

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 だけに適用され、コンポーネントで管理されていない他の HTML に「漏れる」ことはない。 (注釈:動作を見てみると、コンポーネントの HTML 要素に独自の属性を付けて、その属性に絞り込むセレクタを追加した CSS を <style> で出力していた)

この見た目を反映するため、テンプレートは以下のようになっている必要がある。

<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="#hero of heroes">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

たくさんのスタイル(class属性のこと?)があります。 コンポーネントの中に直接スタイルシートを書き込むこともできるし、コンポーネントのコードの見通しを良くするためにスタイルシートだけ別のファイルに出すということもできる。 後の章でそれをやるのだが、今のところはコンポーネントの中に書いたままにしておこう。

ヒーローを選択する

ヒーローのリストがあり、またアプリには1人のヒーローの詳細が表示されている。 その表示は今のところ関連していない。 ここでは、ユーザーがヒーローを選択して、そのヒーローの詳細が表示されるようにしたい。 このUIパターンは、広く「一覧-詳細」として知られている。 このアプリで言えば、マスターデータはヒーローのリストのことであり、詳細データは、選択したヒーローのデータのことである。

コンポーネントに selectedHero というプロパティを追加し、クリックイベントに関連させることで、マスターデータと詳細データを関連づけてみよう。

クリックイベント

<li> に Angular のイベントバインディング用の属性を追加して、クリックイベントを割り当ててみる。

<li *ngFor="#hero of heroes" (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

イベントバインディング用の仕組みに注目しよう。

(click)="onSelect(hero)"

(click)<li> 要素のクリックイベントを示している。 右側の "onSelect(hero)" は、 AppComponent に定義される(まだされていない) onSelect というメソッドをテンプレート変数の hero を渡して呼び出している。 この hero は、先ほど *ngFor で定義したテンプレートのローカル変数である。

イベントのバインディングについては、ユーザー入力およびテンプレート構文の章で、より深く学べるだろう。

クリックしたときのイベントハンドラを追加する

上で追加したイベントバインディングは、まだ存在しない onSelect メソッドを呼び出そうとしている。 そこで、コンポーネントにこのメソッドを追加していく。 このメソッドでは一体何をすべきだろうか? ユーザーがクリックしたヒーローをコンポーネントの中に「選択されたヒーロー」としてセットする必要がある。 今はまだコンポーネントは「選択されたヒーロー」の概念を持っていない。ここから始めよう。

選択されたヒーローを露出させる

そういえば、 AppComponent の中の hero プロパティは必要ないものだった。 (注釈:繰り返しテンプレートの内側で使われるローカル変数 hero とは別物。コンポーネントから取れる方) これを書き換えて selectedHero というプロパティにしてしまおう。

selectedHero: Hero;

まず、ユーザーが何らかのヒーローを選択する前に誰も選択された状態にしない、ということを決めよう(Part0のデモもそうなっている)。 そのために selectedHero はどのヒーローでも初期化されないようにする(予め代入などをしない)。

そして、ユーザーがクリックしたときに selectedHero がセットされるように onSelect メソッドを追加する。

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

今度は選択されたヒーローがテンプレート上に表示されるようにする。 そういえばヒーローの詳細情報を表示する部分は、まだ hero というプロパティを使ってしまっていたのだった。 これを修正して selectedHero から値をバインドするようにする。

<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 は未定義になる。 これが、ブラウザのデバッグコンソールにこのようなエラーが出てしまう原因になる。

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

テンプレート内部で selectedHero.name を表示するようにしていたことを思い出そう。 (起動時は) selectedHero 自体が未定義なので、その中の name プロパティももちろん存在しないのだ。 ヒーローが選択されるまで、ヒーローの詳細情報を DOM に出さないようにすることで、この問題に対処していく。

ヒーローの詳細情報のテンプレートを <div> で囲んでみよう。 そして ngIf というディレクティブを追加し、そこにコンポーネントの selectedHero プロパティをセットする。

<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 の前についているアスタリスク( * )は文法上非常に重要な記号である。

selectedHero がないとき、ngIf ディレクティブはヒーローの詳細 HTML を DOM から外してしまう。 これで、ヒーローがないときにプロパティのバインディングに困ることがなくなった。 ユーザーがヒーローを選択したとき、 selectedHero が"truthy"(true として評価される値)になり ngIf ディレクティブはヒーローの詳細を出力するようになる、そうなって初めて内側のバインディングが行われる(evaluates)。

ngIfngFor は DOM 構造を変更できるディレクティブのため 構造ディレクティブ と呼ばれる。 つまり、これらのディレクティブは Angular が表示するコンテンツの DOM 構造を与えてくれるものである。

ngIfngFor 、他の構造ディレクティブについては、構造ディレクティブおよびテンプレート構文の章で、より深く学べるだろう。

ブラウザは更新され、ヒーローの一覧が表示され、詳細情報は隠れるだろう。 ngIfselectedHero が未定義の間は詳細情報を DOM から外している。 一覧されたヒーローをクリックすると、選択したヒーローの詳細が表示される。 全てが意図通りに動いた。

選択した時に見た目を変える

選択したヒーローの詳細情報を下に表示することができたが、そのヒーローが上部のリストのどれに当たるのかが、パッと見て判りづらい。 リストの <li> タグに対して、適切に selected というCSSのクラスを適用することによって見て分かるようにしよう。 例えば、リストの「Magneta」というヒーローを選択したときに、このように少し背景色を薄くすることによって、他とは視覚的に違いが分かるようにできる。

Selected hero

テンプレートのHTMLに selected というCSSのクラスを反映するための class プロパティを追加する。 そして現在の selectedHero が(テンプレート内ローカル変数の) hero と同一であるかどうかを比較する式を書き込む。

キーはCSSのクラス名( selected )である。 値は selectedHerohero が同一なら true になり、そうでなければ false になる。 つまりこれは「 selectedHerohero が同一なら <li>selected というCSSのクラスを付けよ、そうでなければ外せ」ということを言っているのだ。

[class.selected]="hero === selectedHero"

テンプレート内の class.selected[] で囲われていることを覚えてほしい。 これは、データプロパティバインディングの文法である。 そのためのデータはデータソースから一方通行で流れてくる。 (注釈: 今回のデータソースとは、AppComponent のプロパティとテンプレート内変数で hero === selectedHero であるかどうかを評価した結果である)

<li *ngFor="#hero of heroes"
  [class.selected]="hero === selectedHero"
  (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

プロパティのバインディングについては、テンプレート構文の章で、より深く学べるだろう。

ブラウザはリロードされ、「Magneta」を選ぶと背景色が変わり、選択状態が良くわかるようになるだろう。

Output of heroes list app

別のヒーローを選択すると、そのヒーローに選択状態の色が移る。

最終的な app.component.ts はこのようになっているはずだ。

import {Component} from 'angular2/core';
export class Hero {
  id: number;
  name: string;
}
@Component({
  selector: 'my-app',
  template:`
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
      <li *ngFor="#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) { this.selectedHero = hero; }
}
var 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" }
];

今回のまとめ

この章で我々が得たものは、以下のようにまとめられる。

  • アプリケーション上でヒーローが一覧表示され、選択することができる。
  • ヒーローを選択したときに、そのヒーローの詳細情報が表示できるようになる。
  • コンポーネントのテンプレートで ngIfngFor を使う方法を学んだ。

今回作ったもののデモ

次回

アプリケーションは成長してきたが、完成にはほど遠い。 単一のコンポーネントにアプリケーション全体の動作を記述することはできない。 そのため、アプリケーションを複数のサブコンポーネントに分割する必要がある。 その方法について、次回で作業を行いながら説明していく。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment