『AngularJS: Up and Running』読書メモ 第2章 コントローラーとディレクティブ

AngularJS: Up and Running

AngularJSのモジュール

最初に学ぶべき概念は「モジュール」である。
AngularJSでは、モジュールによって、関連するコードを1つにパッケージする。
AngularJSのモジュールには2つの部品がある。

  • モジュールは自身のコントローラー、サービス、ファクトリ、ディレクティブを定義することができる
    • これらにはモジュールを通してアクセスできる
  • モジュールは別のモジュールに依存することができる
    • モジュールの初期化処理において依存モジュールを指定する

モジュールは、関連するJavaScriptの容れ物であるのと同時に、AngularJSがアプリケーションを開始するのに使用するものでもある。
ng-appディレクティブで、アプリケーションのエントリポイントを指定できる。

var app = angular.module('myApp', []); // myAppモジュール初期化
app = angular.module('myApp'); // myAppモジュールを取得

angular.moduleメソッドの第1引数はモジュール名、第2引数は依存しているモジュール名の配列になる。
第2引数を渡さない場合、myAppという名前のモジュールを探して返す。

コントローラーの作成

AngularJSにおけるコントローラーの役割は以下のようになる。

  • サーバから現在のUIに合わせたデータを取得する
  • ユーザにどのデータを見せるか決める
  • 表示のロジック
  • ユーザーとのやりとり

UIに関係のないコントローラーは存在しない(UIに関係ないビジネスロジックはサービスに書くべき)。
コントローラーは以下のように定義する。

angular.module('myApp', [])
    .controller('MainCtrl', ['$scope', function($scope) {
        $scope.greeting = 'hello';
    }]);

AngularJSのモジュールのcontrollerメソッドを使って、コントローラーを定義することができる。
controllerメソッドは第1引数にコントローラー名、第2引数に配列を取る。
第2引数の配列には、依存しているモジュール名の文字列を入れ、配列の最後の要素にコントローラーとなる関数を入れる。
(依存モジュールが無い場合は関数だけを入れる)

なお、第2引数には配列でなく関数を渡すこともできるが、JavaScriptコードを圧縮すると動作しなくなるため非推奨。

// 非推奨の書き方
angular.module('myApp', [])
    .controller('MainCtrl', function($scope) {
        $scope.greeting = 'hello';
    });

また、従来のサンプルコードでよく使われていた、グローバルに関数を定義してコントローラーとする方法は、1.3以降では動かない。

ng-controllerディレクティブを使うことで、コントローラーを指定できる。

<html ng-app='myApp'>
<head>
<script src='https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js'>
</script><script>
angular.module('myApp', [])
    .controller('MainCtrl', [function () {
        this.helloMsg = 'Hello';
    }]);
</script>
</head>
<body ng-controller='MainCtrl as ctrl'>
{{ctrl.helloMsg}}
</body>
</html>

ControllerAs記法はAngularJS1.2で導入された機能で、コントローラー自身のプロパティにビューからアクセスできるようにすることができる。
上記サンプルでは、ng-controllerのControllerAs記法でMainCtrlに別名をつけ、MainCtrlに定義されたプロパティにアクセスしている。

コントローラーがネストされた複雑なUIでは、ビューの変数がどこから来ているか判別するのが難しいことがある。
ControllerAs記法を使うと、コントローラーの名前によって変数の出処がはっきりする。

配列を扱う

ng-repeatディレクティブを使うことで、配列を表示することができる。

angular.module('myApp', [])
    .controller('MainCtrl', [function() {
        var self = this;
        self.notes = [
            {id: 1, label: 'first'},
            {id: 2, label: 'second'},
            {id: 3, label: 'third'},
        ];
    }]);

ng-repeatディレクティブを使って、MainCtrl.notesを表示するコードは以下。

<div ng-repeat="note in ctrl.notes">
    <span class="label" ng-bind="::note.label"></span>
</div>

ここで、ng-repeatの付いた要素と、その中身の要素は、繰り返しの際のテンプレートになる。
上記コードの場合、中にspanを含むdivが3つ生成される。

ここでは、ビューで{{}}の代わりにng-bindディレクティブを使っている。
{{}}を使うと、AngularJSアプリケーションの読み込みが完了するまで{{}}が表示されてしまうことがあるが、ng-bindではこの問題を回避できる。
この問題は、ng-cloakディレクティブを使うことでも回避可能である。

AngularJSのロードが完了するのを待つ

angular.js(又はangular.min.js)を読み込むと、以下のCSSが自動的に定義される。

[ng:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
    display: none !important;
}

[ng-cloak]は、ng-cloakという属性を持つ要素に対するセレクタなので、このCSSルールでは、ng-cloak属性のついた要素を非表示にする。
ロードが完了すると、AngularJSはng-cloak属性を要素から取り除くため、要素が画面上に表示される。
bodyタグにng-cloakをつけることもできるが、細かなパーツごとにつけていった方がユーザビリティが良い。

angular.jsのソースコードの読み込みが完了するまで、上記CSSは定義されていないことに注意が必要。
※一般に、CSSは先に、JavaScriptは後に読み込むほうがページ表示のパフォーマンスが良くなる。
上記CSSルールを自前のCSSファイルの中で定義しておくことで、より確実に非表示にすることができる。

AngularJSの処理の流れ

  1. HTMLの読み込みが完了し、JavaScriptの読み込みが始まる
  2. ドキュメント全体の読み込みが完了し、AngularJSの動作が始まる
  3. ng-appディレクティブを見つけると、関連するモジュールを探して読み込む
  4. ng-app配下のDOMを探索し、ディレクティブやバインド文を探す
  5. ng-controller又はng-repeatディレクティブを見つけたら、その都度scopeを作成する
  6. HTMLからアクセスされる変数にウォッチャーとリスナーを登録する(2way-binding)
  7. AngularJSはモデルのデータが変わった場合にのみ、UIの更新を行う

ワンタイムバインディングによるngBindの最適化

AngularJS1.3以上では、ワンタイムバインディングを利用することでngBindのパフォーマンスを最適化できる。

以下のようなデータがあると想定する。

// コントローラーの定義内
this.notes = [
    {id: 1, label: 'first'},
    {id: 2, label: 'second'},
    {id: 3, label: 'third'},
];

通常のngBindと、ワンタイムバインドを使った構文の例は以下。

<div ng-repeat="note in ctrl.notes">
    Two-way binding: <div ng-bind="note.label">
    Onet-time binding: <div ng-bind="::note.label">
</div>

ワンタイムバインディングの方では、変数の前に「::」をつけている。
この記号をつけると、変数は初回表示の時にだけ使用され、その後更新はされなくなる。
ページ内で双方向バインディングされている変数の数が多くなるほど、更新処理のオーバーヘッドは大きくなる。
ワンタイムバインディングを使うことで、更新処理のオーバーヘッドを最小化することができる。

その他のディレクティブ

ng-show/ng-hide

ng-show/ng-hideディレクティブを使うと、要素の表示非表示を制御することができる。

<span ng-show="true">show when true</span>
<span ng-hide="true">hide when true</span>

ng-show/hideの条件には式を渡すこともできる。

<span ng-show="ctrl.returnTrueFunc()">show when true</span>

ng-show/hideで真として評価される値は、Booleanのtrue、空白でない文字列、0でない数値、nullでないJavaScriptのオブジェクトである。

ng-class

ng-classディレクティブはCSSクラスを付け外しするのに使用される。
ng-classディレクティブには、適用するクラスを文字列として渡す使い方と、オブジェクトを渡す使い方がある。

<span ng-class="warning">.warning is applied</span>
<span ng-class="{'success': false, 'warning': true}">.warning is applied</span>

オブジェクトを渡す方法の場合、渡すオブジェクトの値がtrueである要素のキーがクラスとして適用される。
実際のコードでは以下のような実装になる。

// コントローラーの定義
this.getClass = function(status) {
    return {
        done: status,
        penging: !status
    }
};

以下はビュー。itemという変数にstatusというプロパティがあるとする。

<span ng-class="ctrl.getClass(item.status)">item.status</span>

上記コードの場合、item.statusがtrueであれば.doneが、falseであれば.pendingが適用される。

ng-repeatを扱う

ng-repeatは繰り返し行うディレクティブである。

オブジェクトをng-repeatする

以下のようなオブジェクトがあるとする。

self.notes = {
    shyam: {
        id: 1,
        label: 'First Note',
        done: false
    },
    Misko: {
        id: 3,
        label: 'Finished Third Note',
        done: true
    },
    brad: {
        id: 2,
        label: 'Second Note',
        done: false
    }
};

このオブジェクトをng-repeatで回す場合は、ビューを以下のように書く。

<div ng-repeat="(author, note) in ctrl.notes">
    <span class="label"> {{note.label}}</span>
    <span class="author" ng-bind="author"></span>
</div>

ng-repeatはオブジェクトをループする場合、アルファベット順で、大文字の方が先になる。そのため、上の例ではMisko, brad, shyamの順に表示される。

ng-repeatは、value in Array又は(key, value) in Objectという形をとる。配列の場合には、ng-repeatは配列の先頭から順に表示する。

ng-repeatのヘルパー変数

ng-repeatには、HTMLテンプレートで利用できるヘルパー変数が存在する。

    <div ng-repeat="note in ctrl.notes"> <div>First Element: {{$first}}</div> <div>Middle Element: {{$middle}}</div> <div>Last Element: {{$last}}</div> <div>Index of Element: {{$index}}</div> <div>At Even Position: {{$even}}</div> <div>At Odd Position: {{$odd}}</div>
    <span class="label"> {{note.label}}</span>
    <span class="status" ng-bind="note.done"></span> <br/><br/>
</div>

Track by ID

デフォルトでは、ng-repeatは繰り返しの度に新しいDOM要素を作る。
しかし、パフォーマンス最適化のため、作成されるオブジェクトが同じであればDOMのキャッシュ又は再利用を行う。
(同一性の検証にはオブジェクトのハッシュ値を用いている)

オブジェクトの同一性に関係なく、同じDOM要素を再利用して欲しいこともある。
この場合には、AngularJSにトラッキングのための情報を渡すことができる。

<div ng-repeat="note in ctrl.notes2 track by note.id"> {{note.$$hashKey}}
    <span class="label"> {{note.label}}</span>
    <span class="author" ng-bind="note.done"></span>
</div>

track by にトラッキングのための変数を設定すると、この変数の値が同じであればDOM要素が再利用される。

$$ で始まる変数を使ってはいけない

$$ で始まる変数を、アプリケーションのコードで使ってはいけない。
AngularJSは $$ で始まる変数をAngularJS内のプライベート変数を示すものとして使用しており、これらの変数が将来に渡っても使える保証はない。

複数のHTML要素にまたがったng-repeat

テーブルを2行ごと(

2つごと)に繰り返す、といった操作が必要になることがある。
ng-repeat-startとng-repeat-endディレクティブを使うことで、AngularJSに、ng-repeatの開始点と終了点を知らせることができる。

<tr ng-repeat-start="note in ctrl.notes">
    <td>{{note.label}}</td>
</tr>
<tr ng-repeat-end>
    <td>Done: {{note.done}}</td>
</tr>

コメントをどうぞ

コメントを残す