Angularの公式チュートリアル:第4章「Multiple Components」

第3章までに作成したプログラムではAppComponentが全てを担っていました。このペースで機能を追加していくとメンテナンスが難しくなります。

そこで本章では、それぞれが決まったタスクもしくはワークフローをもった複数のコンポーネントに分割していきます。最終的にAppComponentは子供のコンポーネントをもったシンプルなシェルになります。

本章ではヒーローの詳細を、再利用可能な別のコンポーネントにします。

ヒーローの詳細コンポーネントの作成

appディレクトリにhero-detail.component.tsというファイルを作成します。このファイルに新たなコンポーネントであるHeroDetailComponentを記述します。

HeroDetailComponentを次のように記載してください:

import { Component } from '@angular/core';

@Component({
  selector: 'hero-detail',
})

export class HeroDetailComponent {
}

コンポーネントを定義するには、必ずComponentシンボルをインポートします。

@Componentデコレータは、コンポーネントにAngularのメタデータを提供します。CSSセレクタ名であるhero-detailは親のコンポーネントのテンプレートにおいてこのコンポーネントを特定するタグを指定しています。

コンポーネントクラスは必ずexportします。理由は、他の場所において必ずimportするからです。

ヒーローの詳細のテンプレート

ヒーローの詳細表示をHeroDetailComponentに移動するには、AppComponentの中のテンプレートの下の部分を切り取り、HeroDetailComponent@Componentメタデータにおけるtemplate属性に設定します。

なお、HeroDetailComponentは選択されたheroではなく特定のheroをもつので、selectedHeroと記載した箇所はheroに変更します。

hero-detail.component.tsは次のようになるはずです:

import { Component } from '@angular/core';

@Component({
  selector: 'hero-detail',
  template: `
    <div *ngIf="hero">
      <div>
        <input [(ngModel)]="hero.name">
      </div>
      <div><label>id: </label>{{hero.id}}</div>
      <div><label>name: </label>{{hero.name}}</div>
    </div>
  `
})

export class HeroDetailComponent {
}

hero属性の追加

HeroDetailComponentのテンプレートは、コンポーネントhero属性にバインドします。よって、HeroDetailComponentクラスに次のように属性を追加します:

hero: Hero;

さて、heroHeroインスタンスです。Heroクラスはapp.component.tsに入っています。Angularのスタイルガイドは1つのファイルにつき1つのクラスを推奨しているので、Heroクラスをapp.component.tsからhero.tsに分割します:

// hero.ts
export class Hero {
  id: number;
  name: string;
}

これでHeroクラスは自分自身のファイルに入っているのでAppComponentHeroDetailComponentはインポートしなければなりません。app.component.tshero-detail.component.tsの最上段の近くに次のimportを追加します:

import { Hero } from './hero';

hero属性は、入力属性です

本章の後半において、AppComponentselectedHeroHeroDetailComponenthero属性にバインドすることにより、子コンポーネントであるHeroDetailComponentがどのヒーローを表示すべきかを指定します。バインドはこのようになります:

<hero-detail [hero]="seletedHero"></hero-detail>

hero属性の周りを[ ]で括るのは属性のバインド式の相手であることを示しています。この場合、相手となる属性が入力属性であることを宣言しなければなりません。そうしないと、Angularはバインドを拒否しエラーを出します。

まず、@angular/coreのインポート分を修正し、Inputシンボルを追加します:

import { Component, Input } from '@angular/core';

次に、@Inputデコレータを頭につけてheroが入力属性であることを宣言します:

@Input() hero: Hero;

以上です。HeroDetailComponentクラスにおける唯一の属性はhero属性です:

export class HeroDetailComponent {
  @Input() hero: Hero;
}

最終的なHeroDetailComponentは次の通りです:

import { Component, Input } from '@angular/core';
import { Hero }             from './hero';

@Component({
  selector: 'hero-detail',
  template: `
    <div *ngIf="hero">
      <div>
        <input [(ngModel)]="hero.name">
      </div>
      <div><label>id: </label>{{hero.id}}</div>
      <div><label>name: </label>{{hero.name}}</div>
    </div>
  `
})

export class HeroDetailComponent {
  @Input() hero: Hero;
}

AppModuleにおいてHeroDetailComponentを宣言する

全てのコンポーネントは一つのNgModuleにおいて1回だけ宣言されなければなりません。 app.module.tsを開き、参照できるようにHeroDetailComponentをインポートします:

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

そして、declarations配列にHeroDetailComponentを追加します:

declarations: [
  AppComponent,
  HeroDetailComponent
],

この時点で、app.module.tsは次のようになっているはずです:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule} from '@angular/forms';

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

@NgModule({
  declarations: [
    AppComponent,
    HeroDetailComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
~

AppComponentにHeroDetailComponentを追加する

AppComponentはmaster/detailを表示します。以前は自分自身でヒーローを表示していましたが、その部分のテンプレートは切り出しました。よって、今後はHeroDetailComponentに表示を委任します。

そこで、AppComponentテンプレートの下のほうに<hero-detail>要素を追加します。<hero-detail>HeroDetailComponentにおいてCSSセレクタとして指定したのを覚えていますでしょうか。

<hero-detail>を記述する際、AppComponentselectedHero属性をHeroDetailComponenthero属性にバインドしたいので、次のように記述します:

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

これにより、selectedHeroが変化するとHeroDetailComponentは表示すべき新たなヒーローを入手します。

AppComponentは次のようになっているはずです:

import { Component } from '@angular/core';
import { Hero }      from './hero';

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>
   <hero-detail [hero]="selectedHero"></hero-detail>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  selectedHero: Hero;
  heroes = HeRoEs; // heroes: ではない点に注意

  onSelect(myhero: Hero): void {
    this.selectedHero = myhero;
  }
}

ブラウザを開くと、以前と同じようにヒーローを選択するとそのヒーローの詳細が画面下部に表示されます。しかし、以前と異なり、詳細はHeroDetailComponentが表示しています。

AppComponentを2つのコンポーネントリファクタリングすることにより、次のメリットがあります:

  1. AppComponentの責任を減らすことによりシンプルにした
  2. AppComponentを触ることなくHeroDetailComponentを通じてエディタ部分を改良できる
  3. ヒーローの詳細表示を触ること無くAppComponentを進化させることができる
  4. HeroDetailComponentのテンプレートを将来作るかもしれない親コンポーネントで再利用できる

最後に、本章終了時点のソースを記載します:

hero-detail.component.ts

import { Component, Input } from '@angular/core';
import { Hero }             from './hero';

@Component({
  selector: 'hero-detail',
  template: `
    <div *ngIf="hero">
      <div>
        <input [(ngModel)]="hero.name">
      </div>
      <div><label>id: </label>{{hero.id}}</div>
      <div><label>name: </label>{{hero.name}}</div>
    </div>
  `
})

export class HeroDetailComponent {
  @Input() hero: Hero;
}

app.component.ts

import { Component } from '@angular/core';
import { Hero }      from './hero';

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>
   <hero-detail [hero]="selectedHero"></hero-detail>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  selectedHero: Hero;
  heroes = HeRoEs; // heroes: ではない点に注意

  onSelect(myhero: Hero): void {
    this.selectedHero = myhero;
  }
}

hero.ts

export class Hero {
  id: number;
  name: string;
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule} from '@angular/forms';

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

@NgModule({
  declarations: [
    AppComponent,
    HeroDetailComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }