Mithril.js 試してみた(4) todo アプリのフロント側まで


todoアプリをとりあえずフロント側だけ完成させてみた。

画面例は以下の様な感じです。

![task-list.png](https://qiita-image-store.s3.amazonaws.com/0/16128/e02711b3-31f6-dae9-3d50-aa3547930b6c.png)

>前回までの記事
[Mithril.js 試してみた(1) todo アプリを作り始める所まで - Qiita]
(http://qiita.com/LightSpeedC/items/a2c967928f9cc13e0ebc)
[Mithril.js 試してみた(2) サーバーからデータを取得する m.request() - Qiita]
(http://qiita.com/LightSpeedC/items/f533f048e01ac19a06ec)
[Mithril.js 試してみた(3) console.logの様に画面に表示してみる - Qiita]
(http://qiita.com/LightSpeedC/items/23dcecfa22b89dc7c165)
Mithril.js 試してみた(4) todo アプリのフロント側まで - Qiita ⇒ この記事
[Mithril.js 試してみた(5) Excelの様な表計算アプリを3時間で作る m.component() - Qiita]
(http://qiita.com/LightSpeedC/items/c19677822f896adc43d9)

# コード

## HTML 部分

この ToDoアプリのコンポーネントは、データを直接リストで渡すこともできます。
URLを指定すればサーバーから取得できます。

```html:todo-app.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>ToDo App - Mithril.js</title>

<script src="mithril.min.js"></script>
<!--[if IE]><script src="es5-shim.min.js"></script><![endif]-->

<body>
<table>
<tr valign="top">
<td><div id="$todoElement1"></div></td>
<td><div id="$todoElement2"></div></td>
<td><div id="$todoElement3"></div></td>
</tr>
</table>
<hr>
<a href="/">home</a>
</body>

<script src="todo-component.js"></script>

<script>

//アプリケーションの初期化
//HTML要素にコンポーネントをマウント
m.mount($todoElement1, m.component(taskListComponent, {url:'todo-list.json'}));

m.mount($todoElement2, m.component(taskListComponent, {list:[
	{done:false, title:'最初のタスク'},
	{done:false, title:'2番目のタスク'},
	{done:false, title:'3番目のタスク'},
	{done:true, title:'完了しているタスク'}
]}));

m.mount($todoElement3, taskListComponent);

// 参考: AngularJS チュートリアル - ToDoアプリをつくろう
// http://8th713.github.io/LearnAngularJS/#/

</script>
```

## JavaScript部分

ModelであるTaskクラスは、ロード時に保存されていたJSONからも作成できるし、保存するためにJSON化することもできる様にした。`toJSON()` メソッドが肝だ。`m.prop()` もそうなっていたので、真似した。
こうすることで表示する時だけに必要な `key` 属性などを保存する必要は無くなった。

チュートリアルなので今は1つのコンポーネントで作成してあるが、次は分解する予定。

また、あえてCSSやMSXを使用していない。なのでタスクランナーなども今のところ必要はない。jQueryはもちろん不要だ。

今回のアプリではサーバー側はWebサーバーだけで良い。
データを保存するにはローカルストレージを使うか、サーバー側を開発する必要がある。
とりあえずデータを一括で保存するには `JSON.stringify(taskList)` をサーバーに投げる様にすると良いと思う。`toJSON()` は今後活用したい。

Text入力時のEnterキーに対応したが、IE8では思った様に動かなかったので、DOM要素を直接見る事にした。本当に勘弁して欲しい。とりあえず今回もIE8に対応したが、今後はサポートしなくて良いと思う。

いろいろ試した結果、`controller` は closure の `view` 関数を作るだけという方式にしてみた。私は使い方を間違っているかも。もしかするとテストしにくくなってると思う。closure を使わずコントローラのプロパティだけで動作する view の方がテストしやすいだろう。view をテストするのに controller オブジェクトを切り替えるだけでテストできるからね。でも、早く書くには closure を使う方が楽なんだもん。

```js:todo-component.js
// コンポーネント定義
this.taskListComponent = function () {
	'use strict';

	// モデル: Taskクラスは2つのプロパティ(title : string, done : boolean)を持つ
	function Task(task) {
		this.title = m.prop(task && task.title || '');
		this.done  = m.prop(task && task.done  || false);
		this.key = (new Date - 0) + Math.random();
	}
	Task.prototype.toJSON = function () {
		return {title: this.title, done: this.done};
	};

	// コントローラ・オブジェクトctrlは
	// 表示されているTaskのリスト(list)を管理し、
	// 作成が完了する前のTaskのタイトル(title)を格納したり、
	// Taskを作成して追加(add)が可能かどうかを判定し、
	// Taskが追加された後にテキスト入力をクリアする

	// このアプリケーションは、taskListComponentコンポーネントでコントローラとビューを定義する
	var taskListComponent = {

		// コントローラは、モデルの中のどの部分が、現在のページと関連するのかを定義している
		// この場合は1つのコントローラ・オブジェクトctrlですべてを取り仕切っている
		controller: function (args) {
			// アクティブなTaskのリスト
			var taskList = [];

			// Taskをタスクリストに追加
			function addTask(task) {
				taskList.push(new Task(task));
			}

			// データのリストからタスクリストに追加
			function importList(list) {
				list.forEach(addTask);
			}

			// 引数に渡されたtaskデータリストlistからTaskのリストに追加
			if (args && args.list)
				importList(args.list);

			// 引数に渡されたtaskデータリストurlからTaskのリストに追加
			if (args && args.url)
				m.request({method: 'GET', url:args.url})
					.then(importList);

			// 新しいTaskを作成する前の、入力中のTaskの名前を保持するスロット
			var titleInput = m.prop(''), titleInputElem;

			// Taskをリストに登録し、ユーザが使いやすいようにtitleフィールドをクリアする
			function addTitleInput() {
				var val = titleInput() || titleInputElem.value;
				if (val) {
					addTask({title: val});
					titleInput('');
				}
				return false;
			}

			// Enterキーに対応
			function configTitleInput(elem, isInit) {
				if (isInit) return;
				elem.focus();
				titleInputElem = elem;
			}

			// タスクが完了している?
			function taskIsDone(task) {
				return task.done();
			}

			// タスクが完了していない?
			function taskIsNotDone(task) {
				return !task.done();
			}

			// 完了タスクの数
			function countDone() {
				return taskList.filter(taskIsDone).length;
			}

			// 未了タスクの数
			function countUndone() {
				return taskList.filter(taskIsNotDone).length;
			}

			// 完了タスクを全て削除(未了タスクを残す)
			function removeAllDone() {
				taskList = taskList.filter(taskIsNotDone);
			}

			// 全て完了/未了
			function checkAll() {
				var state = countUndone() !== 0;
				taskList.forEach(function (task) { task.done(state); });
			}

			// 削除
			function removeTask(todoToRemove) {
				taskList = taskList.filter(function (task) {
					return task !== todoToRemove;
				});
			}

			// 表示モード
			var dispMode = 0; //0:ALL, 1:DONE, 2:UNDONE

			// 編集モード
			var taskToEdit = null;  // 編集中のTask
			var taskToEditElem;     // 編集中のTaskのDOM要素 (ie8対応)
			var saveTitleToEdit;    // 編集開始時のタイトル
			// タスク編集モードをトグル
			function toggleEditTask(task) {
				if (taskToEdit) {
					var val = taskToEditElem.value;
					if (val) taskToEdit.title(val);
					else taskToEdit.title(saveTitleToEdit);
					taskToEdit = null;
				}
				else {
					taskToEdit = task;
					if (task) saveTitleToEdit = task.title();
				}
				return false;
			}
			// 編集するinputを構成
			function configEditTask(elem, isInit) {
				if (isInit) return;
				elem.focus();
				taskToEditElem = elem;
			}

			// DEBUGフラグ
			var debugFlag = m.prop(false);

			this.view = function view(ctrl) {
				return [
					m('h1', 'Task管理アプリ'),
					m('div', ['タスク: ',
						m('form', {onsubmit: addTitleInput},
							m('input', m_on('change', 'value', titleInput,
								{placeholder: '新しいタスクを入力',
								 config: configTitleInput, autofocus: true})),
							m('button[type=submit]', {onclick: addTitleInput}, '追加'))
					]),
					m('hr'),
					m('div', ['表示: ',
						m('button[type=button]', {onclick: function () { dispMode = 0; }},
							[(dispMode === 0 ? '★': '') + '全て', m('span', taskList.length)]),
						m('button[type=button]', {onclick: function () { dispMode = 2; }},
							[(dispMode === 2 ? '★': '') + '未了', m('span', countUndone())]),
						m('button[type=button]', {onclick: function () { dispMode = 1; }},
							[(dispMode === 1 ? '★': '') + '完了', m('span', countDone())])
					]),
					m('div', ['操作: ',
						m('button[type=button]', {onclick: checkAll}, '全て完了/未了'),
						m('button[type=button]', {onclick: removeAllDone}, '完了タスクを全て削除')
					]),
					m('hr'),
					m('table', [taskList.map(function(task) {
						var attrs = {key: task.key};
						if (dispMode === 1 && !task.done() ||
							dispMode === 2 &&  task.done())
								attrs.style = {display: 'none'};
						return m('tr', attrs,
							task === taskToEdit ? [
								//編集
								m('td', {colspan:2}),
								m('td', {},
									m('form', {onsubmit: toggleEditTask.bind(null, null)},
										m('input', m_on('change', 'value', task.title,
											{onblur: toggleEditTask.bind(null, null),
											 config: configEditTask})))
								)] : [
								//表示
								m('td', [
									m('button[type=reset]', {onclick: removeTask.bind(null, task)}, '削除')
								]),
								m('td', [
									m('input[type=checkbox]', m_on('click', 'checked', task.done))
								]),
								m('td',
									{style: {textDecoration: task.done() ? 'line-through' : 'none'},
									 ondblclick: toggleEditTask.bind(null, task)},
									task.title())]
						);
					})]),
					m('div', '※ダブルクリックで編集'),
					//DEBUG表示
					m('input[type=checkbox]', m_on('click', 'checked', debugFlag)), 'dbg',
					!debugFlag() ? [] :
					m('pre', {style:{color:'green', backgroundColor:'lightgray'}},
						JSON.stringify(taskList, null, '  ').split('\n').map(function (x) {
							return m('div', x);
					}))
				];
			};

		},

		// ビュー
		view: function (ctrl) {
			return ctrl.view(ctrl);
		}
	};

	// HTML要素のイベントと値にプロパティを接続するユーティリティ
	function m_on(eventName, propName, propFunc, attrs) {
		attrs = attrs || {};
		attrs['on' + eventName] = m.withAttr(propName, propFunc);
		attrs[propName] = propFunc();
		return attrs;
	}

	return taskListComponent;

}();
```

## テストデータ

```json:todo-list.json
[
	{"title": "最初のタスク"},
	{"title": "2番目のタスク"},
	{"title": "3番目のタスク"},
	{"done": true,  "title": "完了しているタスク"}
]
```


# 参考文献

[すぐできる AngularJS - チュートリアル - ToDo アプリをつくろう]
(http://8th713.github.io/LearnAngularJS/)