MithrilでウェブのUIを作る時に、標準のHTMLフォームの要素を使う時の備忘録
<select>
<select>
タグが選択されたときのイベントは selectedIndex
に対して onchange
イベントを追跡するようにすればOKです。m.prop
で作ったプロパティを指定すれば選択された要素のインデックスが格納されます。もちろん、イベントハンドラの関数を作って、好きな処理を行わせることができます。
<option>
タグの方は選択された要素を selected: true
にしておく必要があります。画面更新時に毎回先頭の要素が表示されてしまいます。
こんな感じの仮想DOMが最終的にできるようにします。
m('select', {onchange: イベントハンドラ}, [
m('option', '選択肢1'),
m('option', '選択肢2'),
m('option', {selected: true}, '選択肢3'),
m('option', '選択肢4'),
]);
プログラム的に書くにはこんな感じ。
const items = ['選択肢1', '選択肢2', '選択肢3', '選択肢4'];
const Component = {
controller: function () {
this.selected = m.prop(0);
},
view(ctrl) => {
return m('select', {
onchange: m.withAttr('selectedIndex', ctrl.selected)
}, items.map((item, index) => {
return m('option', {selected: ctrl.selected() === index}, item);
}));
}
}
変更されると、 ctrl.selected()
が呼ばれます。
実際には、表示項目は実際はサーバにアクセスして取ってきたデータを使ったりするでしょうし、固定値であってもコンポーネントの引数で渡したりする実装が柔軟でいいと思います。
m.withAttr
onchangeなどに関数を書くと、変更された値ではなくて、HTMLのEventオブジェクトが第一引数に渡されてきます。m.withAttr
はそのEventオブジェクトからイベントを発行したDOMElementを取ってきて、制定された名前のプロパティを取り出して、2番目の引数の関数にわたします。
上記のコードのview関数を m.withAttr
を使わずに書くには、次のように書くのと等価です。そのようなヘルパー関数を作る関数です。
function helper(event) {
ctrl.selected(event.srcElement.selectedIndex);
}
return m('select', {
onchange: helper
}, items.map((item, index) => { // itemsは文字列配列
return m('option', {selected: ctrl.selected() === index}, item);
}))
input[type="text"]
こんな感じの仮想DOMが最終的にできるようにします。
m('input', {onchange: イベントハンドラ, value: 値})
ビューモデルのプロパティに対してtwo-way bindingする(プロパティの値を表示し、なおかつUIで変更されたらそれをプロパティに格納するには次のようにします。
const Component = {
controller: function() {
this.text = m.prop('hello world');
},
view(ctrl) {
return m('input', {onchange: m.withAttr('value', ctrl.text), value: ctrl.text()});
}
}
ちょっと他のMVCフレームワークに比べるとテキストの重複が多いように見えてしまいます(valueとかtextとか)が、Mithrilは宣言的構文は用意されてなくて、あくまでもモデル→ビューと、ビュー→モデルの動きを手続き的に書く必要があるので、こうなります。Explicit is better than implicit.
読み込み元から読み込む時は指定の関数から取得して、書き込み先も関数呼び出しなので、必要であれば両方に情報の加工とかバリデーションの関数を挟むことができます。
onchangeだと、enterが押されたり、フォーカスが外れたタイミングでのみ呼ばれますが、oninputにすれば、変更した内容がリアルタイムで取得できます。
<textarea>
こんな感じの仮想DOMが最終的にできるようにします。
m('textarea', {onchange: イベントハンドラ}, 値)
<input type="text">
とほぼ同じなんですが、値はvalue属性ではなくて、子要素にテキストとして渡す点だけが違います。
・・・のはずなんですが、value属性にテキストを入れても表示されます(少なくともChromeでは)。ただし、HTMLの仕様を見ても、<textarea>
タグはvalue属性なんか無いんですけどね。そのため、onchangeイベントもそのまま使えます。
input[type=checkbox]
テキスト入力と似ていますが、チェック状態を作成時に指定するプロパティや、イベントハンドラで使うプロパティ名は'value'
ではなくて、'checked'
を使います。ラベルは<input>
タグを囲むように<label>
タグを置いて、その中に書くと、クリック可能な範囲が箱からラベルを含めた広い領域になるので必須ですね。
const Component = {
controller: () => {
this.checked = m.prop(true);
},
view(ctrl) {
return m('label', m('input[type=checkbox]', {
onchange: m.withAttr('checked', ctrl.checked),
checked: ctrl.checked()
}), 'ラベル')
}
}
input[type=radio]
値を統括するタグがなくて、複数のタグが存在するけどフォームを送信すると値が1つだけ送信されるという、不思議ちゃんなラジオボタン。
通常のHTMLの使用方法だと、nameとvalueを設定して使いますが、リストに入ったテキストに対して動的に作り、選択された要素のインデックス値をプログラム中で管理するのであれば(comboboxみたいに使うのであれば)、イベントハンドラに値を持たせてしまえばこれらの属性の設定は不要です。
controller: function () {
this.selected = m.prop(0);
this.labels = ["ラジオボタン1", "ラジオボタン2", "ラジオボタン3"];
},
view(ctrl) {
return m('div', ctrl.labels.map((label, index) => {
return m('label', m('input[type=radio]', {
onchange: ctrl.selected.bind(null, index),
checked: (ctrl.selected() === index)
}), label);
}));
}
bindなんか使いたくねぇよ、valueに設定してそれを渡すようにしたいよ、ということもあるかもしれませんが、valueに設定すると数値でも文字列型になってしまうのでそこだけ気をつければOKです。今回はラジオボタンの識別子はインデックス値を使っていますが、ラジオボタンごとに固有の文字列があるのであれば、それをvalue属性に持たせるのは問題ありません。
valueを使う場合は次の通り。
controller: function () {
this.selected = m.prop(0);
this.labels = ["ラジオボタン1", "ラジオボタン2", "ラジオボタン3"];
},
view(ctrl) {
return m('div', ctrl.labels.map((label, index) => {
return m('label', m('input[type=radio]', {
onchange: m.withAttr('value', ctrl.selected),
value: index,
checked: (ctrl.selected() === String(index))
}), label);
}))
}));
}
感想
Qiitaにjsfiddle貼れたらよかった