Skip to content

Instantly share code, notes, and snippets.

@dolpen
Created April 25, 2016 06:35
Show Gist options
  • Save dolpen/85c9f05cc33363e36c6568f2f5486ac0 to your computer and use it in GitHub Desktop.
Save dolpen/85c9f05cc33363e36c6568f2f5486ac0 to your computer and use it in GitHub Desktop.

#複数のコンポーネント

単一コンポーネントだった一覧/詳細画面をそれぞれ別のコンポーネントに分割する。

アプリケーションが成長してきている。 コンポーネントを使いまわしたり、データを受け渡したり、再利用可能な資源を作ったりするなどでユースケース(コンポーネントの利用パターン)が移り変わっていく。 ヒーローの一覧部分と詳細情報を、それぞれを再利用できるように分割してみよう。

今回作るもののデモ

前回までのあらすじ

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

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 {
}

命名規則

一般的に、あるクラスがコンポーネントであることや、どのファイルにどのコンポーネントが入っているか、ということが一目見て識別できる方が良いとされている。

AppComponentapp.component.ts というファイルに、新しく作った HeroDetailComponenthero-detail.component.ts というファイルにそれぞれ入っていることに着目してほしい。 コンポーネントを示すクラス名の末尾には全て Component が付いていて、それらが書かれたファイルは全て *.component.ts という名前になっているのだ。 ここでは、我々はファイル名は小文字、かつ区切りを - に統一している(棒で串刺しにしているように見えることからケバブケースと呼ばれる)が、大文字小文字の区別に関してはサーバーや開発環境によっては気にしなくていい場合がある。

ファイルの最初で Angular から ComponentInput のデコレータをインポートしているが、これは今後必要になるからである。 コンポーネントの要素名として使うセレクタを指定して @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 プロパティを追加する

先ほど触れた 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.tshero-detail.component.ts の先頭付近にインポート宣言を追加しよう。

import {Hero} from './hero';

hero プロパティを入力パラメータにする

HeroDetailComponent は、表示すべきヒーローを伝えてもらう必要がある。 一体誰が?実はそれは AppComponent なのだ!

AppComponent はどのヒーローを表示すべきか知っている(それはユーザーが一覧から選んだものである)。 ユーザーが選んだヒーローは、 コンポーネントの selectedHero プロパティに入っている。

では、この selectedHeroHeroDetailComponenthero プロパティにバインドするように、AppComponent のテンプレートを書き換えよう。バインドするたの記述は以下のようになる。

<my-hero-detail [hero]="selectedHero"></my-hero-detail>

[hero] がプロパティに対するバインドであることに気づいてほしい。[] で囲い = でバインドする値を紐づけている。

Angular は、このプロパティ hero が入力プロパティであると宣言しなければならないと求めている。もし宣言がなかった場合は、入力を拒否してエラーを出す。

入力プロパティについては属性ディレクティブで詳しく説明しており、ここでなぜ入力プロパティにそのような扱いが求められ、出力プロパティには必要ないのかも説明している。 (注釈: 主にバグ検知のために、入力が外部から来ないはずのプロパティに予期せずデータが入って来たら警告が出る。外から入って来るならその旨を明記しろというのが決まりのようだ)

さて、hero が入力プロパティであることを明示する方法は2つある。 先ほどついでにインポートしておいた @Input デコレータで注釈を付ける方がよいので、そうしよう。

  @Input() 
  hero: Hero;

@Input デコレータについては、属性ディレクティブの章で、より深く学べるだろう。

AppComponent を更新する

AppComponent に戻って、ここで HeroDetailComponent を使う方法について説明しよう。 まず HeroDetailComponent をインポートして、参照できるようにする。

import {HeroDetailComponent} from './hero-detail.component';

テンプレートから切り取ったヒーロー詳細の場所を探し、代わりに HeroDetailComponent を示すタグを追加しよう。

<my-hero-detail></my-hero-detail>

my-hero-detail とは、先ほど HeroDetailComponent のメタデータの中で指定したセレクタ名称である。 しかし、下記のように AppComponentselectedHero プロパティを HeroDetailComponenthero プロパティにバインドするまで、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>
`,

正しくバインドできていれば、 HeroDetailComponenthero プロパティを 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)**として再構築し、 データが必要なコンポーネント同士で共有させる必要がある。

サービスの作り方について、次回作業を行いながら説明していく。

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