Angular:ビルドしたファイルの場所

コンパイルしたjsファイルを生成するにはng buildng serveの代わりに実行します。 するとdistフォルダにjsファイルが書き出されます。

これまでnpm start (ng serve)を実行していたのですが、jsファイルが生成されないのを不思議に思っていました。ng serveはメモリ内にコンパイルするためファイルは生成されないようです:

serve · angular/angular-cli Wiki · GitHub

When running ng serve, the compiled output is served from memory, not from disk. This means that the application being served is not located on disk in the dist folder.

有線LAN:Mac book pro 2015 earlyに導入

二階で仕事をするのだが、無線LANの電波が弱いためかイライラする。そこで有線LANで接続することにした。ドライバのインストールとかよくわからないのでネットの評価をみて購入したのはAnkerのこいつ、2000円弱だった:

 googleでspeedtestと検索すると速度をテストできるの。回線はau光、無線LANアクセスポイントはAterm 8170Nである(有線LANは8170Nに繋いでいる)

f:id:thebaker:20171016234447p:plain

結果はこの通り:

f:id:thebaker:20171016234547p:plain

いい時代になったと思う。僕が自宅でインターネットを始めたときは14.4kbpsか28.8kbpsのモデムで、56kモデムが憧れだった…

Angularの公式チュートリアル:第6章「Routing」

Tour of Heroesアプリに、新たな要件を加えます:

  • ダッシュボードのビューを追加
  • ヒーロー一覧とダッシュボードの間を遷移可能にする
  • どちらかのビューでユーザがヒーロー名をクリックすると、そのヒーローの詳細ビューに遷移する
  • メールの中のディープリンクをクリックすると、特定のヒーローの詳細ビューを開く

これらを追加すると、次のようにアプリの中をナビゲーション可能になります:

https://angular.io/generated/images/guide/toh/nav-diagram.png

この新たな要件を満たす為にAngularのルータをアプリに追加します。

進め方

次のように進めます:

  • AppComponentにナビゲーションのみを担当させます
  • 現在のAppComponentのヒーローに関する実装を別の、HeroesComponentに分割します。
  • ルーティングを追加します
  • 新たにDashboardComponentを作ります
  • ナビゲーションにダッシュボードを組み込みます

AppComponentの分割

現時点のアプリはAppComponentを読み込み即座にヒーロー一覧を表示します。 変更後のアプリはシェルに対してビューの選択肢(ダッシュボードとヒーロー一覧)を与え、そのうちの一つをデフォルトとして表示します。 AppComponentはナビゲーションのみを担当すべきなので、ヒーローに関する実装をAppComponentからHeroesComponentに移動します。

HeroesComponentについて

AppComponentはすでにヒーローに特化していますので、AppComponentからコードを移動するのはではなくHeroesComponentにリネームし、新たにAppComponentを作成しましょう。

次を実行してください:

  • app.component.tsファイルをheroes.component.tsにリネーム
  • AppComponentクラスをHeroesComponentクラスにリネーム(heroes.component.tsの中のみ)
  • app-rootセレクタmy-heroesセレクタにリネームします

変更後のheroes.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero }      from './hero';
import { HeroService } from './hero.service';

@Component({
  selector: 'my-heroes',
  template: `<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'],
  providers: []
})

export class HeroesComponent implements OnInit {
  selectedHero: Hero;
  heroes:  Hero[];

  onSelect(myhero: Hero): void {
    this.selectedHero = myhero;
  }
  constructor (private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes().then(heroes => this.heroes = heroes);
  }

}

AppComponentを作成する

新しく作るAppComponentはアプリケーションのシェルにすぎません。画面上部にはナビゲーションがあり、その下には表示領域があります。

次の手順を実行してください:

  • src/app/app.component.tsというファイルを作成します
  • AppComponentクラスを作成しexportします
  • クラス定義の上に@Componentデコレータを記述し、セレクタとしてmy-appを指定します
  • HeroesComponentから以下の部分をAppComponentに移動します:
  • クラス属性のtitle
  • @Componentのテンプレートの中の<h1>titleとのバインドを含みます)
  • テンプレートの見出しの下に<my-heroes>要素を追加します
  • AppModuleHeroesComponentをimportし、かつ、declarationsHeroesComponentを追加します
  • AppModuleproviders配列にHeroServiceを追加します。
  • HeroesComponentproviders配列からHeroServiceを削除します
  • AppComponentに必要なimport文を記述します

編集後のapp.component.ts

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

@Component ({
  selector: 'app-root',
  template: `<h1>{{title}}</h1>
    <my-heroes></my-heroes>
  `
})

export class AppComponent {
  title = 'Tour of Heroes';
}

編集後の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';
import { HeroesComponent }     from './heroes.component';
import { HeroService }         from './hero.service';

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

編集後のheroes.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero }      from './hero';
import { HeroService } from './hero.service';

@Component({
  selector: 'my-heroes',
  template: `<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'],
  providers: []
})

export class HeroesComponent implements OnInit {
  selectedHero: Hero;
  heroes:  Hero[];

  onSelect(myhero: Hero): void {
    this.selectedHero = myhero;
  }
  constructor (private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes().then(heroes => this.heroes = heroes);
  }

}

テスト

この時点でnpm startと実行してみてください。無事、アプリケーションが動くはずです。

(続く)

Macの素人が入れたソフトウェア(随時更新)

Scroll Reverser

トラックパッドとマウスのスクロール方向を逆にできる。超便利。

Scroll Reverser for macOS

iTerm2

ターミナルソフト。右クリックでペーストしたり、色々便利なカスタマイズが可能。

iTerm2 - macOS Terminal Replacement

ChomeのAuto Copy

Chromeで、テキストを選択すると自動的にコピーしてくれる:

Auto Copy - Chrome Web Store

Google 日本語入力

こちらのほうが快適だと思う。リターンキーを押す回数少なくて済むし…

Google 日本語入力 – Google

Thinkpad歴10年超の人間がMacを買って使った感想

機種

Mac book pro 13 early 2015 8GB/128GB

Macいいなーと思ったところ

  • トラックパッドが便利
  • ディスプレイが超綺麗
  • ターミナルが最初から入っている・OSに組み込まれている
  • 起動が速い
  • デザインがかっこいい
  • ctrlキーがいい場所にあるので、vimが使いやすくなった
  • Magsafeの電源が便利&気持ちいい

Macイマイチなところ

  • なれるかもしれないが、やはりキーボードはThinkpadの方が良い
  • 細かい操作はトラックポイントの方が便利(だがこれも慣れの問題かもしれない)
  • Safariが遅くて不安定なような…これはChromeを入れれば良いのだろう。
  • パームレストが冷たいw
  • ¥キーが遠くなったのでpsqlが不便になった
  • 本体が重い(1.58kg)
  • 期待していたほど速くない(体感速度)

Angularの公式チュートリアル:第5章「Services」

サービス

Tour of Heroesアプリを進化させていくにつれ、ヒーローのデータをアクセスするコンポーネントを増やすことになります。

同じコードを何度もコピペするのではなく、再利用可能なデータサービスを作り、それを、必要としているコンポーネントに注入していくことにします。独立したサービスを使うことで各コンポーネントを簡素にでき、ビューの構築に専念することができます。またモックサービスを使ってコンポーネント単体テストしやすくなります。

データサービスは常に非同期であるため、最終的にはPromiseベースのデータサービスを構築します。

ヒーローサービスの作成にあたって

ヒーローサービスを利用する側は、ヒーロを様々な場所で、様々な方法で表示します。 ユーザは現在、リストからヒーローを選ぶことができます。次は、デキるヒーローを抜粋したダッシュボードを追加し、 また、ヒーローの詳細を編集するためのビューを作成します。3つのビューはそれぞれ、ヒーローのデータを必要とします。

現時点では、AppComponentは表示に必要なヒーロのモックデータを定義しています。しかし、ヒーローの定義はコンポーネントの本来の仕事ではありません。またヒーローのデータを他のコンポーネントやビューと簡単に共有することができないという欠点があります。本章では、ヒーローデータを取得する実装をサービスに移行し、そのサービスを、ヒーローデータを必要とする全てのコンポーネントに共有します。

ヒーローサービスの作成

appフォルダにhero.service.tsというファイルを作成してください。

サービスファイルの命名規則は、小文字のサービス名に.service.tsをつける、というものです。複数の単語が入ったサービスの場合、lowre-dash caseを使います。例えば、SpecialSuperHeroServiceのファイル名はspecial-super-hero.service.tsになります。

クラス名をHeroServiceとし、他のクラスがインポートできるようにexportします:

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

@Injectable()
export class HeroService {
}

注入可能なサービス

上記コードで、AngularのInjectable関数をインポートしその関数を@Injectable()デコレータとして適用したことに気づいたでしょうか。

丸括弧()を忘れないでください。省略すると、特定が難しいエラーになります(笑)。 @Injectable()デコレータはサービスに関するメタデータを発行するようにTypeScriptに指示します。メタデータはAngularが本サービスに他の依存性を注入する必要がある可能性を指定しています。

現時点では、HeroServiceは他の依存を持ちません。しかし最初から@Injectable()デコレータを使うことにより、一貫性と将来への担保を実現できます。

ヒーローデータの取得

スタブメソッドのgetHeroesを追加します:

@Injectable()
export class HeroService {
  getHeroes(): void {} // スタブ
}

HeroServiceは、色々な場所からヒーローデータを取得する可能性があります。例えば、ウェブサービス、ローカルストレージ、モックデータなどです。データアクセスをコンポーネントから隔離することにより、ヒーローデータを必要とするコンポーネントを変更することなく実装を変更できます。

モックのヒーローデータの移動

app.component.tsからHeRoEs配列を削除し、appディレクトリ内にmock-heroes.tsというファイルを作成してペーストします。また、import {Hero} ...文をコピーします。なぜならば、ヒーローの配列はHeroクラスを利用するためです。

// mock-heroes.ts
import { Hero } from './hero';
 
export 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定数は、他の場所、例えばHeroServiceからインポートできるようにexportします。 app.component.tsには、HeRoEs配列を削除した場所に初期化しないheroes属性を追加してください:

heroes: Hero[];

モックのヒーローデータの返却

HeroServiceでは、モックのHeRoEsをインポートしgetHeroes()メソッドから返却します。HeroServiceはこうなります:

// hero.service.ts
import { Injectable } from '@angular/core';

import { Hero } from './hero';
import { HeRoEs } from './mock-heroes';

@Injectable()
export class HeroService {
  getHeroes(): Hero[] {
    return HEROES;
  }
}

ヒーローサービスのインポート

これで、AppComponentを始めとするコンポーネントHeroServiceを利用することができます。 プログラム内で参照できるよう、HeroServiceをインポートします。

// app.component.ts
import { HeroService } from './hero.service';

ヒーローサービスを利用する際、newは使いません

AppComponentHeroServiceのコンクリートインスタンスをどうやって取得すべきでしょうか? 選択肢の1つとして、次のようにHeroServiceインスタンスを作成できます:

heroService = new HeroService(); // 実際にはこのようにしないでください

これは次の理由から不適切です:

  • コンポーネントHeroServiceの作り方を知っている必要があるHeroServiceのコンストラクタを変更した場合、サービスをnewした場所を全て探し出し、更新しなければならない。複数箇所の更新はミスになりやすく、テストの負担増になります。
  • newをする度に新しいサービスを作成します。しかしサービスがヒーローのデータをキャッシュし、他のコンポーネントとキャッシュを共有していたらどうでしょうか。それはできません
  • AppComponentが`HeroService`の特定の実装に結合してしまうと、異なる状況に対応するための実装の変更、例えば、オフラインでの動作やテスト用に異なるモックデータの利用が、難しくなります。

ヒーローサービスの注入

newの代わりに、次の2行を追加します。

コンストラクタの追加

constructor(private heroService: HeroService) { }

コンストラクタ自体は何もしません。引数のheroServiceは同時並行でprivateなheroService属性を定義し、HeroServiceの注入場所として明記します。

これで、Angularは、AppComponentを作成する際、HeroServiceインスタンスを提供すべきことを知ることができます。 しかし、インジェクタはHeroServiceの作り方がわかりません。現時点でプログラムを実行するとこのエラーが出るはずです:

EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)

インジェクタにHeroServiceの作り方を教えるため、次のproviders配列を@Componentコールの、コンポーネントメタデータの最下部に追加します。

// app.component.ts 
providers: [HeroService]

providers配列はAppComponent作成時に新たなHeroServiceインスタンスを作るようAngularに対して指示します。AppComponent、および、その子コンポーネントはそのサービスを使ってヒーローデータを取得できます。

AppComponentの中のgetHeroes()

サービスは、heroServiceというprivate変数の中にいます。 サービスを呼ぶと、1行で、データを取得できます:

this.heroes = this.heroService.getHeroes();

1行だけのメソッドを作る必要はないですが、ここでは作りましょう:

getHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}

ngOninitライフサイクルフック

AppComponentはこれで、ヒーローデータを取得して表示できるはずです。

ここでコンストラクタ内でgetHeroes()メソッドを呼ぶ誘惑にかられるかもしれませんが、 コンストラクタは複雑な処理を含むべきではありません。特に、データアクセスメソッドのようにサーバを呼ぶ コンストラクタならなおさらです。コンストラクタは例えば、コンストラクタの引数を属性に設定するといった簡単な初期化程度に徹すべきです。

AngularにgetHeroes()を呼ばせるにはAngularのngOnInitライフサイクルフックを実装します。Angularは、コンポーネントのライフサイクルにおける重要なポイントに介入するインタフェースを提供しています:生成時、変化時、そして最終的な破壊時です。

各インタフェースは1つのメソッドを備えています。コンポーネントがそのメソッドを実装すると、 Angularが適切なタイミングで呼び出します。

以下が、OnInitインタフェースの概要です(これをお手元のソースに記述しないでください)

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

export class AppComponent implements OnInit {
  ngOnInit(): void {
  }
}

OnInitの実装をexport文に追加します:

export class AppComponent implements OnInit {}

次にngOnInitメソッドを書き、その中に初期化の処理を含めます。Angularはこれを適切なタイミングで呼び出します。この場合、getHeroes()を呼び出して初期化を行います:

ngOnInit(): void {
  this.getHeroes();
}

アプリは予定通り動くはずです。ヒーローのリストを表示し、ヒーロー名をクリックすると詳細が表示されるはずです。

 現時点のソース

app.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero }      from './hero';
import { HeroService } from './hero.service';

@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'],
  providers: [HeroService]
})

export class AppComponent implements OnInit {
  title = 'Tour of Heroes';
  selectedHero: Hero;
  heroes:  Hero[];

  onSelect(myhero: Hero): void {
    this.selectedHero = myhero;
  }
  constructor (private heroService: HeroService) { }

  getHeroes(): void {
    this.heroes = this.heroService.getHeroes();
  }

  ngOnInit(): void {
    this.getHeroes();
  }
}

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

hero-detail.compnent.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;
}

hero.service.ts

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HeRoEs } from './mock-heroes';

@Injectable ()
export class HeroService {
  getHeroes(): Hero[] {
    return HeRoEs;
  }
}

hero.ts

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

mock-heroes.ts

import { Hero } from './hero';

export const HeRoEs: Hero[] = [
  { id: 11, name: 'ビアンカ' },
  { id: 12, name: 'フローラ' },
  { id: 13, name: 'デボラ'   }
];

非同期サービスとPromiseについて

HeroServicegetHeroes()シグネチャは同期であり、即座にヒーローのモックデータを返却します。

this.heroes = this.heroService.getHeroes();

ヒーローデータは最終的にはリモートサーバから取得します。リモートサーバを使う場合、ユーザにサーバからの応答を待たせたくなく、また、応答待ちの際、UIをブロックすることはできません。

応答とビューを連携させるにはPromiseを利用します。これは非同期の技術で、getHeroes()メソッドのシグネチャを変更します。

ヒーローサービスがPromiseを作成する

Promiseとは、結果が準備できたときにコールバックすることを約束する機構です。非同期のサービスに何らかの役務を依頼し、その際、コールバック関数を与えます。役務を遂行したサービスは最終的に結果もしくはエラーを持ってコールバックを呼びます。

HeroServicegetHeroes()がPromiseを返却するように以下のように変更します:

getHeroes(): Promise<Hero[]> {
  return Promise.resolve(HeRoEs);
}

まだモックデータを返却していることにご注意ください。ここでは、即座に応答したPromiseを返却することにより超高速な、ゼロレイテンシのサーバをシミュレートしています。

Promiseに対して処理を行う

HeroServiceへの変更に伴い、this.heroesはヒーローの配列ではなくPromiseに設定されています。

// 現時点のapp.component.ts
getHeroes(): void{
  this.heroes = this.heroService.getHeroes();
}

そこで、実装を変更し、Promiseが解決すると共にそれに対して処理を行わねばなりません。Promiseの解決に成功すると、ヒーローが表示されます。

Promiseのthen()メソッドに対してコールバック関数を引数として渡します:

getHeroes(): void {
  this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}

このコールバック関数は、サービスによって返却されるヒーローの配列をコンポーネントheroes属性に設定します。 この状態でもアプリは正常に実行し、ヒーローの一覧を表示し、名前の選択に応じて詳細を表示します。

まとめ

  • 複数のコンポーネントから共有可能なサービスクラスを作成しました
  • nOnInitライフサイクルフックを使い、AppComponentの有効化と同時にヒーローデータを取得するようになりました。
  • AppComponentへのプロバイダとしてHeroServiceを定義しました。
  • モックのヒーローデータを作成し、サービスにインポートしました。
  • サービスがPromiseを返却するように変更し、コンポーネントがPromiseからデータを取得するように変更しました。

本章での最終的なapp.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero }      from './hero';
import { HeroService } from './hero.service';

@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'],
  providers: [HeroService]
})

export class AppComponent implements OnInit {
  title = 'Tour of Heroes';
  selectedHero: Hero;
  heroes:  Hero[];

  onSelect(myhero: Hero): void {
    this.selectedHero = myhero;
  }
  constructor (private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes().then(heroes => this.heroes = heroes);
  }

}

本章での最終的なhero.service.ts

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HeRoEs } from './mock-heroes';

@Injectable ()
export class HeroService {
  getHeroes(): Promise<Hero[]> {
    return Promise.resolve(HeRoEs);
  }
}

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