JavaScript(ES6)でElm Architecture(Virtual-DOMなし) --- 可変個コントロール
下記記事にてJavaScriptにElmのModelとUpdateの機構を取り入れてみました。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) - 何でもプログラミング
今回は引き続き、可変個のコントロールに対応してみたいと思います。
アプリケーションコード
カウンターを追加、削除できるアプリケーションを実装してみたいと思います。
可変個のコントロールの定義として、htmlのtemplate要素を利用しています。
また可変個コントロールの動作の実装にbindItemsを利用しています。(実装はのちほど)
その他CellクラスやstartApp関数は下記記事を参照してください。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) - 何でもプログラミング
<button id="add">add</button> <button id="remove">remove</button> <!-- 可変個カウンタ --> <div id="counts"> <template> <div> <button class="dec">dec</button> <input type="text" class="count"></input> <button class="inc">inc</button> </div> </template> </div>
const initialModel = { counts : [] }; const message = { addCounter : Symbol(), removeCounter : Symbol(), increment : Symbol(), decrement : Symbol() } function update(model, msg){ switch (msg.id) { case message.addCounter : return copy(model, {counts : model.counts.insertLast(0)}); case message.removeCounter : return copy(model, {counts : model.counts.removeLast()}); case message.increment : return copy(model, {counts : model.counts.updateAt(msg.index, x => x + 1)}); case message.decrement : return copy(model, {counts : model.counts.updateAt(msg.index, x => x - 1)}); default : throw "invalid message" } } function bind(model, send) { byId("add").onclick = () => send({ id: message.addCounter }); byId("remove").onclick = () => send({ id: message.removeCounter }); // 可変個カウンタBinding bindItems(model.map(x => x.counts), "counts", (item, index, element) => { const byClass = x => element.getElementsByClassName(x)[0]; item.listen(x => byClass("count").value = x); byClass("dec").onclick = () => send({ id: message.decrement, index: index }); byClass("inc").onclick = () => send({ id: message.increment, index: index }); }); } window.onload = () => startApp(initialModel, update, bind);
bindItems
itemsのCell、親要素のID、コントロールのbindingを受け取る関数になります。
要素内のtemplateを取得し、実際にコントロールを配置するdiv要素を追加しています。
itemsが変更されたときに、変更の送出 or コントロールの作成 or コントロールの削除を行っています。
function bindItems(itemsCell, parentId, bind) { const parent = byId(parentId); const itemTemplate = parent.getElementsByTagName("template")[0]; const panel = document.createElement("div"); parent.appendChild(panel); const cells = []; itemsCell.listen(items => { for (let i = 0; i < items.length; i++) { // コントロールが存在 if (i < cells.length) { cells[i].send(items[i]); } // コントロール不足 else { const cell = new Cell(items[i]); const element = document.importNode(itemTemplate.content, true); cells.push(cell); panel.appendChild(element); bind(cell, i, panel.lastElementChild); } } // コントロール過剰 for (let i = 0; i < cells.length - items.length; i++) { cells.pop(); panel.lastElementChild.remove(); } }); }
外側のスコープのCellをbind内で利用するとリーク
スコープ外のCellを利用すると、コントロールの参照がCell内に残り続けてリークを起こしてしまいます。
そのため、スコープ外のCellを利用する場合は、一旦キャプチャしてbind関数の引数に追加する必要があります。
実装例ですが、C#のものが下記にありますので参照してください。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング