xengineer’s diary

結果、メモ的な内容になっています。

angularjs2のtutorialをやってみた(その3)

お断り事項

本記事は、Angularjs 公式サイトのチュートリアルを

純粋にやってみているだけです。

英語苦手な方には悪くないと思いますが、本家のほうが、アップデートもかかるし、

英語問題ない方は本家をみたほうがよいです。

5分クイックスタートをやってみたときの記事は以下から。

angularjs2のtutorialをやってみた - xengineer’s diary

Tour of Heroesの前半をやってみたときの記事は以下から。

angularjs2のtutorialをやってみた(その2) - xengineer’s diary

概要

今回やるのは、下記リンクにある、"Tour of Heroes"というチュートリアルです。

Tutorial: Tour of Heroes - ts ←これは、前回の記事で書いた Master/Detail - ts ←今回はこれをやってみる

前回までは、↓↓こんな感じで、

f:id:xengineer:20151105104239p:plain

  • アプリ名(Tour of Heroes)表示
  • ヒーローの詳細表示(上記では、Windstormの詳細)
  • ヒーロー名をテキストフィールドで編集すると、"Windstorm details!"の名前部分が変わる

というところまでやりました。

本来の最終目標は、これに、

  • ヒーローをリスト表示する
  • リストからヒーローを選択して、詳細表示する

上記機能が追加されたところまでなので、今回はそこまでやる予定。

前提条件確認などなど

前提条件:

  • OS: Ubuntu 14.04.1 LTS
  • npm: 2.14.7
  • angularjs: 2.0.0-alpha.44
  • systemjs: 0.19.5
  • live-server: 0.8.1
  • typescript: 1.6.2
  • Linuxの操作には慣れている(開発にあたっての環境整備はできるレベル)
  • AngularjsのチュートリアルMaster/Detail - ts、をやっていること

まずは、前回(Master/Detail - ts)の記事の最後と同じ状態のディレクトリ構造になってるかの確認を。

↓↓こうなってればいい。

angular2-tour-of-heroes
  |
  +--- node_modules
  |
  +--- src
  |     +---app
  |     |    |
  |     |    +-- app.ts
  |     |
  |     +-- index.html
  |     +-- tsconfig.json
  |
  +── package.json 

では、この状態で、tsc/live-serverは起動しちゃいます。

$npm run tsc
$npm start

ざっくりフロー

本記事のざっくりした流れです。

  1. 早速!Heroリスト表示
  2. ヒーローを選ぶ!
  3. ヒーローをクリックしてリストから選択するまで
  4. directive配列肥大について

詳細

以降ではやったことを少し詳細に。

早速!Heroリスト表示

ヒーロー生成!

ついにヒーローたちを沢山召喚できます。

これ↓↓を、app.tsに追記しましょう。

たぶんどこでもよさそう。

var 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" }
];

入力して追加していく方式ではなくて、一旦は配列でこっちから与える方式で表示なので、

まずは、Heroクラスの配列を作ってるのですね。

この状態だと、AppComponent内に定義されてないので、AppComponent内に↓↓を追記して宣言します。

public heroes = HEROES;

ヒーロー表示!!(準備)

次は表示します。

templateに↓↓を追記します。(これは完全に下準備)

この、li tagの中で、heroesをloopさせて表示する魂胆ですな。

<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <!-- each hero goes here -->
  </li>
</ul>

位置的には、↓↓の行の上に突っ込みましょう。

<h2>{{hero.name}} details!</h2>

ヒーロー表示!!(angularのfor文)

次に、まさにヒーローをループさせる処理を書きます。

angularで、loop処理書くには、NgForなるdirectiveを使うようです。

さっき書いたtemplateの、li tagに、*ng-for="#hero of heroes"なるdirectiveを追記します。

<li *ng-for="#hero of heroes">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

こんな感じ。

*がついているのは、li elementとその子elementが、master templateを構成してるってことを示すから、

らしいんだけど、master template?なんだろうね。そのうちわかったらまとめよう・・・今は見てみぬ振り。

"#hero of heroes" は、heros配列を、ローカル変数、heroに入れてループしなさいな、ってことらしいです。

#heroって、シャープを冒頭につけて書くと、ローカル変数になるんだと。

はい、ではここまでを反映した版。

import {bootstrap, Component, FORM_DIRECTIVES} from 'angular2/angular2';

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

var HEROES: Hero[] = [               ★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" }
];

// Annotation section
@Component({
  selector: 'my-app',
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>  ★★★ここからヒーローたち表示追記★★★
    <ul class="heroes">
      <li *ng-for="#hero of heroes">
        <span class="badge">{{hero.id}}</span> {{hero.name}}
      </li>
    </ul>               ★★★★★★★★ここまで★★★★★★★★
    <h2>{{hero.name}} details!</h2>
    <div><label>id: </label>{{hero.id}}</div>
    <label>name: </label>
    <div><input [(ng-model)]="hero.name" placeholder="name"></div>
    `,
  directives: [FORM_DIRECTIVES]
})
// Component controller

class AppComponent {
  public title = 'Tour of Heroes';
  public heroes = HEROES;
  public hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };
}

bootstrap(AppComponent);

よーし!これで表示されるかなー!

http://127.0.0.1:8080

にアクセスすると・・・

↓↓エラーがでます。Developer toolに(ブラウザには何も表示されない)

EXCEPTION: Template parse errors:
Can't bind to 'ngForOf' since it isn't a known native property in AppComponent > ul:nth-child(5) > li:nth-child(1)[*ng-for=#hero of heroes]
Property binding ngForOf not used by any directive on an embedded template in AppComponent > ul:nth-child(5) > li:nth-child(1)[*ng-for=#hero of heroes]
angular2.dev.js:21835 EXCEPTION: Template parse errors:
Can't bind to 'ngForOf' since it isn't a known native property in AppComponent > ul:nth-child(5) > li:nth-child(1)[*ng-for=#hero of heroes]
Property binding ngForOf not used by any directive on an embedded template in AppComponent > ul:nth-child(5) > li:nth-child(1)[*ng-for=#hero of heroes]BrowserDomAdapter.logError @ angular2.dev.js:21835BrowserDomAdapter.logGroup @ angular2.dev.js:21846ExceptionHandler.call @ angular2.dev.js:4431(anonymous function) @ angular2.dev.js:19543NgZone._onError @ angular2.dev.js:10711errorHandling.onError @ angular2.dev.js:10630run @ angular2.dev.js:141(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$$internal$$tryCatch @ angular2.dev.js:1507lib$es6$promise$$internal$$invokeCallback @ angular2.dev.js:1519lib$es6$promise$$internal$$publish @ angular2.dev.js:1490(anonymous function) @ angular2.dev.js:219microtask @ angular2.dev.js:10670run @ angular2.dev.js:138(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$asap$$flush @ angular2.dev.js:1301
angular2.dev.js:21835 STACKTRACE:BrowserDomAdapter.logError @ angular2.dev.js:21835ExceptionHandler.call @ angular2.dev.js:4433(anonymous function) @ angular2.dev.js:19543NgZone._onError @ angular2.dev.js:10711errorHandling.onError @ angular2.dev.js:10630run @ angular2.dev.js:141(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$$internal$$tryCatch @ angular2.dev.js:1507lib$es6$promise$$internal$$invokeCallback @ angular2.dev.js:1519lib$es6$promise$$internal$$publish @ angular2.dev.js:1490(anonymous function) @ angular2.dev.js:219microtask @ angular2.dev.js:10670run @ angular2.dev.js:138(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$asap$$flush @ angular2.dev.js:1301
angular2.dev.js:21835 Error: Template parse errors:
Can't bind to 'ngForOf' since it isn't a known native property in AppComponent > ul:nth-child(5) > li:nth-child(1)[*ng-for=#hero of heroes]
Property binding ngForOf not used by any directive on an embedded template in AppComponent > ul:nth-child(5) > li:nth-child(1)[*ng-for=#hero of heroes]
    at new BaseException (angular2.dev.js:16034)
    at TemplateParser.parse (angular2.dev.js:21192)
    at angular2.dev.js:24913
    at Zone.run (angular2.dev.js:138)
    at Zone.run (angular2.dev.js:10644)
    at zoneBoundFn (angular2.dev.js:111)
    at lib$es6$promise$$internal$$tryCatch (angular2.dev.js:1507)
    at lib$es6$promise$$internal$$invokeCallback (angular2.dev.js:1519)
    at lib$es6$promise$$internal$$publish (angular2.dev.js:1490)
    at angular2.dev.js:219

理由は、エラーに書いてあるとおりで、NgForってなんだよ!ちゃんとimportしてね。

ということなので、importと、@Componentのdirectivesに、NgForを追記した版。

import {bootstrap, Component, FORM_DIRECTIVES, NgFor} from 'angular2/angular2';  ★NgFor追記

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

var 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" }
];

// Annotation section
@Component({
  selector: 'my-app',
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
      <li *ng-for="#hero of heroes">
        <span class="badge">{{hero.id}}</span> {{hero.name}}
      </li>
    </ul>
    <h2>{{hero.name}} details!</h2>
    <div><label>id: </label>{{hero.id}}</div>
    <label>name: </label>
    <div><input [(ng-model)]="hero.name" placeholder="name"></div>
    `,
  directives: [FORM_DIRECTIVES, NgFor]   ★NgFor追記
})
// Component controller

class AppComponent {
  public title = 'Tour of Heroes';
  public heroes = HEROES;
  public hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };
}

bootstrap(AppComponent);

はい!やっと表示されました↓↓

見た目しょぼいけど。

f:id:xengineer:20151105124418p:plain

見た目がしょぼすぎるので、StyleSheetをあててみます。

styles:[`
  .heroes {list-style-type: none; margin-left: 1em; padding: 0; width: 10em;}
  .heroes li { cursor: pointer; position: relative; left: 0; transition: all 0.2s ease; }
  .heroes li:hover {color: #369; background-color: #EEE; left: .2em;}
  .heroes .badge {
    font-size: small;
    color: white;
    padding: 0.1em 0.7em;
    background-color: #369;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -1px;
  }
  .selected { background-color: #EEE; color: #369; }
  `],

こんなStyleSheet。badgeと、heroes classのtransitionの設定と。

実際の見た目。

f:id:xengineer:20151105134006g:plain

StyleSheet追記した版↓↓

import {bootstrap, Component, FORM_DIRECTIVES, NgFor} from 'angular2/angular2';

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

var 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" }
];

// Annotation section
@Component({
  selector: 'my-app',
  styles:[`   ★stylesheet追記
  .heroes {list-style-type: none; margin-left: 1em; padding: 0; width: 10em;}
  .heroes li { cursor: pointer; position: relative; left: 0; transition: all 0.2s ease; }
  .heroes li:hover {color: #369; background-color: #EEE; left: .2em;}
  .heroes .badge {
    font-size: small;
    color: white;
    padding: 0.1em 0.7em;
    background-color: #369;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -1px;
  }
  .selected { background-color: #EEE; color: #369; }
  `],
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
      <li *ng-for="#hero of heroes">
        <span class="badge">{{hero.id}}</span> {{hero.name}}
      </li>
    </ul>
    <h2>{{hero.name}} details!</h2>
    <div><label>id: </label>{{hero.id}}</div>
    <label>name: </label>
    <div><input [(ng-model)]="hero.name" placeholder="name"></div>
    `,
  directives: [FORM_DIRECTIVES, NgFor]
})
// Component controller

class AppComponent {
  public title = 'Tour of Heroes';
  public heroes = HEROES;
  public hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };
}

bootstrap(AppComponent);

TypeScriptの中にCSS書くのやだなぁ。

ちなみに、こういう感じに書くと、このStyleは、AppComponentの中にしか影響は及ぼさないとのこと。

それはとてもいい。でも書く場所は切り離したい。

ヒーローを選ぶ!

ヒーローたちを表示するところまでは平和に完了したので、

次は表示されてるヒーローたちから、一人選択する機能。

現状はこれ↓↓ f:id:xengineer:20151105134006g:plain

つまり、ヒーローたちのリストがあって、その下に、1人のヒーローの詳細が

表示してある。(今は、Windstormさん固定)

この詳細表示のところに、リストから選択したヒーローの内容を反映させる機能をつけます。

(このUIパターンは、"master-detail"っていう一般的なパターンらしい)

master : ヒーローたちのリスト detail : 選択されたヒーロー

ヒーローをクリックしてリストから選択するまで

「ヒーローをクリックしてリストから選択するまで」は、

  1. クリックイベントを取得する
  2. 「ヒーローが選択された状態」を作る
  3. ヒーローがクリックされたら、当該ヒーローを選択する

の3ステップあります。

まずひとつめの、

クリックイベントを取得する

から。

これは、li tagに仕込みます。↓↓のように、(click)="onSelect(hero)"を設定すればOK。

<li *ng-for="#hero of heroes" (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

hero は、ng-for文でheroesをループさせたときのloop内の変数。

onSelectメソッドは、まだ存在しないやーつ。これがクリックしたヒーローを選択状態にするメソッドになる予定。

次は、

「ヒーローが選択された状態」を作る

AppComponentの、

  public hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };

これ。は、staticで固定されすぎなので、選択したヒーローを表示するように下記に変更します。

public selectedHero: Hero;

今回は、ユーザが選択するまでは、ヒーロー詳細は表示しないようにするらしいので、Heroは初期化不要。

そして、onSelectメソッドを、AppComponentに追記。

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

これで、selectedHeroに、クリックされたheroが格納される。

でも現状だと、templateの中が、以前のままで、selectedHeroが登場しないので、下記のように修正。

<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
    <label>name: </label>
    <input [(ng-model)]="selectedHero.name" placeholder="name"></input>
</div>

この状態で、ブラウザをリロードすると、、、こんなエラー。ちーん。

EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
angular2.dev.js:21835 EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]BrowserDomAdapter.logError @ angular2.dev.js:21835BrowserDomAdapter.logGroup @ angular2.dev.js:21846ExceptionHandler.call @ angular2.dev.js:4431(anonymous function) @ angular2.dev.js:19543NgZone._onError @ angular2.dev.js:10711errorHandling.onError @ angular2.dev.js:10630run @ angular2.dev.js:141(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$$internal$$tryCatch @ angular2.dev.js:1507lib$es6$promise$$internal$$invokeCallback @ angular2.dev.js:1519lib$es6$promise$$internal$$publish @ angular2.dev.js:1490(anonymous function) @ angular2.dev.js:219microtask @ angular2.dev.js:10670run @ angular2.dev.js:138(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$asap$$flush @ angular2.dev.js:1301
angular2.dev.js:21835 ORIGINAL EXCEPTION: TypeError: Cannot read property 'name' of undefinedBrowserDomAdapter.logError @ angular2.dev.js:21835ExceptionHandler.call @ angular2.dev.js:4440(anonymous function) @ angular2.dev.js:19543NgZone._onError @ angular2.dev.js:10711errorHandling.onError @ angular2.dev.js:10630run @ angular2.dev.js:141(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$$internal$$tryCatch @ angular2.dev.js:1507lib$es6$promise$$internal$$invokeCallback @ angular2.dev.js:1519lib$es6$promise$$internal$$publish @ angular2.dev.js:1490(anonymous function) @ angular2.dev.js:219microtask @ angular2.dev.js:10670run @ angular2.dev.js:138(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$asap$$flush @ angular2.dev.js:1301
angular2.dev.js:21835 ORIGINAL STACKTRACE:BrowserDomAdapter.logError @ angular2.dev.js:21835ExceptionHandler.call @ angular2.dev.js:4443(anonymous function) @ angular2.dev.js:19543NgZone._onError @ angular2.dev.js:10711errorHandling.onError @ angular2.dev.js:10630run @ angular2.dev.js:141(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$$internal$$tryCatch @ angular2.dev.js:1507lib$es6$promise$$internal$$invokeCallback @ angular2.dev.js:1519lib$es6$promise$$internal$$publish @ angular2.dev.js:1490(anonymous function) @ angular2.dev.js:219microtask @ angular2.dev.js:10670run @ angular2.dev.js:138(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$asap$$flush @ angular2.dev.js:1301
angular2.dev.js:21835 TypeError: Cannot read property 'name' of undefined
    at AbstractChangeDetector.ChangeDetector_AppComponent_0.detectChangesInRecordsInternal (eval at <anonymous> (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:20415:14), <anonymous>:94:32)
    at AbstractChangeDetector.detectChangesInRecords (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:20209:14)
    at AbstractChangeDetector.runDetectChanges (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:20192:12)
    at AbstractChangeDetector._detectChangesInShadowDomChildren (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:20259:14)
    at AbstractChangeDetector.runDetectChanges (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:20196:12)
    at AbstractChangeDetector.detectChanges (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:20183:12)
    at http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:14960:27
    at Array.forEach (native)
    at LifeCycle_.tick (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:14959:31)
    at tick (http://127.0.0.1:8080/node_modules/angular2/bundles/angular2.dev.js:19629:16)BrowserDomAdapter.logError @ angular2.dev.js:21835ExceptionHandler.call @ angular2.dev.js:4444(anonymous function) @ angular2.dev.js:19543NgZone._onError @ angular2.dev.js:10711errorHandling.onError @ angular2.dev.js:10630run @ angular2.dev.js:141(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$$internal$$tryCatch @ angular2.dev.js:1507lib$es6$promise$$internal$$invokeCallback @ angular2.dev.js:1519lib$es6$promise$$internal$$publish @ angular2.dev.js:1490(anonymous function) @ angular2.dev.js:219microtask @ angular2.dev.js:10670run @ angular2.dev.js:138(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$asap$$flush @ angular2.dev.js:1301
angular2.dev.js:21835 ERROR CONTEXT:BrowserDomAdapter.logError @ angular2.dev.js:21835ExceptionHandler.call @ angular2.dev.js:4447(anonymous function) @ angular2.dev.js:19543NgZone._onError @ angular2.dev.js:10711errorHandling.onError @ angular2.dev.js:10630run @ angular2.dev.js:141(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$$internal$$tryCatch @ angular2.dev.js:1507lib$es6$promise$$internal$$invokeCallback @ angular2.dev.js:1519lib$es6$promise$$internal$$publish @ angular2.dev.js:1490(anonymous function) @ angular2.dev.js:219microtask @ angular2.dev.js:10670run @ angular2.dev.js:138(anonymous function) @ angular2.dev.js:10644zoneBoundFn @ angular2.dev.js:111lib$es6$promise$asap$$flush @ angular2.dev.js:1301
angular2.dev.js:21835 _Context {element: input, componentElement: my-app, context: AppComponent, locals: Object, injector: Injector

これは、最初、selectedHeroは、空っぽなので、表示がおかしくなってるみたいで、

selctedHeroが undefinedのときは、そもそもヒーロー詳細は表示しない、

と、書き換えておきます。

<div *ng-if="selectedHero">
  <h2>{{selectedHero.name}} details!</h2>
  <div><label>id: </label>{{selectedHero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ng-model)]="selectedHero.name" placeholder="name"></input>
  </div>
</div>

で、ここで登場する謎の、ng-if directive。でもまぁif文だよね。

例によって、importとdirectivesに追記して・・・

って、いう版↓↓

import {bootstrap, Component, FORM_DIRECTIVES, NgFor, NgIf} from 'angular2/angular2'; ★NgIf追記

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

var 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" }
];

// Annotation section
@Component({
  selector: 'my-app',
  styles:[`
  .heroes {list-style-type: none; margin-left: 1em; padding: 0; width: 10em;}
  .heroes li { cursor: pointer; position: relative; left: 0; transition: all 0.2s ease; }
  .heroes li:hover {color: #369; background-color: #EEE; left: .2em;}
  .heroes .badge {
    font-size: small;
    color: white;
    padding: 0.1em 0.7em;
    background-color: #369;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -1px;
  }
  .selected { background-color: #EEE; color: #369; }
  `],
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
      <li *ng-for="#hero of heroes" (click)="onSelect(hero)"> ★クリックイベント取得
        <span class="badge">{{hero.id}}</span> {{hero.name}}
      </li>
    </ul>
    <div *ng-if="selectedHero">   ★★★ここから修正★★★
      <h2>{{selectedHero.name}} details!</h2>
      <div><label>id: </label>{{selectedHero.id}}</div>
      <div>
        <label>name: </label>
        <input [(ng-model)]="selectedHero.name" placeholder="name"></input>
      </div>
    </div>            ★★★★ここまで★★★★
    `,
  directives: [FORM_DIRECTIVES, NgFor, NgIf] ★NgIf追記
})
// Component controller

class AppComponent {
  public title = 'Tour of Heroes';
  public heroes = HEROES;
  public selectedHero: Hero;      ★選択されたヒーロー格納用
  onSelect(hero: Hero) { this.selectedHero = hero; } ★onSelectメソッド追記
}

bootstrap(AppComponent);

結果は、こんな感じ↓↓↓ f:id:xengineer:20151105164620g:plain

  • ヒーローたちがリスト表示になってて
  • 選択できて
  • 選択すると詳細が表示されて
  • 名前を編集できる

おー。すばらC。

選択中のヒーローを見つけやすくする

現状、詳細情報をみれば、誰が選択されてるかがわかるけど、

リストの中から探すのはちょっと大変。

なので、選択中のヒーローのli tagに、selectedっていうclassをつけます。

で、StyleSheetで、そこはちょっと色つける、とかやる。

まずは、選択されてるヒーローを取得するメソッドを作ります。

getSelectedClass(hero: Hero) {
  return { 'selected': hero === this.selectedHero };
}

これを、AppComponentに追記すると、AppComponentの、member?の、

selectedHeroが、li tagのheroと等しいかどうか次第で、classに、selectedをつけたり外したりしてくれる。

で、liに、property bindingで、ng-class directiveなるものを入れてあげる。

property bindingは、きっとCSSの値変えられたりするんだろうな。

<li *ng-for="#hero of heroes"
  [ng-class]="getSelectedClass(hero)"
  (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

新しいdirective使っちゃったんで、またimportとdirectivesに追記。

import {bootstrap, Component, 
        FORM_DIRECTIVES, NgClass, NgFor, NgIf} from 'angular2/angular2';
directives: [FORM_DIRECTIVES, NgClass, NgFor, NgIf]

で、これを全部反映した版↓↓

import {bootstrap, Component, FORM_DIRECTIVES, NgClass, NgFor, NgIf} from 'angular2/angular2'; ★ここを変更

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

var 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" }
];

// Annotation section
@Component({
  selector: 'my-app',
  styles:[`
  .heroes {list-style-type: none; margin-left: 1em; padding: 0; width: 10em;}
  .heroes li { cursor: pointer; position: relative; left: 0; transition: all 0.2s ease; }
  .heroes li:hover {color: #369; background-color: #EEE; left: .2em;}
  .heroes .badge {
    font-size: small;
    color: white;
    padding: 0.1em 0.7em;
    background-color: #369;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -1px;
  }
  .selected { background-color: #EEE; color: #369; }
  `],
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
    <li *ng-for="#hero of heroes"
      [ng-class]="getSelectedClass(hero)"★ここを追記
      (click)="onSelect(hero)">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </li>
    </ul>
    <div *ng-if="selectedHero">
      <h2>{{selectedHero.name}} details!</h2>
      <div><label>id: </label>{{selectedHero.id}}</div>
      <div>
        <label>name: </label>
        <input [(ng-model)]="selectedHero.name" placeholder="name"></input>
      </div>
    </div>
    `,
  directives: [FORM_DIRECTIVES, NgClass, NgFor, NgIf] ★ここを変更
})
// Component controller

class AppComponent {
  public title = 'Tour of Heroes';
  public heroes = HEROES;
  public selectedHero: Hero;
  onSelect(hero: Hero) { this.selectedHero = hero; }
  getSelectedClass(hero: Hero) {
    return { 'selected': hero === this.selectedHero };
  }
}

bootstrap(AppComponent);

そうすると、こう動く。

おお、選択されている。

f:id:xengineer:20151105173149g:plain

でもこれでいまいち理解できてないのは、

ng-classの挙動。

<li *ng-for="#hero of heroes"
  [ng-class]="getSelectedClass(hero)"
  (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

これは描画時に発動するのかなぁ・・・

こう書いてあるということは、clickされた場合って、onSelectが呼び出されるけど、

呼びだされたあとに、ng-classが呼び出されるのかなぁ。

この辺がよくわからない。。。

directive配列肥大について

こんな小さいアプリ書いてるだけで、

こんなにdirective書かないといけないとなると、あとが怖いですね。

import {bootstrap, Component, FORM_DIRECTIVES, NgClass, NgFor, NgIf} from 'angular2/angular2';

それの改善ができるみたいです。

↑↑のimportの中の、FORM_DIRECTIVESは、ng-modelを利用するにあたって必要なdirectiveの詰め合わせでした。

他にもそういうのがあるようで。

今回使った、NgClass, NgFor, NgIfは、CORE_DIRECTIVES配列につめあわされているので、

それを書いておけばOK!!!だそうな。

というわけで、最終版↓↓

import {bootstrap, Component, FORM_DIRECTIVES, CORE_DIRECTIVES} from 'angular2/angular2'; ★ここを変更

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

var 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" }
];

// Annotation section
@Component({
  selector: 'my-app',
  styles:[`
  .heroes {list-style-type: none; margin-left: 1em; padding: 0; width: 10em;}
  .heroes li { cursor: pointer; position: relative; left: 0; transition: all 0.2s ease; }
  .heroes li:hover {color: #369; background-color: #EEE; left: .2em;}
  .heroes .badge {
    font-size: small;
    color: white;
    padding: 0.1em 0.7em;
    background-color: #369;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -1px;
  }
  .selected { background-color: #EEE; color: #369; }
  `],
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
    <li *ng-for="#hero of heroes"
      [ng-class]="getSelectedClass(hero)"
      (click)="onSelect(hero)">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </li>
    </ul>
    <div *ng-if="selectedHero">
      <h2>{{selectedHero.name}} details!</h2>
      <div><label>id: </label>{{selectedHero.id}}</div>
      <div>
        <label>name: </label>
        <input [(ng-model)]="selectedHero.name" placeholder="name"></input>
      </div>
    </div>
    `,
  directives: [FORM_DIRECTIVES, CORE_DIRECTIVES] ★ここを変更
})
// Component controller

class AppComponent {
  public title = 'Tour of Heroes';
  public heroes = HEROES;
  public selectedHero: Hero;
  onSelect(hero: Hero) { this.selectedHero = hero; }
  getSelectedClass(hero: Hero) {
    return { 'selected': hero === this.selectedHero };
  }
}

bootstrap(AppComponent);

これで目標のブツはできたぜー!!!

次は、せっかくなので、angular使ってSPAでも作ろうかな。

丁度作ってたやつが丁度よさげな題材なので。

あー、わからないことがいっぱいだった・・・

尚、angularjs 5min quick startをやってみた記事は、↓↓↓こちら angularjs2のtutorialをやってみた - xengineer’s diary

Tour of Heroesをやってみた記事の前半は、↓↓↓こちら angularjs2のtutorialをやってみた(その2) - xengineer’s diary