◆ elm の sandbox の機能
◆ init, update, view を渡すとイベント時に update で model を更新
◆ → 更新された model を使って view で再 render

lit-html と似てる

久々に elm 触りました

elm ってコンポーネントじゃないので イベント時の更新で全体が更新対象なのですよね
毎回 view 関数で全体のツリーを作ってますし

React だとコンポーネントがあるので 更新があったときに状態の変化が起きたコンポーネント以下の仮想 DOM のツリーが更新されて そこだけを差分があるかチェックして更新になるかと思います
変更ない場合の仮想 DOM を作ったり差分検出コストはそれほどないとはいえ コンポーネントだけで済むところを全体チェックはムダの多さが気になるところです

ただ lit-html でも lit-element などを使って WebComponents にしないと render 関数に渡すテンプレート全部を作ってます
仮想 DOM とは違いますが ${} で埋め込んだ Part ごとの差分チェックを全てに対して行っています
変更は編集エリアだけで済んだとしても その他ヘッダーなども変更の有無のチェックしてます

そう考えてみると elm って lit-html に近い感じがしました
内部の差分更新エンジンとかは気にせず ライブラリを使う側の使い方としては 同じようにできそうな気がします

elm 風にしてみる

そんな思いつきで作ったのがこちら
機能的には elm の sandbox に対応してます
Cmd や Sub まで考えると複雑になってくるのでシンプルなものにしました

[lelm.js]
import { html, render as litRender } from "https://unpkg.com/lit-html?module"
import { live } from "https://unpkg.com/lit-html/directives/live.js?module"
export { html, live }

export const event = (type, option) => event => {
const detail = { type, option, event }
event.target.dispatchEvent(new CustomEvent("#", { bubbles: true, detail }))
}

export const app = ({ init, update, view, root }) => {
const root_elem = root instanceof HTMLElement ? root : document.querySelector(root)
if (!root_elem) throw new Error("invalid root element")

let model = init

const render = () => {
litRender(view(model), root_elem)
}

const onEvent = eve => {
const msg = eve.detail
model = update(msg, model)
render()
}

root_elem.addEventListener("#", onEvent)
render()
}

短いですが 差分更新部分がないとこれだけで似た使い方できます

使い方

使い方は上のモジュール (lelm.js) をインポートして app 関数を使います
app 関数に init, update, view, root プロパティをもつオブジェクトを渡して実行します

view には model を受け取って lit-html の html 関数を使って TemplateResult を返す関数を設定します
lelm.js は lit-html の html と live 関数を export してるのでこれらを import して使えます

init には model の初期値を設定します

update には msg と今の model から新しい model を返す関数を設定します
msg は { type, option, event } のオブジェクトで event は生のブラウザのイベントです
type と option はリスナ設定時にセットします

html`<div @click=${event("type", "option")}>DIV</div>`

lelm.js からインポートした event 関数の引数に type と option を渡します
event 関数を通さないと update に設定した関数が呼び出されないのでこの関数は必須です

root にはこのアプリのコンテナになる HTML 要素かそのセレクタを設定します
lit-html の render 関数の第二引数にわたすものです

使用例

使用例はこんな感じです
よくある TODO アプリに近いなにかです
テキストボックスに入力して OK ボタンを押すとそのときのテキストが下に追加されて X ボタンで削除できます

<!doctypt html>

<script type="module">
import { html, app, event, live } from "/l/lelm.js"

const view = model => html`
<div>
<input @input=${event("input")} .value=${live(model.text)}>
<button @click=${event("add")}>OK</button>
</div>
<div>
${model.items.map(itemView)}
</div>
`

const itemView = (item, i) => html`
<div class="item">
<b>${i}</b>
<span>${item}</span>
<button @click=${event("delete", i)}>X</button>
</div>
`

const update = (msg, model) => {
switch (msg.type) {
case "input":
return { ...model, text: msg.event.target.value }
case "add":
if (model.text) {
return { ...model, text: "", items: model.items.concat([model.text]) }
} else {
return model
}
case "delete":
return { ...model, items: model.items.filter((x, i) => i !== msg.option) }
}
}

const init = {
text: "",
items: [],
}

app({
init,
update,
view,
root: "#root",
})
</script>

<div id="root"></div>

ライブラリ部分はほとんどないだけあって 普段の lit-html を使うときとそこまで変わりません
ただ イベントが起きた時の更新を update でまとめて受け取ったり update や msg や view などの名前を決めて固定にしたほうが見やすくなっていいかなと思います