リズムのじかん

javascript、typescriptなど中心に書きます。

【javascript】フレームワークを使わずにMVC

javascriptで、フレームワークを使わずにMVCしているサンプルがなかったので、作ってみました。

あくまで参考です。(業務ではちゃんとフレームワークを使ったほうが良いと思います。)

作ったもの

よくあるTodoアプリです。以下で試せます。

http://jsfiddle.net/54sjqL6z/

機能は以下だけです。

  • Enterキー押下でTodo追加
  • 各Todoをクリックして完了にする

コード

index.html

フレームワークは使わないけれど、underscorejsは使います。

あとjQueryも使いません。(最近できるだけ使わないようにしている。これ本当にjQuery必要なの?ってことが多いため。)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Todo MVC Sample</title>
  <style>
  .completed {
    text-decoration:line-through;
  }
  </style>
</head>
<body>
  <div>
    <input type="text" id="todoInput">
  </div>
  <div>
    <ul id="todoList"></ul>
  </div>
  <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore.js"></script>
  <script src="./util.js"></script>
  <script src="./model.js"></script>
  <script src="./view.js"></script>
  <script src="./controller.js"></script>
</body>
</html>

util.js

ユーティリティです。オブザーバパターン用のeventsオブジェクトを返します。

(function(_, exports) {
  'use strict';

  exports.events = function() {
    var events = {};
    return {
      on: function(name, func) {
        (events[name] || (events[name] = []))
          .push(func);
        return this;
      },
      trigger: function(name /*, ...args */ ) {
        var funcs = events[name] || [];
        var args = _.rest(arguments);
        funcs.forEach(function(func) {
          setTimeout(function() {
            func.apply(null, args);
          }, 0);
        });
        return this;
      }
    };
  };

})(this._, this.util = this.util || {});

model.js

モデルです。ここではTodoモデルとTodoListモデルを定義します。

(function(_, util, exports) {
  'use strict';

  // Model
  var Todo = _.extend(function(id, description) {
    this.id = id;
    this.description = description;
    this.complete = false;
  }, util.events());

  Todo.prototype.done = function() {
    this.complete = true;
    Todo.trigger('change', this); // 変更を通知
  };

  // Collection
  var TodoList = _.extend([], util.events());

  TodoList.add = function(todo) {
    this.push(todo);
    this.trigger('change', TodoList); // 変更を通知
  };

  // export
  exports.Todo = Todo;
  exports.TodoList = TodoList;

})(this._, this.util, this.todo = this.todo || {});

view.js

ビューです。何か長いですが、、、DOMの更新とコントローラへの通知ロジックを実装しているだけです。

(function(_, util, exports) {
  'use strict';

  // モデルの参照
  var Todo = exports.Todo;
  var TodoList = exports.TodoList;

  // DOM参照
  var input = window.document.getElementById('todoInput');
  var ul = window.document.getElementById('todoList');

  var View = _.extend({}, util.events());
  var todoHtml = _.template('<li id="todo_<%= id %>" <% if(complete) { %>class="completed" <% }%>><%= description %></li>');

  // DOM操作 TodoListを描画
  function render(todoList) {
    ul.innerHTML = todoList.reduce(function(html, todo) {
      return html + todoHtml(todo);
    }, '');
  }

  // DOM操作 Todoの状態を更新
  function complete(todo) {
    var target = window.document.getElementById('todo_' + todo.id);
    target.className = todo.complete ? 'completed' : '';
  }

  // DOM操作 入力をクリア
  function clearInput() {
    input.value = '';
  }
  View.clearInput = clearInput; // export

  // モデルを監視
  Todo.on('change', complete);
  TodoList.on('change', render);

  // Event通知
  input.addEventListener('keydown', function(e) {
    if (e.keyCode === 13 && e.target.value) View.trigger('add', e.target.value); // inputでENTERが押下された場合に通知
  });
  ul.addEventListener('click', function(e) {
    if (e.target.tagName.toLowerCase() === 'li') {
      View.trigger('change', _.last(e.target.id.split('_'))); // 各アイテムがクリックされた時に通知
    }
  });

  // export
  exports.View = View;

})(this._, this.util, this.todo);

controller.js

最後はコントローラです。viewの通知を受け取って、modelを更新したりviewを変更したりします。

(function(todo) {
  'use strict';

  // viewの追加イベントを監視。TodoListに新規Todoを追加する。
  todo.View.on('add', function(description) {
    todo.TodoList.add(new todo.Todo(todo.TodoList.length, description));
    todo.View.clearInput();
  });

  // viewの変更イベントを監視。モデルの状態を更新を命令。
  todo.View.on('change', function(index) {
    todo.TodoList[index].done();
  });

})(this.todo);

おまけ

上のコードをgistに置いてます。

https://gist.github.com/masahirompp/0fc24cd168651bdcc2f4