#複数のコンポーネント
単一コンポーネントだった一覧/詳細画面をそれぞれ別のコンポーネントに分割する。
アプリケーションが成長してきている。 コンポーネントを使いまわしたり、データを受け渡したり、再利用可能な資源を作ったりするなどでユースケース(コンポーネントの利用パターン)が移り変わっていく。 ヒーローの一覧部分と詳細情報を、それぞれを再利用できるように分割してみよう。
この章を続ける前に、まず前回の章が終わった時のフォルダ構造を確認してみよう。 もし以下のようになっていないなら、前の章に戻ってどこが足りないのかを探してみてほしい。
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 で動かし、サーバーを動作させる。
アプリケーションをブラウザで起動し、開発中にアプリケーションを動作させたまま更新を反映させることができる。
今、ヒーロー一覧と詳細は同じファイルの同じコンポーネントに一緒になっている。 今は一緒のままでも十分に小さく、このまま改良を続けていけるだろう。 しかし、新しい追加機能が片方だけに入り、もう片方には影響しない、ということは今後起こるだろうということはわかる。 そのとき、この(2つの機能が一緒になった)コンポーネントで発生する全ての変更は、 コンポーネントが持つどちらの機能にも壊れてしまうリスクを生じさせ、また利益のないテストを2倍書かなければならなくなる。 さらに、ヒーローの詳細の機能だけを再利用したいときに、ヒーロー一覧の機能が余計に付いて回ることになる。
今の AppComponent
は 単一責任の原則(Single Responsibility Principle) に反してしまっているのだ。
確かに今はチュートリアルではあるが、こういう点をちゃんとすることは可能だ。
(特に、その作業自体は難しくないし、それが Angular でアプリケーションを作る方法を学ぶ過程でもあるからだ)
ではまず、ヒーローの詳細情報を表示する機能を独自のコンポーネントに切り出してみよう。
app
フォルダに hero-detail.component.ts
を新規作成して、以下のように HeroDetailComponent
を作ろう。
import {Component, Input} from 'angular2/core';
@Component({
selector: 'my-hero-detail',
})
export class HeroDetailComponent {
}
一般的に、あるクラスがコンポーネントであることや、どのファイルにどのコンポーネントが入っているか、ということが一目見て識別できる方が良いとされている。
AppComponent
がapp.component.ts
というファイルに、新しく作ったHeroDetailComponent
がhero-detail.component.ts
というファイルにそれぞれ入っていることに着目してほしい。 コンポーネントを示すクラス名の末尾には全てComponent
が付いていて、それらが書かれたファイルは全て*.component.ts
という名前になっているのだ。 ここでは、我々はファイル名は小文字、かつ区切りを-
に統一している(棒で串刺しにしているように見えることからケバブケースと呼ばれる)が、大文字小文字の区別に関してはサーバーや開発環境によっては気にしなくていい場合がある。
ファイルの最初で Angular から Component
と Input
のデコレータをインポートしているが、これは今後必要になるからである。
コンポーネントの要素名として使うセレクタを指定して @Component
のメタデータを作る。
こうすることで、他のコンポーネント内で使えるように、コンポーネントクラスを export できる。
これが終わったら AppComponent
にインポートして、このコンポーネントに対応した <my-hero-detail>
要素を作っていこう。
さて、今は一覧と詳細の view は AppComponent
内で一つに統合されたテンプレートになってしまっている。
HeroDetailComponent
クラスに template
プロパティを作り AppComponent
から詳細情報のテンプレート部分をカット&ペーストしよう。
以前までは AppComponent
内の selectedHero.name
をバインドしていた。
HeroDetailComponent
に(それ相当の) hero
プロパティを持つことになるだろう。
(ただし selectedHero
ではない、理由は多分後々)
ということで、今ペーストしたテンプレートの selectedHero
を全て hero
に置き換える。
結果は以下のようになるだろう。
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
`
これで、ヒーローの詳細情報のレイアウトは HeroDetailComponent
だけにあることになる。
先ほど触れた hero
プロパティを HeroDetailComponent
に追加しよう。
hero: Hero;
あ、あれ……。 hero
プロパティは Hero
型なのだが、そのクラス定義が app.component.ts
に書かれているぞ。
今コンポーネントは2つある。どっちのファイルでも Hero
クラスを参照する必要があるのだ。
Hero
クラス定義を app.component.ts
から新たに hero.ts
に起き直すことによって、この問題を解決しよう。
export class Hero {
id: number;
name: string;
}
2つのコンポーネントから参照する必要があったため、このように hero.ts
に定義を出した。
次は、 app.component.ts
と hero-detail.component.ts
の先頭付近にインポート宣言を追加しよう。
import {Hero} from './hero';
HeroDetailComponent
は、表示すべきヒーローを伝えてもらう必要がある。
一体誰が?実はそれは AppComponent
なのだ!
AppComponent
はどのヒーローを表示すべきか知っている(それはユーザーが一覧から選んだものである)。
ユーザーが選んだヒーローは、 コンポーネントの selectedHero
プロパティに入っている。
では、この selectedHero
を HeroDetailComponent
の hero
プロパティにバインドするように、AppComponent
のテンプレートを書き換えよう。バインドするたの記述は以下のようになる。
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
[hero]
がプロパティに対するバインドであることに気づいてほしい。[]
で囲い =
でバインドする値を紐づけている。
Angular は、このプロパティ hero
が入力プロパティであると宣言しなければならないと求めている。もし宣言がなかった場合は、入力を拒否してエラーを出す。
入力プロパティについては属性ディレクティブで詳しく説明しており、ここでなぜ入力プロパティにそのような扱いが求められ、出力プロパティには必要ないのかも説明している。 (注釈: 主にバグ検知のために、入力が外部から来ないはずのプロパティに予期せずデータが入って来たら警告が出る。外から入って来るならその旨を明記しろというのが決まりのようだ)
さて、hero
が入力プロパティであることを明示する方法は2つある。
先ほどついでにインポートしておいた @Input
デコレータで注釈を付ける方がよいので、そうしよう。
@Input()
hero: Hero;
@Input
デコレータについては、属性ディレクティブの章で、より深く学べるだろう。
AppComponent
に戻って、ここで HeroDetailComponent
を使う方法について説明しよう。
まず HeroDetailComponent
をインポートして、参照できるようにする。
import {HeroDetailComponent} from './hero-detail.component';
テンプレートから切り取ったヒーロー詳細の場所を探し、代わりに HeroDetailComponent
を示すタグを追加しよう。
<my-hero-detail></my-hero-detail>
my-hero-detail
とは、先ほど HeroDetailComponent
のメタデータの中で指定したセレクタ名称である。
しかし、下記のように AppComponent
の selectedHero
プロパティを HeroDetailComponent
の hero
プロパティにバインドするまで、2つのコンポーネントは機能しないだろう。
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
AppComponent
のテンプレート全体は、このようになっている必要がある。
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>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
正しくバインドできていれば、 HeroDetailComponent
は hero
プロパティを AppComponent
から受け取ることができ、
ヒーロー一覧の下に詳細情報を出すことができるだろう。そして、ヒーローを選択し直す度に表示は更新されるはずだ。
しかしまだである!
ヒーロー一覧の中からクリックしてみると、詳細情報は出て来ない。
ブラウザの開発コンソール上にエラーがないか探してみるが、エラーもない。
Angular が新たなタグ( <my-hero-detail>
)を無視しているのだ。その理由を説明しよう。
ブラウザは認識できないタグや属性を無視する。Angular もそうである。
我々は HeroDetailComponent
をインポートし、AppComponent
のテンプレートに使った。しかしそれを Angular 本体に知らせていない。
(注釈: テンプレート内の <my-hero-detail>
タグが HeroDetailComponent
を示すタグであることを、AppComponent
は分からない)
さて、メタデータのdirectives
という配列プロパティに(HeroDetailComponent
を)加えることで、Angular に伝えなければならない。
@Component
の設定オブジェクトの一番下、テンプレートやスタイルシートの下に配列のプロパティを追加してみよう。
directives: [HeroDetailComponent]
(注釈: directives
は複数形)
これで動くぞ!
ブラウザで見ると一覧が詳細されている。ヒーローを選択すると詳細に追加される。
根本的に新しくなった部分は、HeroDetailComponent
を使うことでアプリのどの場所でもヒーローの詳細情報を出せるようになったことだ。
これで、再利用可能なコンポーネントを初めて作ることができた。
この章でリファクタリングをした後のフォルダ構造を確認してみよう。
angular2-tour-of-heroes
├app
│├app.component.ts
│├hero.ts
│├hero-detail.component.ts
│└main.ts
├node_modules ...
├typings ...
├index.html
├package.json
├styles.css
├tsconfig.json
└typings.json
この章で議論に挙がったファイルはこちら。
この章で我々が得たものは、以下のようにまとめられる。
- 再利用可能なコンポーネントを作る
- コンポーネントが外部からの入力を受け入れられるようにする
- 親コンポーネントから子コンポーネントへ値をバインドする
- コンポーネントのディレクティブ配列を通して、必要な子コンポーネントを明示する
このアプリケーションのコンポーネントをより再利用しやすくする。
いまだ、ヒーロー一覧のダミーデータは AppComponent
の中にある。あまり便利とは言えないだろう。
このようなデータへのアクセスを、コンポーネントから分離された**サービス(Services)**として再構築し、
データが必要なコンポーネント同士で共有させる必要がある。
サービスの作り方について、次回作業を行いながら説明していく。