Angularの公式チュートリアル「Tour of Heroes」:第2章「The Hero Editor」

元のサイトはこちら。かなり端折っていますが一応日本語版です。

第2章ではアプリを実際に作ります。 チュートリアルでは開発をローカルで進めていますが私はWindowsユーザでローカル開発が苦痛なのでAmazon Linux上で進めます。

アプリの作成

cd
ng new angular-tour-of-heroes

これでangular-tour-of-heroesディレクトリ以下にアプリのファイルが生成されます

アプリの起動

念のため起動して動くかどうか確認しましょう:

cd angular-tour-of-heroes
ng serve --host=0.0.0.0 --public=グローバルIP

ブラウザでhttp://グローバルIP:4200/をアクセスするとQuickStartと同じ画面が表示されるはずです。

ターミナルをもう1つ開く

ターミナルをもう1つ開きます。なぜもう1つ開くかというと、1つ目はng serveが実行していてソース編集に使えないためです。 こちらでアプリのappディレクトリに移動します:

cd ~
cd angular-tour-of-heroes/src/app

app.component.tsの編集

まず、アプリで表示しているHTMLを変更します。 app.component.tsを開くと、次のようになっているはずです:

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';
}

まず、templateUrlの行を消し、代わりにtemplateを定義します。 その際、バッククオートを使ってタグを括ってください(シングルクオートでも動作しますが)

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

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1><h2>{{hero}} details!</h2>`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';
}

次にAppComponent classの中において、titleを定義していますが、これを変更し、かつ、heroという変数を追加します:

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

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1><h2>{{hero}} details!</h2>`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  hero  = 'Windstorm';
}

これで保存します。1つ目のターミナルでコンパイルが走りますので、エラーがなければブラウザを開いてhttp://グローバルIP:4200/を確認します:

f:id:thebaker:20171007224849p:plain

templateの中の{{ }}はAngularの補完バインドシンタックスです。 コンポーネント内のtitleとhero属性をHTMLのタグの中で文字列として表現します。

Heroオブジェクトの作成

我々のヒーローにIDと名前を与えることにします。app.component.tsにHeroクラスを追記します:

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

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

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1><h2>{{hero}} details!</h2>`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  hero  = 'Windstorm';
}

次に、AppComponentクラスの中で、hero属性をHero型に変更します。その際、id=1、name=Windstormで初期化します:

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

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

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1><h2>{{hero}} details!</h2>`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };
}

ここでブラウザを開き再読込をすると次のような画面になるはずです:

f:id:thebaker:20171007225921p:plain

タイトルは正常に表示されますが、ヒーローの名前は[object Object]となっています。 これは、AppComponentにおける heroをstringからオブジェクトに変えたにも関わらず、 templateにおいて {{ hero }}と参照しているためです。

そこで、templateで名前を参照するよう、{{ hero }} から {{ hero.name }} に変更します:

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

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

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1><h2>{{hero.name}} details!</h2>`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };
}

ブラウザを再読込すると、名前が正しく表示されるようになっているはずです。

次に、ヒーローのidとnameを表示するためのHTMLをtemplateに定義します。その際、HTMLを複数行をまたいで記載します。`(バックティック)を使用しているのは、複数行の記載を可能にするためです。これはES2015/TypeScriptのテンプレートリテラルの機能です。

  template: `<h1>{{title}}</h1>
    <h2>{{hero.name}} details!</h2>
    <div><label>id: </label>{{hero.id}}</div>
    <div><label>name: </label>{{hero.name}}</div>
  `,

編集後のソースはこうなります:

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

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

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1>
    <h2>{{hero.name}} details!</h2>
    <div><label>id: </label>{{hero.id}}</div>
    <div><label>name: </label>{{hero.name}}</div>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };
}

ブラウザをリロードするとこのようになっているはずです:

f:id:thebaker:20171007231138p:plain

次に、ヒーローの名前を編集可能にします。編集した名前は即座に表示に反映させたいとします。

そこでtemplateの中の名前を表示する部分を以下のように変更します:

    - <h2>{{hero.name}} details!</h2>
    + <div>
    +   <label>name: </label>
    +   <input [(ngModel)]="hero.name">
    + </div>

ngModelは、hero.name属性をこのテキストボックスにバインドします。テキストボックスへの変更はhero.name属性に反映され、hero.name属性への変更はテキストボックスに反映されます。

さて、このままではブラウザを開いても何も表示されないはずです。Chromeの場合、Ctrl+Shift+Iを教えてデベロッパーコンソールを開いてみてください。次のエラーが表示されているはずです:

f:id:thebaker:20171007231834p:plain

原因はngModelです。これはAngularにおける妥当な記述ですが標準では読み込まれてないのです。 そこで、app.module.tsファイルを編集します。

まず、3行目あたりに次を追記します:

import { FormsModule}    from '@angular/forms'; // <-- ngModelが含まれている場所

さらに@NgModuleディレクティブのimportsに次を追記します:

    FormsModule // <-- [(ngModel)]とバインドする前にFormsModuleをインポートする
import { BrowserModule } from '@angular/platform-browser';
import { NgModule }      from '@angular/core';
import { FormsModule}    from '@angular/forms'; // <-- ngModelが含まれている場所

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule // <-- [(ngModel)]とバインドする前にFormsModuleをインポートする
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

これでブラウザを開きリロードすると次の画面が表示されると思います。 入力欄を編集すると、編集結果がnameの後に表示されるはずです:

importを記載し、かつ、importsに記載している理由は、私の推測ですが、 最初のimportはTypeScriptにおけるライブラリの追加。そして2つめのimportsが Angularにおけるライブラリの追加なのだと思います。

f:id:thebaker:20171007232528p:plain

まとめ

本章では次を行いました:

  • {{ }} を使ってアプリのタイトルとHeroオブジェクトの属性をHTMLの中に読み込んだ
  • ES2015のテンプレートリテラル機能を使って複数行に渡るテンプレートを作った
  • ngModelディレクティブを通じて、<input>に双方向のデータバインディングを行い、ヒーローの名前の表示と編集を可能にした
  • なお、ngModelディレクティブはhero.nameにバインドしている全ての場所を更新します。