Mithril 1.1.0

m(selector, attributes, children)


説明

Mithrilのビュー内でHTML要素を表現するのに使います。

m("div", {class: "foo"}, "hello")
// <div class="foo">hello</div>と表現しています

Babelプラグインを使うことで、HTML文法でも表現できます。

/** jsx m */
<div class="foo">hello</div>

シグニチャ

vnode = m(selector, attributes, children)

引数 必須 説明
selector String|Object Yes CSSセレクター、もしくはコンポーネント
attributes Object No HTML属性、もしくは要素のプロパティ
children Array<Vnode>|String|Number|Boolean No 子供のvnodeフラットな引数として書くこともできます。
返り値 Vnode vnode

シグニチャの読み方


どのように動作するのか

Mithrilはhyperscript関数m()を提供しています。これを使うと、あらゆるHTML構造をJavaScriptの文法を使って表現できます。この関数はselector文字列(必須)、attributesオブジェクト(オプション)、children配列(オプション)を受け取ることができます。

m("div", {id: "box"}, "hello")

// 同等のHTML:
// <div id="box">hello</div>

m()関数は実際のDOM要素を返すわけではありません。その代わりに仮想DOMノードあるいはvnodeと呼ばれる、作成すべきDOMエレメントを表現するJavaScriptオブジェクトを返します。

// vnode
var vnode = {tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]}

m.render()関数を使うと、vnodeを実際のDOMに変換することができます。

m.render(document.body, m("br")) // <body>タグの中に<br>タグを追加する

m.render()を何度か呼ぶと、DOMツリーがすべて破棄されて毎回作られるわけではありません。その代わりに、呼び出しごとに関数に渡された仮想DOMツリーを反映させるのに必要な時にだけ、DOMツリーが変更されます。DOMの再作成は極めてコストが高い操作であるため、このような設計になっています。しかし、これによって入力フォーカスが失われるなど、他の問題も発生する可能性があります。これと比較すると、必要なところだけ更新をかける方法の方がはるかに高速で、数多くのユーザ機能を満たす必要があるような複雑なUIを管理するのも簡単になります。


柔軟性

m()関数はポリモーフィック(型によって動作が変わる)とともに引数の数も可変です。言い換えると、受け取れる入力パラメータが非常に柔軟であるということです。

// シンプルなタグ
m("div") // <div></div>

// オプションの属性と子ノード
m("a", {id: "b"}) // <a id="b"></a>
m("span", "hello") // <span>hello</span>

// 子ノードを持つタグ
m("ul", [             // <ul>
    m("li", "hello"), //   <li>hello</li>
    m("li", "world"), //   <li>world</li>
])                    // </ul>

// 配列はオプション
m("ul",               // <ul>
    m("li", "hello"), //   <li>hello</li>
    m("li", "world")  //   <li>world</li>
)                     // </ul>

CSSセレクター

m()の最初の引数にはHTMLエレメントを説明するためのCSSセレクターを指定できます。#(ID)、.(クラス)、[](属性)などを組み合わせた、CSSとしてルールに従った表記を解釈します。

m("div#hello")
// <div id="hello"></div>

m("section.container")
// <section class="container"></section>

m("input[type=text][placeholder=Name]")
// <input type="text" placeholder="Name" />

m("a#exit.external[href='http://example.com']", "Leave")
// <a id="exit" class="external" href="http://example.com">Leave</a>

タグ名を省略すると、Mithrilはdivタグとみなします。

m(".box.box-bordered") // <div class="box box-bordered"></div>

Mithrilでは、状態によって変わらない、静的な属性ではCSSセレクターを使うことが推奨されます。状態によって変わるような属性はattributesオブジェクトで設定します。

var currentURL = "/"

m("a.link[href=/]", {
    class: currentURL === "/" ?"selected" : ""
}, "ホーム")

// 次のHTMLと同等:
// <a href="/" class="link selected">ホーム</a>

m()の第一引数と第二引数の両方にクラス名があった時は、両方のクラス名のリストがマージされ、期待どおりに動作します。


DOM属性

Mithrilでは属性の解決にJavaScriptのAPIとDOMのAPI(setAttribute)の両方が使えます。これは、属性の参照のために両方の文法が使えることを意味します。

例えば、readonly属性は、JavaScriptのAPIでは大文字混じりのelement.readOnlyとなります。Mithrilでは下記の全ての形式をサポートしています:

m("input", {readonly: true}) // 小文字
m("input", {readOnly: true}) // 大文字
m("input[readonly]")
m("input[readOnly]")

スタイル属性

Mithrilはスタイルの設定として、文字列とオブジェクトの両方をサポートしています。別の言い方をすると、次の表記法はすべて動作します。

m("div", {style: "background:red;"})
m("div", {style: {background: "red"}})
m("div[style=background:red]")

文字列形式のstyleはCSSルールの値の変更時だけではなく、再描画時にエレメントのすべてのインラインのスタイルを上書きします。.

Mithrilは数値に単位は付与しません。


イベント

Mithrilは、touchstartのように、on${event}という形式ではないものも含めて、すべてのDOMイベントへのイベントハンドラーのバインディングをサポートしています。

function doSomething(e) {
    console.log(e)
}

m("div", {onclick: doSomething})

プロパティ

Mithrilは<select>selectedIndexvalue属税のように、プロパティでアクセスできる属性にアクセスできるDOMの機能をサポートしています。.

m("select", {selectedIndex: 0}, [
    m("option", "オプションA"),
    m("option", "オプションB"),
])

コンポーネント

コンポーネントを使うとロジックを1つの塊にカプセル化し、1つのエレメントのように使えるようになります。コンポーネントは、巨大でスケーラブルなアプリケーションを作るための基本となります。

viewメソッドを持つJavaScriptのオブジェクトであれば何でも、コンポーネントとなることができます。コンポーネントを利用する時は、CSSセレクター文字列の代わりにm()の最初の引数にコンポーネントを渡します。次のサンプルのように、属性や子要素を定義することで、コンポーネントに引数を渡すことができます。

// コンポーネントの定義
var Greeter = {
    view: function(vnode) {
        return m("div", vnode.attrs, ["Hello ", vnode.children])
    }
}

// コンポーネントを利用する
m(Greeter, {style: "color:red;"}, "world")

// 等価なHTML
// <div style="color:red;">Hello world</div>

コンポーネントについて詳しく知りたい方は、コンポーネントだけを扱った章をご覧ください。.


ライフサイクルメソッド

vnodeとコンポーネントは(フックとも呼ばれる)ライフサイクルメソッドを持つことができます。 これらのメソッドは、DOM要素のさまざまなタイミングで呼び出されます。Mithrilでサポートされているライフサイクルメソッドには、oninit, oncreate, onupdate, onbeforeremove, onremove, and onbeforeupdateがあります。

ライフサイクルメソッドはDOMイベントハンドラーと同じように定義しますが、メソッドの引数はイベントオブジェクトではなく、vnodeです。

function initialize(vnode) {
    console.log(vnode)
}

m("div", {oninit: initialize})
フックHook 説明
oninit(vnode) vnodeが実際のDOMエレメントとしてレンダリングされる前に呼び出されます。
oncreate(vnode) vnodeがDOMに追加されたあとに実行されます。
onupdate(vnode) DOMエレメントがドキュメントに追加されている間中、再描画発生するたびに呼び出されます。
onbeforeremove(vnode) DOMエレメントがドキュメントから削除される前に呼び出されます。もしPromiseが返されると、MithrilはそのPromiseが完了されたあとに取り除きます。このDOM要素は親要素から取り除かれるDOM要素に対してのみ呼び出されます。その子要素に対しては呼び出されません。
onremove(vnode) DOMエレメントがドキュメントから削除される前に呼び出されます。もしonbeforeremoveフックが定義されていると、onremovedoneが呼ばれた後に呼び出されます。このDOM要素は親要素から取り除かれるDOM要素と、そのすべての子要素に対して呼び出されます。
onbeforeupdate(vnode, old) onupdateが実行される前に呼び出されます。もしこのメソッドがfalseを返すと、その対象要素と子要素の差分計算は行われません。

ライフサイクルメソッドについて詳しく知りたい方は、ライフサイクルメソッドのページを御覧ください。.


キー

リスト内のvnodeはkeyと呼ばれる特別な属性を持つことができます。この属性はvnodeと、それを生成するのに使用したモデルデータを識別するのに使えます。

keyはデータのリストの中でオブジェクトを一意に識別するユニークな識別子でなければなりません。

var users = [
    {id: 1, name: "ジョン"},
    {id: 2, name: "メアリー"},
]

function userInputs(users) {
    return users.map(function(u) {
        return m("input", {key: u.id}, u.name)
    })
}

m.render(document.body, userInputs(users))

キーを持つと、users配列がシャッフルされてビューが再描画されても、inputタグも同じ順序で正しくシャッフルされ、フォーカスとDOMの状態が維持されます。

キーについてより詳しく知りたい方は、キーのページを参照してください。


SVGとMathML

MithrilはSVGを完全にサポートしています。Xlinkもサポートしていますが、v1.0以前のMithrilとは異なり、名前空間を明示的に定義しなければなりません:

m("svg", [
    m("image[xlink:href='image.gif']")
])

MathMLも完全にサポートしています。


動的なテンプレートの作成

ネストされたvnodeもJavaScriptの式でしかないため、JavaScriptの機能を活用して操作することができます。

動的なテキスト

var user = {name: "ジョン"}

m(".name", user.name) // <div class="name">ジョン</div>

ループ

mapなどのArrayのメソッドを使い、データのリストの各項目に対するループが行えます

var users = [
    {name: "ジョン"},
    {name: "メアリー"},
]

m("ul", users.map(function(u) { // <ul>
    return m("li", u.name)      //   <li>ジョン</li>
                                //   <li>メアリー</li>
}))                             // </ul>

// ES6:
// m("ul", users.map(u =>
//   m("li", u.name)
// ))

条件分岐

三項演算子を使って、条件によってビューの内容を変えることができます。

var isError = false

m("div", isError ?"エラーが発生しました" : "保存しました") // <div>保存しました</div>

JavaScriptの式の中では、ifforといったJavaScriptの文は使えません。テンプレートの形を線形で宣言に保ち、最適化がはずれないようにするために、これらの文を使うのを避けましょう。


HTMLの変換

Mithrilでは、正しく書かれたHTMLはJSXとしてそのまま使えます。コピーアンドペースト程度の労力を払えば、独立して作られたHTMLファイルを元にしてJSXを使ってプロジェクトに組み込むことができます。

hyperscriptを使う時は、実行前にHTMLからhyperscriptの文法に変換する必要があります。HTML-to-Mithrilテンプレートコンバーターを使って変換できます。


アンチパターンを避ける

Mithrilは制約が少なく柔軟ですが、いつくか非推奨のコードパターンがあります。

動的なセレクターを避ける

異なるDOM要素は異なる属性を持ち、多くの場合は異なる挙動をします。セレクターを変更可能にすると、コンポーネントの実装の詳細がその単位から抜け落ちてしまいます。

// 避けるべきコード
var BadInput = {
    view: function(vnode) {
        return m("div", [
            m("label"),
            m(vnode.attrs.type || "input")
        ])
    }
}

セレクターを動的に変更するのではなく、コードの挙動の振れ幅を明示的に表現するか、リファクタリングして変更可能性のある部分を外に出すようにしましょう。

// 望ましい明示的なコード
var BetterInput = {
    view: function(vnode) {
        return m("div", [
            m("label", vnode.attrs.title),
            m("input"),
        ])
    }
}
var BetterSelect = {
    view: function(vnode) {
        return m("div", [
            m("label", vnode.attrs.title),
            m("select"),
        ])
    }
}

// 可変部分を外に出した望ましいコード
var BetterLabeledComponent = {
    view: function(vnode) {
        return m("div", [
            m("label", vnode.attrs.title),
            vnode.children,
        ])
    }
}

ビューメソッド内で文を避ける

JavaScriptの文を使うと、HTMLのツリー構造を変えたほうがスムーズに書けることがあり、結果的にツリー構造に影響をあたえることがあります。それによりコード量が増えたり、理解が難しくなることがあります。仮想DOMツリーを手続き的に構築することは、テンプレート全体を最初から再作成するなどの最適化が外れて、高価なペナルティを負うことがあります。

// 避けるべきコード
var BadListComponent = {
    view: function(vnode) {
        var list = []
        for (var i = 0; i < vnode.attrs.items.length; i++) {
            list.push(m("li", vnode.attrs.items[i]))
        }

        return m("ul", list)
    }
}

これらの代わりに、三項演算子や配列のメソッドを使うほうが望ましいです。

// 望ましいコード
var BetterListComponent = {
    view: function() {
        return m("ul", vnode.attrs.items.map(function(item) {
            return m("li", item)
        }))
    }
}

viewの外でvnodeを作るのを避ける

再描画時に、以前のレンダリング時と完全に同じvnodeが見つかると、レンダリングがスキップされ、内容は更新されません。これはパフォーマンスの最適化に使えるように見えるかもしれませんが、ノードのツリーの動的な変更が行われなくなります。これにより、下流のライフサイクルメソッドが再描画時にトリガされないなどの副作用が発生します。新しいvnodeは古いものと比較され、vnodeそのものへの変更が保存されないため、この意味でMithrilのvnodeはイミュータブルです。

コンポーネントのドキュメントにはこの問題に関する詳細情報とアンチパターンが含まれています。


License: MIT. © Leo Horie.