AngularJS で ToDo アプリのサンプルを書いてみた

Google 製の JavaScript MVC フレームワークAngularJS』で、簡単な ToDo アプリのサンプルを書いてみた。

<!DOCTYPE html>
<html ng-app>
  <head>
    <meta charset="utf-8">
    <title>AngularTodo</title>
    <script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
    <script>
      // モデル
      function Todo(text, done) {
        this.id = Todo.getNextId();
        this.text = text;
        this.done = done || false;
        this.editing = false;
      }
      // 連番の ID を作成
      Todo.getNextId = (function() {
        var _nextId = 0;
        var _getNextId = function() {
          _nextId++;
          return _nextId;
        };
        return _getNextId;
      })();

      // コントローラー
      function TodoCtrl($scope) {
        $scope.todos = [
          new Todo("foo", true),
          new Todo("bar")
        ];

        // 未完了の ToDo を数える
        $scope.remaining = function() {
          var count = 0;
          angular.forEach($scope.todos, function(todo) {
            count += todo.done ? 0 : 1;
          });
          return count;
        };

        // 完了した ToDo を削除
        $scope.archive = function() {
          var oldTodos = $scope.todos;
          $scope.todos = [];
          angular.forEach(oldTodos, function(todo) {
            if (!todo.done) {
              $scope.todos.push(todo);
            }
          });
        };

        // ToDo 追加
        $scope.addTodo = function() {
          $scope.todos.push(new Todo($scope.todoText));
          $scope.todoText = "";
        };

        // ToDo の編集開始
        $scope.editTodo = function(id) {
          var todo = findTodoById(id);
          todo.editing = true;
        };

        // ToDo の編集終了
        $scope.updateTodo = function(id) {
          var todo = findTodoById(id);
          todo.editing = false;
        };

        // ToDo を削除
        $scope.deleteTodo = function(id) {
          var index = findTodoIndexById(id);
          $scope.todos.splice(index, 1);
        };

        // Todo を ID で検索してインデックスを取得
        function findTodoIndexById(id) {
          var numId = Number(id);
          for (var i = 0, max = $scope.todos.length; i < max; i++) {
            var todo = $scope.todos[i];
            if (todo.id === numId) {
              return i;
            }
          }
          return null;
        }

        // Todo を ID で検索
        function findTodoById(id) {
          var index = findTodoIndexById(id);
          return $scope.todos[index];
        }
      }
    </script>
    <style>
      .done-true {
        text-decoration: line-through;
        color: grey;
      }
    </style>
  </head>
  <body>
    <h2>Angular Todo</h2>
    <div ng-controller="TodoCtrl">
      <span>{{remaining()}} of {{todos.length}} remaining</span>
      [<a href="" ng-click="archive()">archive</a>]
      <ul class="unstyled">
        <li ng-repeat="todo in todos">
          <!--通常表示するビュー-->
          <div ng-show="!todo.editing">
            <input type="checkbox" ng-model="todo.done">
            <span class="done-{{todo.done}}">{{todo.text}}</span>
            [<a href="" ng-click="editTodo(todo.id)">edit</a>]
            [<a href="" ng-click="deleteTodo(todo.id)">delete</a>]
          </div>
          <!--編集時に表示するビュー-->
          <div ng-show="todo.editing">
            <input type="text" ng-model="todo.text">
            <input type="button" ng-click="updateTodo(todo.id)" value="Finish">
          </div>
        </li>
      </ul>

      <!--name を指定するとテンプレート内でアクセスできる-->
      <form name="form" ng-submit="addTodo()">
        <input type="text" ng-model="todoText" size="30"
               placeholder="add new todo here" required>

        <!--todoText が空のときはボタンを押せないようにする-->
        <input class="btn-primary" type="submit" value="add"
               ng-disabled="form.$invalid">
      </form>
    </div>
  </body>
</html>

ToDo MVC と差別化するために、ToDo はその場で編集できるようにしている。編集画面の切り替えを、バインドしているモデルのプロパティ変更で行っているのは、AngularJS 力が足りなかったための苦肉の策。もっとキレイに実装できると思う。

今回のサンプルで使っている機能は、テンプレートとデータバインディング、あとフォームのバリデーション。AngularJS には他にもルーティングや国際化やコンポーネントなど、かなりの機能があるみたいなので、それらは追々試してみる。

作ってみての感想は、HTML テンプレート+データバインディングはやはり楽チン。Backbone.js だとモデルが変更されたらビューで DOM 操作して表示を変更していたけど、その部分を AngularJS が自動でやってくれるおかげでサンプルがサクッっと記述できた。

Knockout.js でもデータバインディングと HTML テンプレートは使えたけど、AngularJS はさらに多機能。正直、現時点では AngularJS ではなく Knockout.js を選択する理由が見当たらないな。

今は Backbone.js をメインで使っていて、Backbone.js のコンパクトさや Underscore.js の便利さをスゴク気に入っているんだけど、AngularJS に心揺れ動いている自分がいる。