かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

TypeScript + AngularJSでTodoのサンプルを書いてみた

2014/05/25追記

こういう書き方も出来ると紹介してもらいました。

ためしたところばっちり動いたので、次からはこう書こうと思いました。

はじめに

JavaScriptでSPA作るのにはAngularJSがいいらしいということで、とりあえずシンプルな例として、勉強がてら以下のページのしょっぱなにあるTodoアプリをTypeScriptで書いてみました。

プロジェクトの作成

TypeScriptHTMLApp1という名前(名前つけるのさぼった)でプロジェクトを作成して、NuGetから以下のライブラリを追加します。

  • angularjs
  • angularjs.TypeScript.DefinitelyTyped
  • Twitter.Bootstrap

Twitter.Bootstrapは、見た目をちょっと変えようかなと思ったからです。

コントローラの作成

AngularJSをTypeScriptで作るには、とりあえず以下の手順を踏むのがよさそうに感じました。

  • 使用するデータの型の作成
  • $scope用のインターフェースの定義
  • コントローラの作成

順番にTodoアプリでやってみます。

今回のTodoアプリでは、テキストと完了したかどうかをデータとして持つので、そのクラスを定義します。クラス名はTodoItemにしました。

class TodoItem {
    text: string;
    done: boolean;
}

そして、スコープを定義します。スコープはng.IScopeを拡張したインターフェースとして定義します。インターフェース名はTodoScopeにしました。

interface TodoScope extends ng.IScope {
    todos: Array<TodoItem>;
    todoText: string;

    addTodo: Function;
    remaining: Function;
    archive: Function;
}

関数は、全部Functionとして型指定するのが、個人的にちょっと気に入らないです。

そして、コントローラクラスです。コントローラは、コンストラクタでスコープを受け取り、スコープに必要なデータの設定と、スコープにメソッドの実体を設定します。

class TodoCtrl {

    constructor(private $scope: TodoScope) {
        $scope.todos = [
            { text: "AngularJSの学習", done: true },
            { text: "AngularJSのアプリケーション構築", done: false }
        ];
        $scope.addTodo = angular.bind(this, this.addTodo);
        $scope.remaining = angular.bind(this, this.remaining);
        $scope.archive = angular.bind(this, this.archive);
    }

    addTodo(): void {
        this.$scope.todos.push({ text: this.$scope.todoText, done: false });
        this.$scope.todoText = "";
    }

    remaining(): number {
        var count = 0;
        angular.forEach(this.$scope.todos, (todo: TodoItem) => {
            count += todo.done ? 0 : 1;
        });
        return count;
    }

    archive(): void {
        var old = this.$scope.todos;
        this.$scope.todos = [];
        angular.forEach(old, (todo: TodoItem) => {
            if (!todo.done) {
                this.$scope.todos.push(todo);
            }
        });
    }
}

angular.bindを使ってコントローラに定義したメソッドのthisをバインドしてスコープのメソッドに設定してると思われます。

app.tsの全体は以下のようになりました。

class TodoItem {
    text: string;
    done: boolean;
}

interface TodoScope extends ng.IScope {
    todos: Array<TodoItem>;
    todoText: string;

    addTodo: Function;
    remaining: Function;
    archive: Function;
}

class TodoCtrl {

    constructor(private $scope: TodoScope) {
        $scope.todos = [
            { text: "AngularJSの学習", done: true },
            { text: "AngularJSのアプリケーション構築", done: false }
        ];
        $scope.addTodo = angular.bind(this, this.addTodo);
        $scope.remaining = angular.bind(this, this.remaining);
        $scope.archive = angular.bind(this, this.archive);
    }

    addTodo(): void {
        this.$scope.todos.push({ text: this.$scope.todoText, done: false });
        this.$scope.todoText = "";
    }

    remaining(): number {
        var count = 0;
        angular.forEach(this.$scope.todos, (todo: TodoItem) => {
            count += todo.done ? 0 : 1;
        });
        return count;
    }

    archive(): void {
        var old = this.$scope.todos;
        this.$scope.todos = [];
        angular.forEach(old, (todo: TodoItem) => {
            if (!todo.done) {
                this.$scope.todos.push(todo);
            }
        });
    }
}

Viewの作成

Viewは特に例と変わらず。app.cssにTodoのチェックが入ったときのスタイルを例の通り定義します。

.done-true {
    text-decoration: line-through;
    color: gray;
}

htmlはTwitter Bootstrapを使うようにする点以外は、サンプルと同じです。

<!DOCTYPE html>

<html lang="ja" ng-app>
<head>
    <meta charset="utf-8" />
    <title>Todo sample app</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <link href="Content/bootstrap.min.css" rel="stylesheet" />
    <script src="Scripts/jquery-1.9.0.min.js"></script>
    <script src="Scripts/bootstrap.min.js"></script>
    <script src="Scripts/angular.min.js"></script>
    <script src="app.js"></script>
</head>
<body>
    <div class="jumbotron">
        <h1>Todo</h1>
    </div>
    <div class="container" ng-controller="TodoCtrl">
        <div class="row">
            <div class="col-md-1">
                <span>残り:{{remaining()}}/{{todos.length}}</span>
            </div>
            <div class="col-md-1">
                [<a href="" ng-click="archive()">完了</a>]
            </div>
        </div>
        <div class="row">
            <div class="col-md-12">
                <ul>
                    <li ng-repeat="todo in todos">
                        <input type="checkbox" ng-model="todo.done" />
                        <span class="done-{{todo.done}}">{{todo.text}}</span>
                    </li>
                </ul>
            </div>
        </div>
        <div class="row">
            <div class="col-md-12">
                <form ng-submit="addTodo()">
                    <input type="text" ng-model="todoText" size="30" placeholder="新しいTODOを追加" />
                    <input class="btn" type="submit" value="追加" />
                </form>
            </div>
        </div>
    </div>
</body>
</html>

{{ }}を使ってバインドしたり、ng-***で色々やるみたいですが、Visual Studioでも{{ }}の中までは補完してくれないので、ちょっとストレス…。

実行結果

いい感じのTodoになりました。

f:id:okazuki:20140525010627j:plain

これだけで、コレクションのバインドとかできるのは強力ですね。

f:id:okazuki:20140525010848j:plain

まとめ

自分で色々やるのに比べたら、こいつを使うのがいいのかなあと思ったり思わなかったり。でもとっかかりは、いい感じなので、もう少しやってみようと思います。