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

Mithril.js 試してみた(1) todo アプリを作り始める所まで

More than 3 years have passed since last update.

Mithril.js を試してみた

  • jQuery はなんだか最初は簡単だなぁ、と思って使ってみたが、やっている内に、泥臭いし、たくさんコードを書かないといけないし、DOMに追加したり、削除したり、順番を変えたり、保持しているデータとDOMを同期させる仕事をやるだけで疲れてきて、嫌いになった。

  • Angular.js (Google) も試してみて、最初は感動したけど、なんかいろいろと難しくて挫折した。

  • React.js (Facebook) は、絶対これが本命だと思って、すごく期待して、本まで買って試していたが、ちょっと覚える事が多くて、どうしようかと迷っていた。

  • Riot.js もちょっと試そうかと思ったけど、やっぱり覚える事が多くて、大変そうに思えた。

  • Mithril.js は、なんか思った通りに書ける気がする。覚える事が少ない。軽い。速い。
    JavaScriptオンリーでHTMLもCSSも書ける感じだ。

現時点で Virtual DOM を使うフレームワークは正解だと思う。
jQuery は初心者の勉強のためにはいいけど、僕の中では使っちゃダメなものとして位置付けられている。

追加記事を書いた。
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

いつの間にか日本語のサイトが...

知らない間に http://mithril.js.org/ の日本語訳サイトが作られていた。
スゴイ。すばらしい。ありがたい。

http://mithril-ja.js.org/ (http://shibukawa.github.io/mithril-ja/)

本も出てるじゃん
Mithril――最速クライアントサイドMVC
http://www.oreilly.co.jp/books/9784873117447/
サンプル https://github.com/oreilly-japan/mithril-book-sample

とりあえず最小限のAPIだけでViewだけのサンプルアプリを作ってみた

ダウンロードした mithril.min.js を使用する。
以下のサンプルはm.mount()m.draw()しか使用していない。

counter-min.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>counter minimum - Mithril.js</title>

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

<body></body>

<script>

//カウンター
var counter = 0;

//タイマーでカウントアップ
setInterval(function () {
    counter++;
    m.redraw(true);
}, 1000);

//ビュー
function view() {
    return 'count: ' + counter;
}

//HTML要素にコンポーネントをマウント
m.mount(document.body, {view: view});

</script>

ルール無視のサンプルだ。真似してはいけない。
Viewから外の変数を見ているし、外から再描画の指示があるし、
例としてはあまり良くないかな。

IE8でも動く様に es5-shim.min.js を以下から入手した。
es5-shim - github

ControllerとViewだけのアプリ

ひどい例だったので、もうちょっと改良してみる。

counter-prop.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>counter prop - Mithril.js</title>

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

<body>
<div id="$counterElement"></div>
</body>

<script>

//コンポーネント定義
var counterComponent = {
    //コントローラ
    controller: function () {
        var ctrl = this;

        //カウンター
        ctrl.counter = m.prop(0);

        //タイマーでカウントアップ
        setInterval(function () {
            ctrl.counter(ctrl.counter() + 1);
            m.redraw(true);
        }, 1000);
    },
    //ビュー
    view: function (ctrl) {
        return m('div', 'count: ' + ctrl.counter());
    }
};

//HTML要素にコンポーネントをマウント
m.mount($counterElement, counterComponent);

</script>

ちょっと変えてみた。
ControllerとViewだけのアプリだ。
m.prop()も試しに使ってみた。
この例ではm.prop()もあまり役に立つように見えないがm.withAttr()と一緒に使うといいよ。

TIPS: ちなみに
document.getElementById('element')とかjQueryの$('#element')というのは
シンプルにelementと省略できるよ。知ってた?
本当はwindow.elementwindow['element']という事なんだ。
だからJavaScriptから扱いたいHTMLエレメントのidにはハイフンとか使わないで
JavaScriptの変数名の規約に合わせておくと扱いやすいよ。
<div id="$elementName">の様にidを$で始めると$elementNameという変数で扱えるようになる。jQuery使ってるみたいでいいよね。
※古いブラウザで使うにはbodyタグが閉じていないといけないけどね。
※グローバル変数がたくさんできるので注意が必要だけどね。

controller関数

controller関数は、コンストラクタとして呼ばれている。
だから return {counter: m.prop(0)}; の様にオブジェクトを作って返してもいいみたいなんだけど、どちらかと言うとthisのプロパティに追加するパターンを使う方がいいかなぁ、と思う。
どちらでもいいか。好みの問題か。

view関数とm()

view関数は、文字列でも、数字でも、配列を返しても、m()の結果のオブジェクトを返してもいいらしい。
めちゃめちゃ柔軟性があるなぁ。m()の引数の後半も同様に柔軟性がある。

view関数の返り値の例
return 'count: ' + counter;                       // 文字列
return [m('div', 'count: '), m('div', counter)];  // m('div')を2つ持つ配列
return m('div', 'count: ' + counter);             // m()のコンテンツは文字列
return m('div', 'count: ', counter);              // m()のコンテンツは文字列と数字
return m('div', ['count: ', counter]);            // m()のコンテンツは配列
return m('div', [['count: '], [[[counter]]]]);    // m()のコンテンツは複雑な配列

viewで2つのHTMLエレメントを返すためにdivで囲む必要もない。配列で十分だ。
※文字列を配列で返す場合はDOMとしてテキストノード要素がわかれているみたい。

サンプルのToDoアプリをちょっと改良してみた

例題になっているToDoアプリを改良して、テキスト入力時にEnterキーを受け付けるようにしてみた。

todo-app.html
<!DOCTYPE html>
<html>
<head>
<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]-->
</head>

<body>
<div id="$todoElement"></div>
</body>

<script>

//コンポーネント定義
var todoComponent = function () {
    'use strict';

    // モデル: Todoクラスは2つのプロパティ(title : string, done : boolean)を持つ
    function Todo(data) {
        this.title = m.prop(data.title);
        this.done  = m.prop(false);
    };

    // モデル: TodoListクラスはTodoオブジェクトの配列
    var TodoList = Array;

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

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

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

            // アクティブなToDoのリスト
            ctrl.list = new TodoList();

            // 引数に渡されたtodoタイトルリストからToDoのリストに追加
            if (listArg) {
                listArg.forEach(function (title) {
                    ctrl.list.push(new Todo({title: title}));
                });
            }

            // 新しいToDoを作成する前の、入力中のToDoの名前を保持するスロット
            ctrl.title = m.prop('');

            // ToDoをリストに登録し、ユーザが使いやすいようにtitleフィールドをクリアする
            ctrl.add = function () {
                var val = ctrl.title() ||
                    document.getElementById('$title').value; // for ie8
                if (val) {
                    ctrl.list.push(new Todo({title: val}));
                    ctrl.title('');
                }
                return false;
            };

        },

        // ビュー
        view: function (ctrl) {
            return [
                m('form', {onsubmit: ctrl.add},
                    m('input', m_on('change', 'value', ctrl.title, {id: '$title'})),
                    m('button[type=submit]', {onclick: ctrl.add}, 'Add')),
                m('table', [ctrl.list.map(function(todo) {
                    return m('tr', [
                        m('td', [
                            m('input[type=checkbox]', m_on('click', 'checked', todo.done))
                        ]),
                        m('td', {style: {textDecoration: todo.done() ? 'line-through' : 'none'}}, todo.title())
                    ]);
                })])
            ];
        }
    };

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

    return todoComponent;

}();

// アプリケーションの初期化
// HTML要素にコンポーネントをマウント
m.mount($todoElement, m.component(todoComponent, ['first task', 'second task']));
// m.mount($todoElement, todoComponent);

</script>
</html>

controller関数に引数を追加して、m.component()を使ってみた。
m.component()を複数の場所で使える部品にするには、コツが必要な気がする。

P.S.
Chrome, Firefox, ie8, ie11, Edge で動作確認しました。
React.js の JSX の様な MSX というのもあるが、そちらが好きな人はそちらをどうぞ。

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