m
- 使用方法
- データへバインディング
- HTMLエンティティを使用する
- 本物のDOM要素へアクセス
- 設定データの永続化
- デストラクタ
- ラウト変更前後でのDOMエレメントの永続化
- SVG
- フォーカスの操作
- リスト内のソートと削除の操作
- コンポーネントの短縮形
- シグニチャ
config
属性
この関数は仮想DOM要素を組み立てるための簡単なインタフェースを提供する関数です。この仮想DOM要素はm.render()
メソッドを使ってレンダリングします。
仮想エレメントを定義するときはCSSセレクタを使うのが推奨です。詳細についてはシグニチャのセクションを参照してください。
使用方法
シンプルなタグセレクタを使って、HTMLに似たテンプレートを作成できます:
m("br"); //<br>を表現する仮想エレメントを作成
m("div", "Hello"); //<div>Hello</div>を作成
m("div", {class: "container"}, "Hello"); //<div class="container">Hello</div>を作成
m()
関数が返す値は、実際のDOM要素ではありません。仮想DOMを本物のDOMに変換するにはm.render()
を呼ぶ必要があります。
m.render(document.body, m("br")); //<br>タグを<body>に出力
より複雑なCSSセレクタを使用することもできます:
m(".container"); //<div class="container"></div>を作成
m("#layout"); //<div id="layout"></div>を作成
m("a[name=top]"); //<a name="top"></a>を作成
m("[contenteditable]"); //<div contenteditable></div>を作成
m("a#google.external[href='http://google.com']", "Google"); //<a id="google" class="external" href="http://google.com">Google</a>を作成
それぞれのm()
呼び出して作成される仮想DOM要素は、DOMエレメントに対応する情報を持つJavaScriptオブジェクトで、最終的にDOMエレメントに変換されます。
もちろん、ネストされた仮想エレメントも作成できます:
m("ul", [
m("li", "アイテム 1"),
m("li", "アイテム 2"),
]);
/*
これが作成される:
<ul>
<li>アイテム 1</li>
<li>アイテム 2</li>
</ul>
*/
a#google.external[href='http://google.com']
)のようなCSSセレクタ文法は、エレメントの静的なアトリビュートの定義で使用します。静的というのはアプリケーションの中で動的に変更されることがない要素になるという意味です。
m("div", {class: "container"}, "Hello")
の2番目のパラメータのattributes
引数は、動的に変更する可能性のある属性を設定するのに使います。
ウェブサービスから帰ってきたエントリーを元に、動的にリンクが書き換わるようなリンクを作成するには次のようにします:
//この`link`変数の値はウェブサービスから返ってきた値という想定
var link = {url: "http://google.com", title: "Google"}
m("a", {href: link.url}, link.title); //<a href="http://google.com">Google</a>を生成
ちょっと応用的なサンプルです:
var links = [
{title: "アイテム 1", url: "/item1"},
{title: "アイテム 2", url: "/item2"},
{title: "アイテム 3", url: "/item3"}
];
m.render(document.body, [
m("ul.nav",
links.map(function(link) {
return m("li",
m("a", {href: link.url}, link.title)
);
})
)
]);
生成される結果:
<body>
<ul class="nav">
<li><a href="/item1">アイテム 1</a></li>
<li><a href="/item2">アイテム 2</a></li>
<li><a href="/item3">アイテム 3</a></li>
</ul>
</body>
このように、普通のJavaScriptの文法を利用してフロー制御を行うことができます。これによって開発者から見てテンプレートに対してあらゆる抽象化が行えるようなっています。
attributes
引数の中では、JavaScriptのプロパティ名とHTML属性名の両方を使って設定できますが、適切な型を使って渡されます。JavaScriptとHTMLで同じ名前の属性があった場合は、MithrilはJavaScript側の属性を優先します。
m("div", {class: "widget"}); //<div class="widget"></div>を生成
m("div", {className: "widget"}); //<div class="widget"></div>を生成
m("button", {onclick: alert}); //押されるとアラートを表示する<button></button>を生成
//`readonly`の設定にJavaScript文法(大文字の"O"になる)を使用
//HTML属性とは異なり、JavaScriptのプロパティではboolean型を使用する
m("input", {readOnly: true}); //yields <input readonly />
//HTML属性名は`setAttribute`が使われるため期待と異なることがある
m("input", {readonly: false}); //<input readonly="false" />は属性が存在するので読み込み専用
アルファベットと数値以外の文字列を持つ属性名を設定したいときは、JSON文法を使うことができます:
m("div", {"data-index": 1}); //<div data-index="1"></div>を生成
次のようにインラインでスタイルを設定できます:
m("div", {style: {border: "1px solid red"}}); //<div style="border:1px solid red;"></div>を生成
フレームワークは必要最低限の動作をするように設計されているため、どの属性名にも、px
、%
といった単位を付けることはしません。一般的に動的に値を変更する時以外は、インラインでのスタイル設定は使用すべきではありません。
Mithrilはまた、インラインのスタイル属性のCSSのプロパティ名を自動でcamel-caseに変換することはありません。そのため、JavaScriptのオブジェクトを経由してJavaScript文法を使って設定すべきです:
m("div", {style: {textAlign: "center"}}); //<div style="text-align:center;"></div>を生成
m("div", {style: {cssFloat: "left"}}); //<div style="float:left;"></div>を生成
//これは動作しない
m("div", {style: {"text-align": "center"}});
m("div", {style: {float: "left"}});
JavaScript文法におけるCSSのルールはこちらで確認できます。
インラインの文字列を使うことで、CSS文法を使ってスタイルルールを定義することもできます:
m("div[style='text-align:center']"); //<div style="text-align:center;"></div>を生成
注意点としては、CSS文法を使うとDOM要素の再描画の時にstyle
属性が強制的にテンプレートで設定されたスタイルに上書きされます。そのため、Mithrilのテンプレートの外からスタイルを変更するようなサードパーティ製ツールを使用する必要があるときは、CSS文法を避けてください。この後のconfig
の項目で詳しく説明します。
データへバインディング
柔軟性を維持するために、Mithrilでは双方向バインディングを生成するヘルパーは提供していません。しかし、簡単に実現できます:
//データストア
var name = m.prop("")
//データをビューにバインド
m("input", {oninput: m.withAttr("value", name), value: name()})
この上記のサンプルを実行すると、oninput
イベントハンドラがname
getter-setterを更新します。Mithrilの自動再描画システムが表示される値の更新にともなって再描画を行います。詳細の情報はこちらのm.prop
getter-setterユーティリティと、こちらのm.withAttr
イベントハンドラで読むことができます。また、こちらのドキュメントで再描画システムがどのように動くのか学ぶことができます。
Mithrilは、常にモデルレイヤのデータが正当なものであると判断します。これはつまり、下記のサンプルのスクリーン上のテキスト入力の値は、モデルデータが変更されるたびに上書きされることを意味しています:
//このサンプルではイベントハンドラが`name` getter-setterの値を変更することはない
//再描画があるたびに現在のUI上の値が`name()`の値を使って書き換えられる
m("input", {value: name()})
一般的なリファクタリングのテクニックを使うと、表現力を向上させることができます:
//バインディングをヘルパ関数にリファクタリング
var binds = function(prop) {
return {oninput: m.withAttr("value", prop), value: prop()}
}
//データストア
var name = m.prop("")
//ビュー内のデータストアにバインド
m("input", binds(name))
もっとアグレッシブなリファクタリングを行うとこうなります:
//バインディングをシンプルなヘルパにリファクタリング
var input = function(prop) {
return m("input", {oninput: m.withAttr("value", prop), value: prop()})
}
//データストア
var name = m.prop("")
//ビュー内のデータストアにバインディング
input(name)
バインディングに関してパフォーマンスと表現を改善する他の方法についてはブログの記事で学ぶことができます。
HTMLエンティティを使用する
デフォルトでは、MithrilはXSS攻撃を防ぐためにHTMLの文字列をエスケープします。
m("div", "×") //<div>&times;</div>になる
信頼できるHTML文字列をエスケープを抑える時はm.trust
を使用します。
m("div", m.trust("×")) //<div>×</div>になる
本物のDOM要素へアクセス
config
という名前のHTML標準ではない属性を定義することができます。この特別なパラメータを使うと、DOMエレメントの精製後にその要素に対してメソッドが呼べるようになります。
この機能は、例えばcanvas
エレメントを作り、JavaScriptの描画APIを使って絵を描く場合などに便利です:
function draw(element, isInitialized, context) {
//一度描画したら再描画はしない
if (isInitialized) return;
var ctx = element.getContext("2d");
/* 描画コード */
}
var view = [
m("canvas", {config: draw})
]
//このメソッド呼び出しをすると、canvasエレメントが作られ、`initialized`がfalseに設定される
m.render(document.body, view);
//ここではisInitializedは`true`になる
m.render(document.body, view);
設定config
の一般的な使用法の1つに、m.route
と一緒に使用して、設定されているラウティングモードにかかわらず、透過的に動作するようにリンクを拡張するというものがあります。
//このリンクはどのMithrilのラウティングシステムのモードでも使用できる。
//ラウティングのモードにはhash, querystring, pathnameがあるが、
//`href`属性で、ラウティングの文法(`#`, `?`)をハードコードする必要はない。
m("a[href='/dashboard']", {config: m.route}, "Dashboard");
config
の仕組みを使うと、入力フォームにフォーカスを移動したり、通常の属性文法では呼び出せないメソッドを呼び出すことができます。
また、config
コールバックはレンダリングのライフサイクルが終わった時にしか呼ばれません。このため、コントローラやモデルの値など、すぐにレンダリングに反映したい変更をするのにconfig
を使うのは適しません。この方法でコントローラやモデルの値を変更しても、次のm.render
呼び出しか、m.mount
呼び出しがあるまでは描画されません。
この機能を使うと、サードパーティのライブラリを統合する時などに、コントローラのメソッドを呼び出すようなカスタムのイベントハンドラを登録できます。ただし、Mithrilの自動描画システムがきちんと働くようにしなければなりません。これについては他のライブラリとの統合を参照してください。
この機能を使って、window.onresize
などのイベントを他のエレメントに付与することもできますが、その場合はトラブルを避けるためにctx.onunload
を使って確実にイベントハンドラを削除してください。
設定データの永続化
config
の3番目の引数を使うと、再描画時に仮想のDOM要素を保持しておくことができます。config
のコールバックがサードパーティのクラスのインスタンスを作成したり、再描画時にそのインスタンスにアクセスすることができます。
次のサンプルは、再描画のカウントを表示します。このコードの中で、再描画のカウントはコンテキストオブジェクト内に格納されます。このオブジェクトは再描画のたびにアクセスができます。
function alertsRedrawCount(element, isInit, context) {
if (!isInit) context.count = 0
alert(++context.count)
}
m("div", {config: alertsRedrawCount})
デストラクタ
config
関数に渡されるcontext
オブジェクトには、onunload
と呼ばれるプロパティがあります。この関数は、Mithirilの差分検出エンジンによって対象のエレメントがドキュメントから切り離される時に呼ばされます。
エレメントが破棄されるときに、なんらかの後片付けのタスクを実行したい時にはこの機能は便利です。例えば、setTimeout
を辞めるといったタスクがあるでしょう。
function unloadable(element, isInit, context) {
context.timer = setTimeout(function() {
alert("timed out!");
}, 1000);
context.onunload = function() {
clearTimeout(context.timer);
console.log("divをアンロード");
}
};
m.render(document, m("div", {config: unloadable}));
m.render(document, m("a")); //`divをアンロード`とログに表示されるが、`alert`は呼ばれない
ラウト変更前後でのDOMエレメントの永続化
routerを使用している時は、ラウトが変更されると、前のページで使用されていたプラグインをアンロードするために、DOMツリーがスクラッチから再生成されます。もし、ラウトの変更前後で、DOMエレメントを破棄せずに残しておきたい場合には、configのコンテキストオブジェクト内のretain
フラグを設定してください。
下記のサンプルには、2つのラウトがあります。ユーザがどちらかのURLにアクセスすると、それぞれのコンポーネントをロードします。どちらのコンポーネントもmenu
テンプレートを使用しています。このテンプレーには2つのコンポーネント間をナビゲートするリンクがあります。そして、最初期化のコストが非常に高いエレメントだとします。エレメントのconfig関数の中でcontext.retain = true
と設定すると、ラウトが変更されてもコストが高い(つもりの)spanタグを保持します。
//メニューテンプレート
var menu = function() {
return m("div", [
m("a[href='/']", {config: m.route}, "Home"),
m("a[href='/contact']", {config: m.route}, "Contact"),
//最初期化が非常に高価(つもり)のDOMエレメント
m("span", {config: persistent})
])
}
//ラウトが変更されてもエレメントを保持する設定
function persistent(el, isInit, context) {
context.retain = true
if (!isInit) {
// `/` と`/contact` の間を何度行き来しても、一度だけ実行される
doSomethingExpensive(el)
}
}
//上記のメニューを使用するコンポーネント
var Home = {
controller: function() {},
view: function() {
return m("div", [
menu(),
m("h1", "ホーム")
])
}
}
var Contact = {
view: function() {
return m("div", [
menu(),
m("h2", "Contact")
])
}
}
m.route(document.body, "/", {
"/": Home,
"/contact": Contact
})
context.retain = true
を設定しても、既存のエレメントと大きく異る場合はエレメントは破壊されて再生成されます。「大きく異る」の判定基準は:
- タグ名が変更されるか、
- HTML属性のリストが変更されるか、
- エレメントのid属性が変更された場合
context.retain = false
が設定された場合は、変更が小さくてもエレメントは破棄されて再生成されます。
SVG
ネイティブでSVGをサポートしていないブラウザをサポートしたくないと思わない限り、Mithrilを使ってSVGドキュメントを作ることができます。
仮想のDOMツリーの中にSVGの要素が現れると、Mithrilは自動で正しいXML名前空間を設定します。
m("svg[height='200px'][width='200px']", [
m("image[href='foo.jpg'][height='200px'][width='200px']")
])
フォーカスの操作
仮想DOMの差分検知アルゴリズムには、DOM要素の厳密な同一性について判断できないという欠点があります。例えば、リストの先頭に挿入するような操作は、すべての要素が変更されたとみなして、すべてが再生成されてしまう可能性もあります。他の副作用としては、フォーカスを持ったエレメントが移動してしまったり、config
を通じて追加されたサードパーティのプラグインが違うエレメントの内側に移動してしまう可能性があります。
幸い、Mithrilでは、開発が各エレメントに識別子のキーを付与できるため、シフト、スプライス、ソートといった配列操作が行われても、再描画時に最小のエレメントだけを変更し、残りのDOMエレメントは変更されないようにする、といったことが可能です。これを使うと、入力のフォーカスや、プラグインの状態を正しく維持することができます。
DOM要素の識別子を維持するには、変更したい配列の子供に、key
プロパティを設定します。リストの子の兄弟のDOMエレメントのキーはユニークでなければなりませんが、アプリケーション全体でグローバルにユニークである必要はありません。キーは文字列でも数値でも大丈夫です。
m("ul", [
items.map(function(item) {
return m("li", {key: item.id}, [
m("input")
]);
})
]);
上記のサンプルでは、items
がソートされたり、順序が逆転したあとに再描画されても、正しくフォーカスが維持されます。キーは、items
配列に一番近いエレメントのli
エレメントに設定します。フォーカスを維持したいのはinput
エレメントですが、このエレメントに直接キーを設定することはしません。
key
属性があるかどうかに加えて、差分検知のルールもエレメントの再生成を行うかどうかの判定に使用される点は注意してください。ノード名が変更されたり、属性名のリストが変更されたり、ID属性が変更されれば、エレメントは再生成されます。想定外の事態を避けるには、undefined
やnull
を使って属性の値だけを変更するようにしてください。条件によって属性の辞書そのものを置き換えるコードは避けましょう。
//このイディオムは避けること
m("li", selected ?{class: "active"} : {})
//代わりにこのイディオムを使う
m("li", {class: selected ?"active" : ""})
リスト内のソートと削除の操作
入力のフォーカスと同様に、キーを使うとリスト内のデータとそのDOM表現間の参照の整合性を維持できます。
m("ul", [
items.map(function(item) {
return m("li", {key: item.id}, [
m("input")
]);
})
]);
リストをソートしたり、要素を削除したり、スプライスする場合は常にキーを使用すべきです。
コンポーネントの短縮形
もし、m()
の最初の引数がコンポーネントであれば、m.component()
の代わりに使うことができます。
var MyComponent = {
controller: function() {
return {greeting: "hello"}
},
view: function(ctrl, args) {
return m("h1", ctrl.greeting + " " + args.data)
}
}
m.render(document.body, [
//以下の2行は等価です
m(component, {data: "world"}),
m.component(component, {data: "world"})
])
より詳細な情報はコンポーネントを参照してください。
シグニチャ
VirtualElement m(String selector [, Attributes attributes] [, Children... children])
where:
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object<any | void config(DOMElement element, Boolean isInitialized, Object context, VirtualElement vdom)>
Children :: String text | VirtualElement virtualElement | Component | SubtreeDirective directive | Array<Children children>
Component :: Object { Function?controller, Function view }
SubtreeDirective :: Object { String subtree }
String selector
DOMエレメントを表現するCSSルールの文字列を指定します。
タグ、ID、クラス、属性セレクタのみがサポートされています。
もしタグセレクタが省略されると、
div
がデフォルトで設定されます。もし
selector
とattributes
パラメータで同じ属性が定義されると、attributes
が使用されます。開発者の利便性のために、Mithrilは
class
属性は特別扱いをします。もし両方で定義されていた場合には、スペース区切りのリストとして2つのパラメータを統合します。ただし、同じクラスが2度宣言されていたら、重複は取り除かれます。サンプル:
"div"
"#container"
".active"
"[title='Application']"
"div#container.active[title='Application']"
".active#container"
Attributes attributes (optional)
キー・バリューのマップはHTML属性とその値を定義します。
HTMLとJavaScriptの両方の属性名を使用できます。
class
もclassName
も使えます。型は値の、それぞれの属性が期待する型とマッチする必要があります。
例えば、
className
の値は文字列である必要があります。もし、指定された属性名がHTMLとJavaScriptで異なる型を期待している場合は、JavaScriptの型が使用されます。
例えば、
onclick
属性は関数を受け入れます。同様に
readonly
属性にfalse
を設定すると、その属性をHTMLから取り除くのと同じ結果になります。<a>
エレメントのhash
属性のように、JavaScriptだけで使用できるプロパティの値も設定できます。もし
selector
とattributes
パラメータで同じ属性が定義されると、attributes
が使用されます。開発者の利便性のために、Mithrilは
class
属性は特別扱いをします。もし両方で定義されていた場合には、スペース区切りのリストとして2つのパラメータを統合します。ただし、同じクラスが2度宣言されていたら、重複は取り除かれます。サンプル:
{ title: "Application" }
{ onclick: function(e) { /*ハンドラの処理コード*/ } }
{ style: {border: "1px solid red"} }
config
属性void config(DOMElement element, Boolean isInitialized, Object context, VirtualElement vdom) (optional)
config
という名前のHTML標準ではない属性を定義することができます。この特別なパラメータを使うと、DOMエレメントの精製後にその要素に対してメソッドが呼べるようになります。この機能は、例えば
canvas
エレメントを作り、JavaScriptの描画APIを使って絵を描く場合などに便利です:function draw(element, isInitialized) { //一度描画したら再描画はしない if (isInitialized) return; var ctx = element.getContext("2d"); /* 描画コード */ } var view = [ m("canvas", {config: draw}) ] //このメソッド呼び出しをすると、canvasエレメントが作られ、`initialized`がfalseに設定される m.render(document.body, view); //ここではisInitializedは`true`になる m.render(document.body, view);
設定
config
の一般的な使用法の1つに、m.route
と一緒に使用して、設定されているラウティングモードにかかわらず、透過的に動作するようにリンクを拡張するというものがあります。//このリンクはどのMithrilのラウティングシステムのモードでも使用できる。 //ラウティングのモードにはhash, querystring, pathnameがあるが、 //`href`属性で、ラウティングの文法(`#`, `?`)をハードコードする必要はない。 m("a[href='/dashboard']", {config: m.route}, "Dashboard");
config
の仕組みを使うと、入力フォームにフォーカスを移動したり、通常の属性文法では呼び出せないメソッドを呼び出すことができます。また、
config
コールバックはレンダリングのライフサイクルが終わった時にしか呼ばれません。このため、コントローラやモデルの値など、すぐにレンダリングに反映したい変更をするのにconfig
を使うのは適しません。この方法でコントローラやモデルの値を変更しても、次のm.render
呼び出しか、m.mount
呼び出しがあるまでは描画されません。この機能を使うと、サードパーティのライブラリを統合する時などに、コントローラのメソッドを呼び出すようなカスタムのイベントハンドラを登録できます。ただし、Mithrilの自動描画システムがきちんと働くようにしなければなりません。これについては他のライブラリとの統合を参照してください。
この機能を使って、
window.onresize
などのイベントを他のエレメントに付与することもできますが、その場合はトラブルを避けるためにctx.onunload
を使って確実にイベントハンドラを削除してください。- DOMElement element
m()
呼び出しで定義された仮想エレメントに対応するDOMエレメントです。- Boolean isInitialized
関数がこのエレメントに対して実行されるのが初回かどうかを表すフラグです。そのエレメントに対して初めて実行される時はこのフラグがfalseに設定され、エレメントの作成後に再描画が発生してそこから呼ばれる時はtrueになります。
- Object context
再描画間で状態を保持するオブジェクトです。このオブジェクトはページのライフサイクルを通じて何度もアクセスする必要がある、サードパーティのクラスのインスタンスを保持するのにも使えます。
次のサンプルは、再描画のカウントを表示します。このコードの中で、再描画のカウントはコンテキストオブジェクト内に格納されます。このオブジェクトは再描画のたびにアクセスができます。
function alertsRedrawCount(element, isInit, context) { if (!isInit) context.count = 0 alert(++context.count) } m("div", {config: alertsRedrawCount})
config
関数に渡されるcontext
オブジェクトには、onunload
と呼ばれるプロパティがあります。この関数は、Mithirilの差分検出エンジンによって対象のエレメントがドキュメントから切り離される時に呼ばされます。エレメントが破棄されるときに、なんらかの後片付けのタスクを実行したい時にはこの機能は便利です。例えば、
setTimeout
を辞めるといったタスクがあるでしょう。function unloadable(element, isInit, context) { context.timer = setTimeout(function() { alert("timed out!"); }, 1000); context.onunload = function() { clearTimeout(context.timer); console.log("divをアンロード"); } }; m.render(document, m("div", {config: unloadable})); m.render(document, m("a")); //`divをアンロード`とログに表示されるが、`alert`は呼ばれない
- VirtualElement vdom
config
関数が設定された仮想DOMエレメントChildren children (optional)
この属性が文字列の場合は、テキストノードとしてレンダリングされます。文字列をHTMLとしてレンダリングしたい場合は、
m.trust
を参照してください。もし仮想エレメントが指定された場合には、DOMエレメントとしてレンダリングされます。
もしこれがcomponentだった場合は、コンポーネントはインスタンス化され、Mithrilによって内部で管理されます。
配列だった場合には、配列の要素も再帰的にレンダリングされて、生成されたエレメントの子供として追加されます。
SubtreeDirectiveの値が"retain"だった場合は、もしあれば既存のDOMツリーをその場で保持します。要塞については、subtree directives.htmlを参照してください。
returns VirtualElement
返されるVirtualElementは、DOMエレメントを表すデータ構造です。このデータ構造は
m.render
を通じてレンダリングされます。