#一覧画面と詳細画面
ヒーローの一覧について、一覧と詳細の画面を作る。
我々にはたくさんのヒーローが必要である。 このアプリケーションを、ヒーローの一覧を表示できるように拡張し、ユーザーがヒーローを選択できるように、 そして選択されたヒーローの詳細情報を表示できるようにしよう。
まずリストの表示に必要なことを整理してみよう。 まずヒーローの一覧データが必要だ。 一覧データをテンプレートに流し込んで表示したいので、その手段も必要である。
パート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
から推測できるのだ。
(注釈: HEROES
が Hero[]
型を持っているので、それを代入される heroes
もまた Hero[]
型に違いない、ということを TypeScript のコンパイラが判断する)
このコンポーネントクラス(
AppComponent
)にヒーロー達が定義された。 しかし最終的に我々はデータサービスからヒーロー達のデータを得るだろうことを知っている。 我々の最終目的(別のウェブサービスなどからデータを受け取ること)を分かっていれば、コンポーネントクラスの実装の中にヒーロー達のデータを書くことなく、別の場所に最初から分離しておくという形はとても理にかなっているのだ。
コンポーネント( AppComponent
)は heros
というプロパティを持っている。
これら表示するようなリストを作ってみよう。次に記載する HTML の断片をタイトルと詳細の間に入れてみよう。
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<!-- ここにヒーロー1人分の情報が入る -->
</li>
</ul>
これで、ヒーローの情報を埋めることができるテンプレートになった。
コンポーネント内のヒーローの配列データを、反復して1人ずつテンプレートに流し込み、個別で表示させたい。 ここで Angular の助けが必要になる。順を追ってやっていこう。
まず、 Angular の標準ディレクティブである *ngFor
を使って、<li>
の中身を変えてみよう。
<li *ngFor="#hero of heroes">
ここで ngFor
の前についているアスタリスク( *
)は、文法上非常に重要な記号である。
ngFor
の前についているアスタリスク(*
)は、li
要素、およびその子要素(タグの中身全部)が繰り返すテンプレートを構成していることを示す。
この例において、ngFor
はAppComponent.heroes
に入っているヒーローの配列を繰り返し処理し、テンプレートの実体(インスタンス)を複製する。
その後ろのダブルクォートで囲まれた文字列("#hero of heroes"
)は「ヒーローそれぞれに対応したテンプレートの実体から使えるように、hero
というローカル変数を作って、対応するヒーローのデータを割り当てなさい」という意味である。
#hero
の前についた#
記号はローカル変数を定義する構文である。(繰り返される)テンプレート内ではhero
という名前で各ヒーローのデータが参照可能になる。
これで個別のヒーローの情報を <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>
アプリケーションを最初に起動したときにヒーローの一覧が出るが、ヒーローは選択されていない。
このとき 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)。
ngIf
や ngFor
は DOM 構造を変更できるディレクティブのため 構造ディレクティブ と呼ばれる。
つまり、これらのディレクティブは Angular が表示するコンテンツの DOM 構造を与えてくれるものである。
ngIf
やngFor
、他の構造ディレクティブについては、構造ディレクティブおよびテンプレート構文の章で、より深く学べるだろう。
ブラウザは更新され、ヒーローの一覧が表示され、詳細情報は隠れるだろう。
ngIf
は selectedHero
が未定義の間は詳細情報を DOM から外している。
一覧されたヒーローをクリックすると、選択したヒーローの詳細が表示される。
全てが意図通りに動いた。
選択したヒーローの詳細情報を下に表示することができたが、そのヒーローが上部のリストのどれに当たるのかが、パッと見て判りづらい。
リストの <li>
タグに対して、適切に selected
というCSSのクラスを適用することによって見て分かるようにしよう。
例えば、リストの「Magneta」というヒーローを選択したときに、このように少し背景色を薄くすることによって、他とは視覚的に違いが分かるようにできる。
テンプレートのHTMLに selected
というCSSのクラスを反映するための class
プロパティを追加する。
そして現在の selectedHero
が(テンプレート内ローカル変数の) hero
と同一であるかどうかを比較する式を書き込む。
キーはCSSのクラス名( selected
)である。
値は selectedHero
と hero
が同一なら true
になり、そうでなければ false
になる。
つまりこれは「 selectedHero
と hero
が同一なら <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」を選ぶと背景色が変わり、選択状態が良くわかるようになるだろう。
別のヒーローを選択すると、そのヒーローに選択状態の色が移る。
最終的な 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" }
];
この章で我々が得たものは、以下のようにまとめられる。
- アプリケーション上でヒーローが一覧表示され、選択することができる。
- ヒーローを選択したときに、そのヒーローの詳細情報が表示できるようになる。
- コンポーネントのテンプレートで
ngIf
とngFor
を使う方法を学んだ。
アプリケーションは成長してきたが、完成にはほど遠い。 単一のコンポーネントにアプリケーション全体の動作を記述することはできない。 そのため、アプリケーションを複数のサブコンポーネントに分割する必要がある。 その方法について、次回で作業を行いながら説明していく。