かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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

まとめ

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