JavaScript(ES6)でElm Architecture(Virtual-DOMなし) --- 可変個コントロール

下記記事にてJavaScriptにElmのModelとUpdateの機構を取り入れてみました。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) - 何でもプログラミング

今回は引き続き、可変個のコントロールに対応してみたいと思います。

アプリケーションコード

カウンターを追加、削除できるアプリケーションを実装してみたいと思います。

f:id:any-programming:20170723141650p:plain

可変個のコントロールの定義として、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利用しない版 --- 可変個のコントロール - 何でもプログラミング