AngularJS x TypeScript でちょっと本格的な TODO アプリを作ってみる – AngularJS + TypeScript #2

controllerAs について追記しました。

はじめに

とりあえず何かアプリっぽいものを作ってみようということで、定番の TODO アプリに挑戦してみようと思います。

ググればいくらでも情報は出てきますが、この記事では以下にあるようないかにもチュートリアルっぽいものからもう一歩踏み込んで、より実践的かつ規模の大きな案件にも応用出来るような作りを目指してみます。

サンプルコードはこちらからどうぞ。

機能要件と実装方針

まずは TODO アプリの機能要件を大まかに書き出してみます。

  • TODO の登録・編集・削除
  • 登録済み TODO を一覧表示
  • 完了した TODO にはチェックを付けることができる
  • 登録済み TODO と完了した TODO の数をそれぞれ表示

サンプルということでシンプルに登録・編集・削除だけに絞っていますが、実装方針を以下のようにして作りを本格的にしてみるとします。

  • AltJS に TypeScript を使う
  • フレームワークに AngularJS を使う
  • コントローラ、ディレクティブをクラス化して外部モジュールとして外出しする

完成予想イメージはこちら。

スクリーンショット 2015-05-16 17.14.44

ではこれより実装していくわけですが、いきなりディレクティブ化したりクラス化したりせずに、順を追って進めていくとします。

#1. コントローラと HTML 要素だけのシンプルな構成

HTML

まずはシンプルに View は全て HTML 側に記述し、コントローラだけを TypeScript (JS) 側で書いていくとします。

<div ng-app="app">
    <div ng-controller="todoController">
        <section class="panel panel-default">
            <header class="panel-heading">
                <div class="input-group">
                    <input type="text" class="form-control" ng-model="message" placeholder="ToDo ..." />
                    <span class="input-group-btn">
                        <button class="btn btn-primary" ng-click="addTodoItem(message)">Add</button>
                    </span>
                </div>
            </header>
            <ul class="list-group">
                <li class="list-group-item" ng-repeat="todoItem in todoItems">
                    <div class="list-group-item-inner">
                        <div class="item-wrapper">
                            <input type="checkbox" ng-model="todoItem.done" />
                        </div>
                        <label class="done-{{todoItem.done}}" ng-dblclick="updateTodoItem(todoItem)">{{todoItem.message}}</label>
                        <div class="item-wrapper">
                            <button class="btn btn-danger btn-xs" ng-click="removeTodoItem(todoItem.id)">&times;</button>
                        </div>
                    </div>
                </li>
            </ul>
            <footer class="panel-footer">
                <span class="badge">{{remaining()}} / {{todoItems.length}}</span> Items left
            </footer>
        </section>
    </div>
</div>

テキストインプットの入力値を message というモデルとバインドします。Add ボタンをクリックして addTodoItem() というメソッドを呼び出し、入力した message 値を引数として渡します。
登録済みの Todo は todoItems という変数に配列で格納され、これをループ処理で1つずつ表示していきます。
チェックボックスにチェックを入れると todoItem.done というモデルの値 (boolean) が切り替わり、ラベルのスタイルが動的に変化します。
ラベルをダブルクリックすると updateTodoItem() というメソッドを呼びだし、Todo の編集ビューを表示します。
削除ボタンをクリックすると removeTodoItem() というメソッドを呼び出し、そのTodoを一覧から削除します。
remaining() という関数を呼び出し、todoItem.done が true となっているTodoの数を取得します。

CSS (SCSS)

Bootstrap をベースにしつつ、調整用のスタイルを加えます。

// Import libs
// --------------------
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
// Variables
// --------------------
$theme-primary: #DB1E21;
// Scaffoldings
.text-primary {
  color: $theme-primary;
}
// TODO
// --------------------
.done-true {
  text-decoration: line-through;
  color: gray;
  font-style: italic;
}
.list-group-item-inner {
  display: flex;
  label {
    font-size: 18px;
    width: 100%;
    padding: 0 15px;
  }
}
.item-wrapper {
  > * {
    vertical-align: sub;
  }
}

JavaScript (TypeScript)

ビューは全て HTML 側で定義したので、コントローラだけ記述します。

/// <reference path="./typings/tsd.d.ts" />
var app = angular.module('app', []);
app.controller('todoController', ['$scope', ($scope) => {
    $scope.todoItems = [];
    $scope.message = '';
    var index = 0;
    // todoItem を追加
    $scope.addTodoItem = (msg) => {
        $scope.todoItems.push({
            id: index,
            message: msg,
            done: false
        });
        $scope.message = '';
        index++;
    };
    // todoItem を更新
    $scope.updateTodoItem = (todoItem => {
        var message = window.prompt('変更', todoItem.message);
        if (message) {
            var t;
            for (var i = 0; i < $scope.todoItems.length; i++) {
                t = $scope.todoItems&#91;i&#93;;
                if (t.id == todoItem.id) {
                    t.message = message;
                    break;
                }
            }
        }
    };
    // todoItem を削除
    $scope.removeTodoItem = (id) => {
        var index = 1;
        var t;
        for (var i = 0; i < $scope.todoItems.length; i++) {
            t = $scope.todoItems&#91;i&#93;;
            if (t.id == id) {
                index = i;
                break;
            }
        }
        $scope.todoItems.splice(index, 1);
    };
    // 完了アイテム数を取得
    $scope.remaining = () => {
        var count = 0;
        $scope.todoItems.forEach((todo) => {
            count += todo.done;
        });
        return count;
    };
}]);

$scope.updateTodoItem() の引数に todoItem を受け取って変更処理を行います。変更処理はお手軽という理由からとりあえず window.prompt() を利用しておきます。

DEMO

#2. リスト部分をまるごとディレクティブ化する

全てのビューが HTML 側に記述されているので、ここからカスタムディレクティブ化していきます。まずはリスト部分をまるごと一つのディレクティブとして切り出します。

HTML

<div ng-app="app">
    <div ng-controller="todoController">
        <section class="panel panel-default">
            <header class="panel-heading">
                <div class="input-group">
                    <input type="text" class="form-control" ng-model="message" placeholder="ToDo ..." />
                    <span class="input-group-btn">
                        <button class="btn btn-primary" ng-click="addTodoItem(message)">Add</button>
                    </span>
                </div>
            </header>
            <todo-list todo-items="todoItems"></todo-list>
            <footer class="panel-footer">
                <span class="badge">{{remaining()}} / {{todoItems.length}}</span> Items left
            </footer>
        </section>
    </div>
</div>

todoList というカスタムディレクティブ (タグ)に置き換えました。todo-items というディレクティブ (属性) に リストデータを受け取って Todo アイテムを一覧表示させます。

JavaScript (TypeScript)

todoList というカスタムディレクティブを作成します。

app.directive('todoList', [()=> {
    return {
        restrict: 'EA',
        replace: true,
        scope: {
            todoItems: '=',
        },
        template: '<ul class="list-group">' +
                    '<li class="list-group-item" ng-repeat="todoItem in todoItems">' +
                        '<div class="list-group-item-inner">'+
                            '<div class="item-wrapper"><input type="checkbox" ng-model="todoItem.done"></div>' +
                            '<label class="done-{{todoItem.done}}" ng-dblclick="update($event, todoItem)">{{todoItem.message}}</label>'+
                            '<div class="item-wrapper">'+
                                '<button class="btn btn-xs btn-danger" ng-click="delete($event, todoItem.id)">×</button>'+
                            '</div>'+
                        '</div>'+
                    '</li>'+
                  '</ul>',
        link: (scope, iElement)=> {
            // todoItem を更新
            scope.update = ($event, todoItem) => {
                var message = window.prompt('変更', todoItem.message);
                if (message) {
                    var t;
                    for (var i = 0; i < scope.todoItems.length; i++) {
                        t = scope.todoItems[i];
                        if (t.id == todoItem.id) {
                            t.message = message;
                            break;
                        }
                    }
                }
            };
            // todoItem を削除
            scope.delete = ($event, itemId) => {
                var index = 1;
                var t;
                for (var i = 0; i < scope.todoItems.length; i++) {
                    t = scope.todoItems[i];
                    if (t.id == itemId) {
                        index = i;
                        break;
                    }
                }
                scope.todoItems.splice(index, 1);
            };
        }
    }
}]);

一覧表示や Todo アイテムの更新・削除処理を todoList ディレクティブに移しました。したがってコントローラには Todo アイテム追加と完了アイテム数を取得する処理だけが残ります。

app.controller('todoController', ['$scope', ($scope) => {
    $scope.todoItems = [];
    $scope.message = '';
    var index = 0;
    // todoItem を追加
    $scope.addTodoItem = (msg) => {
        $scope.todoItems.push({
            id: index,
            message: msg,
            done: false
        });
        $scope.message = '';
        index++;
    };
    // 完了アイテム数を取得
    $scope.remaining = () => {
        var count = 0;
        $scope.todoItems.forEach((todo) => {
            count += todo.done;
        });
        return count;
    };
}]);

todoItems モデル自体はコントローラ側で管理してます。そしてディレクティブに todo-items という属性を定義してこれを scope の todoItems というプロパティにデータバインディングで結びつけ、そこからコントローラの todoItems モデルの値を渡します。

DEMO

#3. リストアイテムを個別のディレクティブとして切り出す

Todo リストからリストアイテムを個別のディレクティブとして切り出します。

HTML

<div ng-app="app">
    <div ng-controller="todoController">
        <section class="panel panel-default">
            <header class="panel-heading">
                <div class="input-group">
                    <input type="text" class="form-control" ng-model="message" placeholder="ToDo ..." />
                    <span class="input-group-btn">
                        <button class="btn btn-primary" ng-click="addTodoItem(message)">Add</button>
                    </span>
                </div>
            </header>
            <todo-list class="list-group">
                <!-- ( 1 ) -->
                <todo-item todo="todoItem" ng-repeat="todoItem in todoItems"></todo-item>
            </todo-list>
            <footer class="panel-footer">
                <span class="badge">{{remaining()}} / {{todoItems.length}}</span> Items left
            </footer>
        </section>
    </div>
</div>

todoItem というディレクティブを新規に作りました。このディレクティブに ng-repeat でループ処理を行い、todoItem オブジェクトを todo 属性に渡してリストアイテムを生成させます [1]

JavaScript (TypeScript)

コントローラ

追加処理はコントローラ側で定義されているのと、todoItems 自体もコントローラ側で管理されているので、やはり編集・削除処理もコントローラ側に移すことにします (スミマセン…)。

app.controller('todoController', ['$scope', function($scope) {
    var index = 0;
    $scope.todoItems = [];
    // todoItem を追加
    $scope.addTodoItem = (msg) => {
        $scope.todoItems.push({
            id: index,
            message: msg,
            done: false
        });
        index++;
        $scope.message = '';
    };
    // todoItem を削除
    this.removeTodoItem = function(todoItem) {
        var index = 0;
        var t;
        for (var i = 0; i < $scope.todoItems.length; i++) {
            t = $scope.todoItems[i];
            if (t.id == todoItem.id) {
                index = i;
                break;
            }
        }
        $scope.todoItems.splice(index, 1);
    };
    // 管理している todoItem の編集モードを全てキャンセルする
    this.cancelAll = function() {
        $scope.todoItems.forEach((todoItem) => {
            if (todoItem.isEditMode) {
                todoItem.cancel();
            }
        });
    };
    // 完了アイテム数を取得
    $scope.remaining = () => {
        var count = 0;
        $scope.todoItems.forEach((item) => {
            count += item.done;
        });
        return count;
    };
}]);

TodoList ディレクティブ

次にディレクティブを書いていきますが、コントローラにある処理の呼び出しはディレクティブから行いたいので、ディレクティブにコントローラをインジェクト (DI)して処理を呼び出せるようにします。コントローラのインジェクトは todoList ディレクティブに対して行います [1]

app.directive('todoList', () => {
    return {
        restrict: 'EA',
        replace: true,
        controller: 'todoController'  // [1]
    }
});

TodoItem ディレクティブ

そして todoItem ディレクティブを todoList ディレクティブに依存させます [2] 。require で依存するディレクティブを指定すると、指定ディレクティブのコントローラを link の引数として受け取ることが可能となり、コントローラに定義された処理を呼び出すことが出来るようになります [3]

app.directive('todoItem', () => {
    return {
        restrict: 'EA',
        require: '^todoList',  // [2]
        replace: true,
        template: '<div class="list-group-item">'+
                    '<div class="list-group-item-inner" ng-hide="isEditMode">' +
                        '<div class="item-wrapper"><input type="checkbox" ng-model="todo.done" /></div>'+
                        '<label class="done-{{todo.done}}" ng-dblclick="startEdit(todo)">{{todo.message}}</label>' +
                        '<div class="item-wrapper"><button class="btn btn-danger btn-xs" ng-click="delete(todo)">×</button></div>' +
                    '</div>'+
                    '<div ng-show="isEditMode">'+
                        '<input ng-model="todo.message" class="form-control input-sm" todo-focus ng-blur="updateTodoItem($event)" ng-keyup="updateTodoItem($event)" />' +
                    '</div>'+
                  '</div>',
        scope: {
            todo: '='
        },
        link: (scope: any, element, attrs, TodoController) => {  // [3]
            scope.isEditMode = false;  // [4]
            // 編集モードの開始
            scope.startEdit = (todo) => {
                TodoController.cancelAll();
                scope.isEditMode = true;
            };
            // 編集終了
            scope.updateTodoItem = ($event) => {
                if ($event.type === 'keyup') {
                    if ($event.which !== 13) return;
                } else if ($event.type !== 'blur') {
                    return;
                }
                scope.isEditMode = false;
                $event.stopPropagation();
            };
            // // 編集キャンセル
            scope.cancel = () => {
                if (!scope.isEditMode) return;
                scope.isEditMode = false;
            };
            // Todo アイテムを削除
            scope.delete = (todo) => {
                TodoController.removeTodoItem(todo);
            };
        }
    }
});

また、これまで編集のUIに window.prompt()を使っていましたが、少々イケてないのでラベルをテキストインプットに差し替えるUIに変更します。isEditMode というフラグを定義し、これの値に応じて通常モードと編集モードを切り替えます [4]

TodoFocus ディレクティブ

さらに編集モードに切り替わると同時に対象のテキストインプットにフォーカスが当たるようにしたいので、 todoFocus というカスタムディレクティブを新規に作成します。$watch() で isEditMode の変更を監視し、変更されたらフォーカスが当たるようにするわけですが、普通に element[0].focus(); とするとするだけではビューが切り替わるよりも早く実行されてしまってフォーカスが当たらないので、 $timeout モジュールを使ってタイミングを調整します [5]

app.directive('todoFocus', ($timeout) => {
    return {
        link: (scope: any, element, attrs)=> {
            scope.$watch('isEditMode', (newVal) => {
                // [5]
                $timeout(() => {
                    element[0].focus();
                }, 0, false);
            });
        }
    }
});

DEMO

#4. コントローラとディレクトリをクラス化して外部モジュール化する

ここまででだいぶ処理の分割が出来ましたが、せっかく TypeScript を使うのだから、コントローラとディレクティブをクラス化してしまいましょう。さらに外部モジュール化すればファイルを分割出来るので、アプリの規模が大きくなっても管理しやすくなり、処理の再利用性が高まります。

JavaScript (TypeScript)

処理の実態は先程の例で出来上がっているので、ここで行うのはクラス化するための修正程度です。

コントローラ

まずはコントローラですが、追加・削除・完了アイテム数の取得処理をもたせます。

class TodoController {
    private index = 0;
    constructor(private $scope: ITodoScope) {
        $scope.addTodoItem = angular.bind(this, this.addTodoItem);
        $scope.remaining = angular.bind(this, this.remaining);
        this.$scope.todoItems = [];
    }
    public addTodoItem(msg:string) {
        this.$scope.todoItems.push({
            id: this.index,
            message: msg,
            done: false,
            isEditMode: false
        });
        this.$scope.message = '';
        this.index++;
    }
    public removeTodoItem(id: number) {
        var index = 0;
        for (var i = 0; i < this.$scope.todoItems.length; i++) {
            if (this.$scope.todoItems[i].id === id) {
                index = i;
                break;
            }
        }
        this.$scope.todoItems.splice(index, 1);
    }
    public remaining(): number {
        var count = 0;
        this.$scope.todoItems.forEach((todoItem) => {
          count += todoItem.done? 1 : 0;
        });
        return count;
    }
}

$scopeオブジェクトは通常 ng.IScope というインターフェースで実装されていますが、todoItems, addTodoItem(), remaining() を追加したいので、専用のインターフェースを定義します。

interface ITodoScope extends ng.IScope {
    todoItems: TodoItem[];
    addTodoItem: Function;
    remaining: Function;
    message: string;
}

また、todoItems は Todo アイテムの配列なわけですが、それ用のクラスとして TodoItem クラスも定義します。

class TodoItem {
    id: number;
    message: string;
    done: boolean;
    isEditMode: boolean;
}

TodoList ディレクティブ

こちらはそのままクラス化のお作法に則って書き直すだけでOKです。

class TodoListDirective implements ng.IDirective {
    public restrict: string;
    public controller: string;
    constructor() {
        this.restrict = 'EA';
        this.controller = 'todoController';
    }
    public static Factory(): ng.IDirectiveFactory {
        var directive = ()=> {
            return new TodoListDirective();
        }
        directive.$inject = [];
        return directive;
    }
}

TodoItem ディレクティブ

class TodoItemDirective implements ng.IDirective {
    public restrict: string;
    public replace: boolean;
    public require: string;
    public template: string;
    public link: (scope: ITodoItemDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, todoController: TodoController) => void;
    constructor() {
        this.restrict = 'EA';
        this.replace = true;
        this.require = '^todoList';
        this.template = '<div class="list-group-item">' +
                            '<div class="list-group-item-inner" ng-hide="isEditMode">' +
                                '<div class="item-wrapper"><input type="checkbox" ng-model="todoItem.done" /></div>' +
                                '<label class="done-{{todoItem.done}}" ng-dblclick="startEdit(todoItem.id)">{{todoItem.message}}</label>' +
                                '<div class="item-wrapper"><button class="btn btn-danger btn-xs" ng-click="removeTodoItem(todoItem.id)">×</button></div>' +
                            '</div>' +
                            '<div ng-show="isEditMode">'+
                                '<input ng-model="todoItem.message" class="form-control input-sm" todo-focus ng-blur="updateTodoItem($event)" ng-keyup="updateTodoItem($event)" />' +
                            '</div>'+
                        '</div>';
        this.link = (scope: ITodoItemDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, todoController: TodoController)=> {
            scope.isEditMode = false;
            // 編集モードの開始
            scope.startEdit = (id) => {
                scope.isEditMode = true;
            };
            // 編集終了
            scope.updateTodoItem = ($event) => {
                if ($event.type === 'keyup') {
                    if ($event.which !== 13) return;
                } else if ($event.type !== 'blur') {
                    return;
                }
                scope.isEditMode = false;
                $event.stopPropagation();
            };
            // 編集キャンセル
            scope.cancelEdit = () => {
                if (!scope.isEditMode) return;
                scope.isEditMode = false;
            };
            // Todo アイテムを削除
            scope.removeTodoItem = (id) => {
                todoController.removeTodoItem(id);
            };
        }
    }
    public static Factory(): ng.IDirectiveFactory {
        var directive = ()=> {
            return new TodoItemDirective();
        }
        directive.$inject = [];
        return directive;
    }
}

特に変わったことはしてませんが、先程の例では scope の型を any としていたのに対してこの例ではITodoItemDirectiveScope というカスタムインターフェース型としています。

interface ITodoItemDirectiveScope extends ng.IScope {
    isEditMode: boolean;
    startEdit: Function;
    updateTodoItem: Function;
    cancelEdit: Function;
    removeTodoItem: Function;
}

TodoFocus ディレクティブ

$timeout モジュールを使うのでインジェクトする必要があります。TodoFocusDirective インスタンス作成 (コンストラクタ) 時に引数として渡し、さらにインスタンスの $inject というプロパティを使ってアノテーション指定することでインジェクト出来ます。

class TodoFocusDirective implements ng.IDirective {
    public link: (scope: any, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => void;
    constructor($timeout) {
        this.link = (scope: any, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
            scope.$watch('isEditMode', (newVal) => {
                $timeout(() => {
                    element[0].focus();
                }, 0, false);
            });
        }
    }
    public static Factory(): ng.IDirectiveFactory {
        var directive = ($timeout) => {
            return new TodoFocusDirective($timeout);
        }
        directive.$inject = ['$timeout'];
        return directive;
    }
}

外部モジュール化

クラス化出来たら外部モジュールとしてひとつに纏めます。外部モジュール化する方法は、各クラスやインターフェースに export キーワードを付け、モジュール名をファイル名として外部ファイルに保存します。今回は AngularTodo という名前で外部モジュール化します。

export interface ITodoScope extends ng.IScope {
   ⋮
}
export interface ITodoItemDirectiveScope extends ng.IScope {
    ⋮
}
export class TodoItem {
    ⋮
}
export class TodoController {
    ⋮
}
export class TodoListDirective implements ng.IDirective {
    ⋮
}
export class TodoItemDirective implements ng.IDirective {
    ⋮
}
export class TodoFocusDirective implements ng.IDirective {
    ⋮
}

外部モジュールの読み込み

import キーワードを使って外部モジュールを読み込みます。

/// <reference path="./typings/tsd.d.ts" />
import angular = require('angular');
var app = angular.module('app', []);
import AngularTodo = require('./AngularTodo');
app.controller('todoController', ['$scope', AngularTodo.TodoController]);
app.directive('todoList', AngularTodo.TodoListDirective.Factory());
app.directive('todoItem', AngularTodo.TodoItemDirective.Factory());
app.directive('todoFocus', AngularTodo.TodoFocusDirective.Factory());

なお、 require() 関数は Browserify や RequireJS 等を利用する必要があります。この辺りは以下の記事を参考にしてください。

[ 2015.05.23 追記 ] #5. controllerAs を使う

気になったので少し調べてみると、どうやら Angular2 からは $scope が廃止されるらしく、1.3 系の記述においても $scope を使わないことが望ましいとのこと。こちらの記事でも言及されてますね。

そんな訳で、コントローラを ControllerAs として使う方法に書き直すとします。

HTML を修正

まずは HTML 側を修正します。

<div ng-app="app">
    <div ng-controller="todoController as c">
        <section class="panel panel-default">
            <header class="panel-heading">
                <div class="input-group">
                    <input type="text" class="form-control" ng-model="c.message" placeholder="ToDo ..." />
                    <span class="input-group-btn">
                        <button class="btn btn-primary" ng-click="c.addTodoItem(c.message)">Add</button>
                    </span>
                </div>
            </header>
            <todo-list class="list-group">
                <!-- &#91;1&#93; -->
                <todo-item ng-repeat="todoItem in c.todoItems"></todo-item>
            </todo-list>
            <footer class="panel-footer">
                <span class="badge">{{c.remaining()}} / {{c.todoItems.length}}</span> Items left
            </footer>
        </section>
    </div>
</div>

コントローラ名 as 別名 とします。コントローラのプロパティやメソッドにアクセスするには 別名.プロパティ といったように記述すれば OK です。

コントローラを修正

次にコントローラを以下のように修正します。

export class TodoController {
    private index = 0;
    public todoItems: TodoItem[];
    public message: string;
    constructor() {
        this.todoItems = [];
    }
    public addTodoItem(msg: string) {
        this.todoItems.push({
            id: this.index,
            message: msg,
            done: false,
            isEditMode: false
        });
        this.message = '';
        this.index++;
    }
    public removeTodoItem(id: number) {
        var index = 0;
        for (var i = 0; i < this.todoItems.length; i++) {
            if (this.todoItems[i].id === id) {
                index = i;
                break;
            }
        }
        this.todoItems.splice(index, 1);
    }
    public remaining(): number {
        var count = 0;
        this.todoItems.forEach((todoItem) => {
            count += todoItem.done ? 1 : 0;
        });
        return count;
    }
}

修正内容は以下のとおり。

  1. コンストラクタの引数に $scope を受け取らない
  2. テンプレートに公開するプロパティやメソッドをコントローラ自身 (this)に登録
  3. Angular.bind() を使わない

$scope を使わないのでコンストラクタで受け取る必要がなくなります。それに伴って $scope の型として定義した ITodoScope も要らなくなりました。
todoItemsmessage といったテンプレートに公開するプロパティはコントローラ自身 (this) に定義します。そしてコントローラのメソッドを $scope 内に定義して紐付ける必要がなくなったので、angular.bind() を書かなくて済むようになりました。

TodoList ディレクティブを修正

ディレクティブ側にも controllerAs を指定します。

export class TodoListDirective implements ng.IDirective {
    public restrict: string;
    public controller: string;
    public controllerAs: string;
    public bindToController: boolean;
    constructor() {
        this.restrict = 'E';
        this.controller = 'todoController';
        this.controllerAs = 'c';
        this.bindToController = true;
    }
    public static Factory(): ng.IDirectiveFactory {
        var directive = ()=> {
            return new TodoListDirective();
        }
        directive.$inject = [];
        return directive;
    }
}

controllerAs プロパティを定義し、View 側で指定した別名と同じ値にします。また bindToController プロパティも定義し、true としておきます。と言ってもこのディレクティブに関しては template を持たないので、本来なら指定する必要は無かったりします。

外部モジュール読み込みを修正

$scope をアノテーションで指定する必要もなくなりました。

app.controller('todoController', AngularTodo.TodoController);

以上で controllerAs 化の修正は完了です。先ほどと変りなく動作するはずです。

おわりに

クラス化するにあたっての設計力がまだまだ貧弱ではありますが、今回のように分割出来たことで、自分なりに色々と拡張性の可能性を感じることが出来ました。

とりわけクラス化ディレクティブに 他モジュールをインジェクトする方法で少し苦労しましたが、この記事がどなたかの参考になれば幸いです。