TypeScript
AngularJS

TypeScriptで書くAngularJSのMVC

More than 3 years have passed since last update.

(150522追記)最新のチュートリアルをまとめたAngularJSモダンプラクティスを掲載しました。この記事は2014年2月に掲載したとてもふるい記事です。最新記事をどうぞご覧ください。

この記事は記録のため残します。


AngularJSはチュートリアルに沿ってただ書くだけにするとすぐFat Controllerになる、とは他でも指摘されていますが、複数のコントローラを実装し始めた辺りから問題となってくるのが処理の重複です。すぐにでも一つのファイルにまとめて参照したいところです。

この記事(お前のAngular.jsはもうMVCではない。と言われないためのTutorial)やこの記事(AngularJSをTypeScriptで書くときのあれこれ)にはずいぶんと助けられましたが、ここで自分なりのTypeScriptでの書き方についてまとめておきます。


Factoryの例

クラスやメソッドの命名はすべて例示のためのものです。


Table.ts

/// <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モジュールを読み込みます。


app.ts

interface MyApp extends ng.IModule {}    

var app: MyApp = angular.module('myApplication', ['ngResource', 'myTable']);


Controllerの例

行ごとにチェックボックスの並んだ表をイメージしてください。選択処理を行うごとに選択済みの件数を格納します。($resource$qは今回の例では使用しませんがajax処理をしている雰囲気として…。急いでいたのでanyが多いのですが、TypeScriptとしてはきちんと書いたほうがいいです)


IndexCtrl.ts

/// <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を出力させています。


index.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内に実装しただけではトリガーが反応しないので、IndexCrtlControllerからTableFactoryに委譲するためだけのインタフェースとしてIndexCtrlにもtoggleRecord()を実装します。event引数はキーコンビネーション選択(shift押しながらなど)のために用意しました。


Factoryのインスタンス変数をControllerが共有

どの行がチェックされているかの情報はTableFactoryの変数が保有します。このままではView側でng-classの判定が行えないので、この変数をIndexCtrlも共有する必要があります。

このためTable側にbroadcastCheckboxes()メソッドを実装します。$rootScope.$broadcast()の引数にはチャンネル名(と便宜上呼びます)と値を設定し、これはindexCtrl側の$scope.$onで受け取ります。$onでもチャンネル名を合わせて、中の関数で受け取った値を処理します。こうすることでチェック済み行や行数の情報をTableIndexCtrlとで共有でき、値をng-classが判定し選択された行の色がCSSによって変わる仕組みです。


140224 Moduleを使用するよう改訂

色々考えた末に、汎用的な動作をまとめたものを特定のアプリケーションで縛るのは良くないと判断しangular.module('myTable', []).factory(...と変更しました。var appの宣言時にモジュールはまとめて読み込みますが、モジュール内との依存関係(今回の場合TableFactory)はコントローラーにて注入するため混沌とはしないはずです。Module定義とFactory定義の扱いについては、AngularUIの手法を参考にしました。

もうひとつ、$broadcastの名称に名前空間を与えました。AngularJS style guideによると名前リストを作れとありますが、これは煩雑になってしまうので、ドット表記による命名規則で管理するようにしています。


まとめ

長くなりましたが、FactoryとController間の値渡しをマスターするとこういった簡単なWebアプリケーションが自分のイメージ通りに組めていくので気分も楽です。ぜひ$broadcastを使ってみてください。


拙著の関連記事