TypeScriptで書くAngularJSのMVC
(150522 追記)最新のチュートリアルをまとめたAngularJS モダンプラクティスを掲載しました。この記事は 2014 年 2 月に掲載したとてもふるい記事です。最新記事をどうぞご覧ください。
この記事は記録のため残します。
AngularJS はチュートリアルに沿ってただ書くだけにするとすぐ Fat Controller になる、とは他でも指摘されていますが、複数のコントローラを実装し始めた辺りから問題となってくるのが処理の重複です。すぐにでも一つのファイルにまとめて参照したいところです。
この記事(お前の Angular.js はもう MVC ではない。と言われないための Tutorial)やこの記事(AngularJS を TypeScript で書くときのあれこれ)にはずいぶんと助けられましたが、ここで自分なりの TypeScript での書き方についてまとめておきます。
Factory の例
クラスやメソッドの命名はすべて例示のためのものです。
/// <reference path="../vendor/angular.d.ts" />
/// <reference path="../app.ts" />
angular.module("myTable", []).factory("Table", ($rootScope) => {
return new Table($rootScope);
});
interface MainScope extends ng.IScope {}
class Table {
private rootScope: MainScope;
private numberOfSelected: number;
private checkboxes: {
items: any;
};
constructor($rootScope) {
this.rootScope = $rootScope;
this.initCheckboxes();
}
public initCheckboxes(): void {
this.checkboxes = {
items: {},
};
this.broadcastCheckboxes();
}
public toggleRecord(index, event): void {
// クリックで単一選択したり、
// キーコンビネーションで複数選択したりの処理色々…
this.setNumberOfSelected(n);
this.broadcastCheckboxes();
return;
}
public broadcastCheckboxes(): void {
this.rootScope.$broadcast("Table.checkboxes", this.checkboxes);
}
public setNumberOfSelected(value: number): void {
this.numberOfSelected = value;
this.rootScope.$broadcast("Table.numberOfSelected", this.numberOfSelected);
}
}
app.ts の例
定義したmyTable
モジュールを読み込みます。
interface MyApp extends ng.IModule {}
var app: MyApp = angular.module("myApplication", ["ngResource", "myTable"]);
Controller の例
行ごとにチェックボックスの並んだ表をイメージしてください。選択処理を行うごとに選択済みの件数を格納します。($resource
や$q
は今回の例では使用しませんが ajax 処理をしている雰囲気として…。急いでいたので any が多いのですが、TypeScript としてはきちんと書いたほうがいいです)
/// <reference path="../vendor/angular.d.ts" />
/// <reference path="../app.ts" />
app.controller("IndexCtrl", ($scope, $resource, $q, Table) => {
return new IndexCtrl($scope, $resource, $q, Table);
});
interface MainScope extends ng.IScope {
entities: string[];
toggleRecord: Function;
}
class IndexCtrl {
private scope: MainScope;
private resource: any;
private q: any;
private Table: any;
constructor($scope: MainScope, $resource, $q, Table) {
this.scope = $scope;
this.resource = $resource;
this.q = $q;
this.Table = Table;
$scope.toggleRecord = angular.bind(this, this.toggleRecord);
this.initEntities();
this.on();
}
public on(): void {
// Table
this.scope.$on("Table.checkboxes", (event, value) => {
this.scope.checkboxes = value;
});
this.scope.$on("Table.numberOfSelected", (event, value) => {
this.scope.numberOfSelected = value;
});
}
/**
* Tableに委譲
**/
public toggleRecord(index, event): void {
this.Table.toggleRecord(index, event);
}
public initEntities(): void {
// 以下略
}
}
View の例
実際、私は CakePHP で ctp ファイルから HTML を出力させています。
<!--略-->
<div ng-controller="IndexCtrl">
<table class="records">
<thead>
<tr class="table-header">
<th class="head-kind">KIND</th>
<th class="head-name">NAME</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="e in entities"
ng-class="{selected: checkboxes.items[$index]}"
ng-click="toggleRecord([$index], $event)"
>
<td class="cell-kind">{{e.kind}}</td>
<td class="cell-name">{{e.name}}</td>
</tr>
</tbody>
</table>
</div>
<!--略-->
View でのイベントを Factory で処理
<tr>
に設定したng-click
をトリガーにしてtoggleRecord(index, event)
が処理されますが、ただ単に Factory 内に実装しただけではトリガーが反応しないので、IndexCrtl
Controller からTable
Factory に委譲するためだけのインタフェースとしてIndexCtrl
にもtoggleRecord()
を実装します。event
引数はキーコンビネーション選択(shift 押しながらなど)のために用意しました。
Factory のインスタンス変数を Controller が共有
どの行がチェックされているかの情報はTable
Factory の変数が保有します。このままでは View 側でng-class
の判定が行えないので、この変数をIndexCtrl
も共有する必要があります。
このためTable
側にbroadcastCheckboxes()
メソッドを実装します。$rootScope.$broadcast()
の引数にはチャンネル名(と便宜上呼びます)と値を設定し、これはindexCtrl
側の$scope.$on
で受け取ります。$on
でもチャンネル名を合わせて、中の関数で受け取った値を処理します。こうすることでチェック済み行や行数の情報をTable
とIndexCtrl
とで共有でき、値をng-class
が判定し選択された行の色が CSS によって変わる仕組みです。
140224 Module を使用するよう改訂
色々考えた末に、汎用的な動作をまとめたものを特定のアプリケーションで縛るのは良くないと判断しangular.module('myTable', []).factory(...
と変更しました。var app
の宣言時にモジュールはまとめて読み込みますが、モジュール内との依存関係(今回の場合Table
Factory)はコントローラーにて注入するため混沌とはしないはずです。Module 定義と Factory 定義の扱いについては、AngularUI の手法を参考にしました。
もうひとつ、$broadcast
の名称に名前空間を与えました。AngularJS style guideによると名前リストを作れとありますが、これは煩雑になってしまうので、ドット表記による命名規則で管理するようにしています。
まとめ
長くなりましたが、Factory と Controller 間の値渡しをマスターするとこういった簡単な Web アプリケーションが自分のイメージ通りに組めていくので気分も楽です。ぜひ$broadcast
を使ってみてください。
Discussion