第3章では、複数のヒーローをリスト表示し、ユーザがヒーローを選択すると詳細を表示する仕組みを作ります。 元の記事はこちら
ウェブサーバの起動
(既に実施していたら不要です)
cd ~/angular-tour-of-heroes ng serve --host=0.0.0.0 --public=グローバルIP
ヒーローの名前の定義
app.component.ts
を編集し、次の定数を追加します:
const HeRoEs: Hero[] = [ { id: 11, name: 'ビアンカ' }, { id: 12, name: 'フローラ' }, { id: 13, name: 'デボラ' } ];
追加する場所は、app.component.ts
の@Component
の上にします。追加後のソースはこちら:
import { Component } from '@angular/core'; export class Hero { id: number; name: string; } const HeRoEs: Hero[] = [ { id: 11, name: 'ビアンカ' }, { id: 12, name: 'フローラ' }, { id: 13, name: 'デボラ' } ]; @Component({ selector: 'app-root', template: `<h1>{{title}}</h1> <div> <input [(ngModel)]="hero.name"> </div> <div><label>id: </label>{{hero.id}}</div> <div><label>name: </label>{{hero.name}}</div> `, styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Tour of Heroes'; hero: Hero = { id: 1, name: 'Windstorm' }; }
HeRoEs
は、Hero
型の配列です。Heroは前章において定義したクラスです。このアプリは、いずれはウェブサービスからヒーローのデータを取得するようにしますが、現時点ではモックのヒーローを読み込みます。
次に、ヒーローを外部とバインド可能にする変数をAppComponent
に定義します:
heroes = HeRoEs; // heroes: ではなく heroes = である点に注意
heroes
に型の定義はありません。なぜならば、HeRoEs
配列より型を推論するためです。
この時点でのソースは次の通りです:
import { Component } from '@angular/core'; export class Hero { id: number; name: string; } const HeRoEs: Hero[] = [ { id: 11, name: 'ビアンカ' }, { id: 12, name: 'フローラ' }, { id: 13, name: 'デボラ' } ]; @Component({ selector: 'app-root', template: `<h1>{{title}}</h1> <div> <input [(ngModel)]="hero.name"> </div> <div><label>id: </label>{{hero.id}}</div> <div><label>name: </label>{{hero.name}}</div> `, styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Tour of Heroes'; hero: Hero = { id: 1, name: 'Windstorm' }; heroes = HeRoEs; }
ヒーローのデータはクラスの実装から分けています。なぜかというと、最終的にはヒーローの名前はデータのサービスから取得するためです。
次に、ヒーローの名前の一覧を表示します。表示するには、ヒーローの名前の配列をテンプレートにバインドし、それらに対してイテレーションしてひとつひとつを表示します。app.component.ts
のtemplate
に次のようにHTMLを追加します:
template: `<h1>{{title}}</h1> <h2>私のヒーロー一覧</h2> <ul class="heroes"> <li *ngFor="let hero of heroes"> <span class="badge">{{hero.id}}</span>{{hero.name}} </li> </ul>
ngFor
のプレフィックス(*)は、重要なポイントです。これは<li>
とその子要素がテンプレートのマスターであることを示しています。
ngFor
ディレクティブはヒーローの配列をイテレーションし、配列の中の1つ1つのヒーローに対してテンプレートのインスタンスを描画します。
let hero
の部分は、hero
をテンプレートの入力変数として明示しています。この変数はイテレーションにおける現在のヒーローの値を保持しています。テンプレートの中からこの変数を参照することによりヒーローの属性をアクセスできます。
この内容で保存し、ブラウザをリロードすると、次の画面が表示されるはずです:
次に、デザインを変更します。目的は、マウスカーソルをホバーしたことが分かるようにすること、および、どのヒーローが選択されているかを分かるようにすること。
元のチュートリアルではapp.component.ts
の中にCSSを埋め込んでいますが、これですとソースが長くなってしまうのでここでは別ファイルに切り出します。
app.component.css
というファイルがあるので開き、次のコードをペーストし保存します:
.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; }
このファイルはapp.component.ts
のstyleUrls
で読み込みを指定されています。ブラウザをリロードすると、スタイルが適用されているのが分かると思います:
なお、このスタイルは、スタイルを定義したコンポーネント、この場合はapp.component.ts
にのみ適用されます。その他のHTMLには影響がありません。
さて、現時点ではリスト内のヒーローと、その下のヒーローの紹介が紐付けられていません。そこで、ユーザがリストからヒーローを選択したら、 そのヒーローの詳細が下に表示される処理を実装します。これはmaster/detailというUIパターンです。masterとは、ヒーローの一覧を指し、 detailとはユーザが選択したヒーローを示します。
masterとdetailは、selectedHero
という属性を通じて関連付けます。そしてこの属性をクリックイベントと紐付けます。
まず、<li>
タグに次のようにクリックイベントをバインドします:
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
丸括弧は、<li>
タグのclick
イベントをターゲットとして識別します。onSelect(hero)
式はAppComponent
のonSelect()
メソッドを呼び出し、その際、hero
を引数として渡します。このhero
は、以前にngFor
ディレクティブで定義したhero
と同じです。
さて、AppComponent
クラスは現在hero
属性を持っていますが、今後はユーザが一覧から選択したヒーロを表示するのでこの属性は不要になります。代わりにselectedHero
属性を設けます:
- hero: Hero = { - id: 1, - name: 'Windstorm' - }; + selectedHero: Hero; }
ユーザがクリックするまでヒーローの詳細表示は不要なので、selectedHero
は宣言するだけで初期化は行いません。
次にonSelect()
メソッドを追加して、selectedHero
属性をユーザがクリックしたhero
に設定します:
onSelect(myhero: Hero): void { this.selectedHero = myhero; }
さて、現時点ではテンプレートは古いhero
とバインドしています。そこで今回作成したselectedHero
に代わりにバインドさせます:
template: `<h1>{{title}}</h1> <h2>私のヒーロー一覧</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span>{{hero.name}} </li> </ul> <div> <input [(ngModel)]="selectedHero.name"> </div> <div><label>id: </label>{{selectedHero.id}}</div> <div><label>name: </label>{{selectedHero.name}}</div> `,
この時点でブラウザをリロードするとヒーロー一覧が表示されないはずです。
アプリが読み込まれた時点ではselectedHero
は定義されていません。
定義されていないのにselectedHero.name
を参照しているため、エラーが出ているのです:
ヒーローが選択されるまでselectedHeroへの参照をDOMに含めてはなりません。そこで、selectedHero
が値をもってない場合はDOMから除外するよう、
<div>
で括り、<div>
に*ngIf
のディレクティブを付与します:
template: `<h1>{{title}}</h1> <h2>私のヒーロー一覧</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span>{{hero.name}} </li> </ul> <div *ngIf="selectedHero"> <div> <input [(ngModel)]="selectedHero.name"> </div> <div><label>id: </label>{{selectedHero.id}}</div> <div><label>name: </label>{{selectedHero.name}}</div> </div> `,
これでブラウザをリロードすると次のようになります:
ユーザがヒーローを選択してない場合、ngIf
ディレクティブは子要素をDOMから除外します。
ユーザがヒーローを選択すると、子要素をDOMに含めネストされたバインディングを評価します。
次に、ユーザが選択したヒーローがどれであるかをわかりやすくします。
app.component.ts
のテンプレートにおいて、<li>
に次のバインドを追加します:
[class.selected]="hero === selectedHero"
式hero===selectedHero
がtrue
の場合はCSSとしてselected
クラスを付与します。
false
の場合はクラスを除きます。
ブラウザをリロードし、ヒーローを選択するとそのヒーローの色が変わるはずです:
本章では次のことを行いました:
* ヒーローの一覧を表示する
* ヒーローを選択可能にし、選択したヒーローの詳細を表示する
* ngIf
とngFor
ディレクティブの利用方法を学習
参考までに、現時点のソースを記載します:
import { Component } from '@angular/core'; export class Hero { id: number; name: string; } const HeRoEs: Hero[] = [ { id: 11, name: 'ビアンカ' }, { id: 12, name: 'フローラ' }, { id: 13, name: 'デボラ' } ]; @Component({ selector: 'app-root', template: `<h1>{{title}}</h1> <h2>私のヒーロー一覧</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="badge">{{hero.id}}</span>{{hero.name}} </li> </ul> <div *ngIf="selectedHero"> <div> <input [(ngModel)]="selectedHero.name"> </div> <div><label>id: </label>{{selectedHero.id}}</div> <div><label>name: </label>{{selectedHero.name}}</div> </div> `, styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Tour of Heroes'; selectedHero: Hero; heroes = HeRoEs; // heroes: ではない点に注意 onSelect(myhero: Hero): void { this.selectedHero = myhero; } }