JavaScript(ES6)でElm Architecture(Virtual-DOMなし)

Elm自体がJavaScriptを生成するものなので実際には利用することはないと思いますが(さらに生のJavaScriptがよければReact.jsがあります)、ElmのModelとUpdateの仕組みを生のJavaScriptで実装してみました。

WPFC#で実装されたものは、下記記事にて参照できます。
WPFでElm Architecture --- Xaml利用しない版 - 何でもプログラミング

アプリケーションコード

簡単なカウンタアプリを実装します。

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

initialModel 状態の初期値を定義
message update関数で処理させる内容の識別子
update 現在のmodelと、messageを受け取って、新しいmodelを返す
bind modelのCell(現在値+変更通知)と、send(message)関数を、html要素にバインドする

CellはFunctional Rective Programming(FRP)でのBehaviorを意図しており、詳しくは下記を参照してください。
SodiumでFunctional Reactive Programming (F#) --- 導入 - 何でもプログラミング

<body>
    <button id="dec">dec</button>
    <input type="text" id="count"></input>
    <button id="inc">inc</button>
</body>
const initialModel = {
    count : 0,
};

const message = {
    increment : Symbol(),
    decrement : Symbol(),
}

function copy(obj, diff) {
    return Object.assign({}, obj, diff);
}

function update(model, msg){
    switch (msg.id) {
        case message.increment :   
            return copy(model, {count : model.count + 1});
        case message.decrement :
            return copy(model, {count : model.count - 1});
        default : throw "invalid message"
    }
}

function byId(name) {
    return document.getElementById(name);
}

function bind(model, send) {
    model.map(x => x.count).listen(x => byId("count").value = x);

    byId("dec").onclick = () => send({ id: message.decrement });
    byId("inc").onclick = () => send({ id: message.increment });
}

window.onload = () =>
    startApp(initialModel, update, bind);


Cellクラス

listenでコールバックを登録し、sendでデータを送出し、mapでデータの変換を行います。

listenした後、コールバックを解除する機能は実装してありませんので、利用する際(コントロールを動的に追加or削除)はリークに注意してください。

FRPのライブラリSodiumにJavaScript版がありますので、そちらのCellを利用することも可能です。

class Cell {
    constructor(initialValue) {
        this.value = initialValue;
        this.callbacks = [];
    }
    listen(f) {
        f(this.value);
        this.callbacks.push(f);
    }
    send(value) {
        if (this.value === value)
            return;
        this.value = value;
        this.callbacks.forEach(f => f(value));
    }
    map(f) {
        const cell = new Cell(f(this.value));
        this.callbacks.push(x => cell.send(f(x)));
        return cell;
    }
}


startApp

initialModelとupdateから、modelのCellとsend関数を作成し、bindを呼び出しています。

function startApp(initialModel, update, bind){
    const cell = new Cell(initialModel);
    const send = msg => cell.send(update(cell.value, msg));
    bind(cell, send);
}