Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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

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

task-list.png

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

コード

HTML 部分

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

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 を使う方が楽なんだもん。

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;

}();

テストデータ

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

参考文献

すぐできる AngularJS - チュートリアル - ToDo アプリをつくろう

LightSpeedC
TypeScript, JavaScript, Node.js, Java, Rust, Go, C# 等を勉強中。 React (Mithril), socket.io, D3, Leaflet 等で何か作りたいな。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away