この投稿は、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
ページの後に次のファイル構成になっていることを確認してください。
あなたのファイル構成が一致しない場合は、そのページに戻ってあなたが逃したものを見つけ出してください。
アプリをコンパイル・実行し続ける
ターミナル画面で以下のコマンドを入力します。
npm start
このコマンドは、「watchモード」でTypeScriptコンパイラを実行し、コードが変更されたときに自動的に再コンパイルします。
また、ブラウザでアプリを同時に起動し、コードが変更されたときにブラウザを更新します。
ブラウザを再コンパイルまたは更新するために一時停止することなく、
Tour of Heroes を構築し続けることができます。
ヒーローを表示する
ヒーローのリストを表示するために、ヒーローをビューのテンプレートに追加します。
ヒーローを作成する
10人のヒーローの配列を作成します。
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プロパティを作成し、データバインディング用にヒーロー配列を公開します。
heroes = HEROES;
heroes
プロパティの型は、TypeScriptがHEROES
配列から推論するため、定義されていません。
最終的にヒーロー名はデータサービスから取得するため、
ヒーローデータはクラス実装から分離されます。
テンプレートにヒーロー名を表示する
順序付けられていないリストにヒーロー名を表示するために、タイトルの下、ヒーロー詳細の上に次のHTMLを挿入します。
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<!-- each hero goes here -->
</li>
</ul>
これでテンプレートにヒーローの名前を入力できます。
ngForでヒーローを一覧表示する
目標は、コンポーネントのヒーローの配列をテンプレートにバインドし、それらを反復処理して個々に表示することです。
組込みディレクティブ*ngFor
を追加して、<li>
タグを変更します。
<li *ngFor="let hero of heroes">
ngFor
への接頭辞*
は、この構文の重要な部分です。<li>
要素とその子要素がマスターテンプレートを構成することを示します。
ngFor
ディレクティブは、AppComponent
のhero
配列を繰り返し処理し、その配列のヒーローごとにこのテンプレートのインスタンスをレンダリングします。
let hero
部分は、テンプレート入力変数としてhero
を識別します。これは、各繰り返しの現在のヒーロー項目を保持します。 この変数をテンプレート内で参照すると、現在のhero
のプロパティにアクセスできます。
ngFor
とテンプレート入力変数の詳細については、Displaying DataページのShowing an array property with *ngForとTemplate SyntaxページのngForを参照してください。
<li>
タグ内で、テンプレート変数hero
を使用してヒーローのプロパティを表示するコンテンツを追加します。
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
ブラウザが更新されると、ヒーローのリストが表示されます。
ヒーローのスタイル
ユーザーは、どのヒーローにカーソルを合わせているのか、どのヒーローを選択しているのかを視覚的に確認する必要があります。
コンポーネントにスタイルを追加するには、@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には影響しません。
ヒーローを表示するためのテンプレートは、次のようになっているはずです。
<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>
要素へのクリックイベントのバインディングを次のように追加します。
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
...
</li>
丸括弧(click)
は<li>
要素のクリックイベントをターゲットとして識別します。
onSelect(hero)
はAppComponent
のonSelect()
メソッドを呼び出し、テンプレート入力変数hero
を引数として渡します。
これはngFor
ディレクティブで以前定義したのと同じhero
変数です。
イベントバインディングの詳細については、User Inputページおよび、Template SyntaxページのEvent bindingを参照してください。
選択したヒーローを表示するクリックハンドラを追加する
hero
プロパティはもう必要ありません。
なぜなら、あなたはもう1人のヒーローだけを表示しておらず、ヒーローのリストを表示しているからです。
しかし、ユーザーはヒーローのリストをクリックすることでヒーローの1人を選択することができます。
hero
プロパティを以下の単純なselectedHero
プロパティで置き換えましょう。
selectedHero: Hero;
ヒーローの名前は、ユーザーがヒーローを選択する前にはすべて選択解除する必要があります。
そのため、selectedHero
はhero
と同じように初期化することはありません。
selectedHero
プロパティにユーザーがクリックした<li>
要素のhero
を設定するonSelect()
メソッドを追加します。
onSelect(hero: Hero): void {
this.selectedHero = 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
は未定義です。
選択されたヒーローは、ユーザーがヒーローの名前をクリックすると初期化されます。
Angularは未定義のselectedHero
プロパティを表示できず、ブラウザのコンソールに次のエラーがスローされます。
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
テンプレートにはselectedHero.name
が表示されますが、
選択されたヒーローが存在するまで、ヒーローの詳細をDOMから外しておく必要があります。
テンプレートのヒーロー詳細コンテンツHTMLを<div>
で囲みます。
その後、Angular組み込みのngIf
ディレクティブを追加し、その値としてAppComponent
の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
の前にアスタリスク*
を忘れないでください。
アプリはもうエラーをスローしなくなり、ブラウザにはヒーロー名のリストが再び表示されます。
選択されたヒーローが存在しない場合、ngIf
ディレクティブはヒーローの詳細HTMLをDOMから削除します。
ヒーローの詳細要素も心配されたバインディングもありません。
ユーザーがヒーローを選択すると、selectedHero
が定義され、ngIf
はヒーローの詳細コンテンツをDOMに入れ、ネストされたバインディングを評価します。
ngIf
とngFor
の詳細については、Structural Directivesページと、Template SyntaxページのBuilt-in directivesを参照してください。
選択されたヒーローにスタイルを適用する
選択されたヒーローの詳細はリストの下に表示されますが、
リストの中から選択されたヒーローを見つけることは困難です。
上記で追加したstyles
メタデータには、selected
という名前のカスタムCSSクラスがあります。
選択したヒーローをより目立たせるために、ユーザーがヒーロー名をクリックすると、このselected
クラスを<li>
に適用させます。
たとえば、ユーザーが「Magneta」をクリックすると、次のように独特で繊細な背景色でレンダリングされるはずです。
テンプレートで、次の[class.selected]
バインディングを<li>
に追加します。
[class.selected]="hero === selectedHero"
hero === selectedHero
という式がtrue
の場合、Angularはselected
クラスを追加します。
式がfalse
の場合、Angularは選択したselected
クラスを削除します。
クラスバインディングの詳細については、Template Syntaxページでの説明を参照してください。
<li>
の最終版は次のようになります。
<li *ngFor="let 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 '@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つのコンポーネントだけであってはいけません。
次のページでは、アプリケーションをサブコンポーネントに分割して連携させます。