◆ 自作の作ってると色々気になって調べた結果 使い方がわかってきたのでこっちでいいかもしれない
◆ でも lit-html がでたらそっちにいきたい

前記事では hyperHTML のドキュメントやサンプルみても使い方わかりづらいし 複雑そうで使ってて困りそうだから自分が必要な機能だけのそれっぽいものを作ったのですが いざ作ってみると lit-htmlhyperHTML はどうなってるだろうとか思うことが多くて結局色々動かしたりソースコード見たりすることになってました
いったい何のためにつくったんだろうね

lit-html 風に使う

hyperHTML で一番わかりづらかったのが bind とか wire とかです
何をどうすれば動いてどういう仕組みになってるのかがわかりづらいです

lit-html だとテンプレートを作る機能と DOM を更新する機能が分かれています
テンプレートを作って それとそれを表示する親要素を設定することで画面を更新します

hyperHTML だと先に bind を使ってどこに表示するかを決めてしまい bind で受け取る関数をタグとしてテンプレートストリングを使えば DOM が更新されます

bind(document.body)`<div></div>`

これで DOM が変わるというのが直感的じゃないと言うかわかりにくさの元でした
というのもタグ呼び出しって String.raw などのように何かを取得するためのものって印象が強くて実際には呼び出しているものの getter みたいなもので関数を実行するイメージがあまりないです
なので副作用はなくただ値を取得できるものと感じてしまうのにこれで DOM が変わるというのが気持ち悪い部分です

なのでそこを lit-html 風にしてしまうとけっこうわかりやすくなります

const html = (...args) => {
return {tag_arguments: args}
}

const render = (template, element) => {
hyperHTML.bind(element)(...template.tag_arguments)
}

render の中のコードをみると apply にしたくなりますが この bind は独自のメソッドで 外部公開されていない render 関数の render.bind を中で呼び出しています
なので apply 化はできません

これらの関数を使って

render(html`<div></div>`, document.body)

とできます
テンプレートの書き方 (属性名の規則) やリピートの方法などは hyperHTML のままです
それでも lit-html の example 程度ならそのまま動きます

const helloTemplate = (name) => html`<div>Hello ${name}!</div>`;

// This renders <div>Hello Steve!</div> to the document body
render(helloTemplate('Steve'), document.body);

// This updates to <div>Hello Kevin!</div>, but only updates the ${name} part
render(helloTemplate('Kevin'), document.body);

hyperHTML

もうひとつ分かりづらい原因が hyperHTML 関数です
hyperHTML.wire や hyperHTML.bind でそれぞれの機能が使えますが hyperHTML 関数自体が色々な書き方ができる関数となっていて定義がこうです

function hyper(HTML) {
return arguments.length < 2 ?
(HTML == null ?
content('html') :
(typeof HTML === 'string' ?
hyper.wire(null, HTML) :
('raw' in HTML ?
content('html')(HTML) :
('nodeType' in HTML ?
hyper.bind(HTML) :
weakly(HTML, 'html')
)
)
)) :
('raw' in HTML ?
content('html') : hyper.wire
).apply(null, arguments);
}

もう読む気がでないですね
引数に渡す値によって色々中でやることが変わるのですが 何が起きるかわかりづらいしバグの元なのでこれは触れないことにしました
実際短く書けるだけなので使わなくても困らないと思います

ちなみに content, hyper.wire, weakly の処理はすべて hyper/wire.js にあります

wire

wire は部分的なテンプレートを作れます
これだけだと多分意味なくて bind に渡すテンプレートに含めます

一応文字列から HTML 要素化できるので

document.body.append(hyperHTML.wire()`<div></div>`)

はできるけど この用途にしては必要ないことを内部で色々やってるので 気持ち的にはこういうのを自分で作って使いたいです

ただの wire は毎回作られる

const f1 = () => hyperHTML.wire()`<div></div>`

console.log(f1())
// <div></div>
console.log(f1() === f1())
// false

毎回作り直すので同じテンプレートでも別物です

オブジェクトを渡すとキーになる

const f2 = (x) => hyperHTML.wire(x)`<div></div>`

console.log(f2(window))
// <div></div>
console.log(f2(window) === f2(window))
// true

引数が同じオブジェクトなら同じ参照が返ってきます
なので一度変更したら それ以降のものも変更されています

f2(window).append("a", "b")
console.log(f2(window))
// <div>ab</div>

f2(window) で返ってくる値は ab がついたものになってます

type

ここまでならわかりやすかったのですが id というのもあります

第二引数に

hyperHTML.wire(x, ":id")`<div></div>`

のように指定します
コロンがあるのは html, svg の指定するタイプと同じ引数にしているからです
分ければいいのにとしか思えませんがなんかこうなってます

html か svg で内部処理が変わるみたいで wire は作られる段階ではどの要素が親になるかわからないのでここで指定が必要みたいです
デフォルトは html なので svg を使うとき以外は気にしなくて良いものです
あえて指定するとこうなります

hyperHTML.wire(x, "html:id")`<div></div>`

id

この id ですが同じオブジェクトをキーにしても複数のテンプレートを使い分けたいとき用らしいです
わざわざ id を指定するくらいなので 別テンプレートでも id が同じなら同じ値になるのかと思ったのですが

const f3 = (x) => hyperHTML.wire(x, ":id")`<p>p</p>`
const f4 = (x) => hyperHTML.wire(x, ":id")`<b>b</b>`

console.log(f3(window))
// <p>p</p>
console.log(f3(window) === f3(window))
// true
console.log(f4(window))
// <b>b</b>
console.log(f4(window) === f4(window))
// true
console.log(f3(window) === f4(window))
// false
// テンプレートが異なる場合は再作成される

テンプレートが違えば再作成されて同じテンプレートなら同じものが使われます
これだけだと id の必要性がわからなかったのですが 合間に別のテンプレートを作ると違いがありました

let tmp_f3 = f3(window)
f4(window)
console.log(tmp_f3 === f3(window))
// false

f3 のあとに f4 を作ってもう一度 f3 を作った時に f3 どうしでも別物です
それぞれに id を指定すると

const f5 = (x) => hyperHTML.wire(x, ":h1")`<h1>1</h1>`
const f6 = (x) => hyperHTML.wire(x, ":h2")`<h2>2</h2>`

let tmp_f5 = f5(window)
let tmp_f6 = f6(window)
console.log(tmp_f5 === f5(window))
// true
console.log(tmp_f6 === f6(window))
// true

合間に別のテンプレートを作っても同じものになります
ただこれって id を文字列で指定しなくても テンプレートストリング自体をキーにして自動で内部でやってくれれば良いことだと思います
なんでわざわざ自分でやらないといけないでしょうね

テンプレートストリングを受け取る前の

hyperHTML.wire(x, ":xx")

の時点で id が確定しますが それによるメリットも特に思いつきません
同じテンプレートだけど、異なるものにしたいときに id を変える というは思いついたのですがそういう場合って第一引数の方のオブジェクト変えれば十分な気がします

こういうよくわからなさがあるので lit-html の方がわかりやすくていいなと感じます

なにも指定ないと

id を指定しない場合は type の 「html」 が使われて常に同じ id となって f3, f4 と同じ結果になります

const f7 = (x) => hyperHTML.wire(x)`<h3>3</h3>`
const f8 = (x) => hyperHTML.wire(x)`<h4>4</h4>`

let tmp_f7 = f7(window)
let tmp_f8 = f8(window)
console.log(tmp_f7 === f7(window))
// false
console.log(tmp_f8 === f8(window))
// false

とりあえず同じ object に複数テンプレート使うなら id 必須です

属性

また lit-html のほうが良いと感じた部分が属性の種類を明示的に書けることです
lit-html だと 「.」 から始まるとプロパティの書き込みで 「@」 から始まるとリスナの設定で 「?」 から始まると属性の有無の設定で それ以外は通常の属性になります

hyperHTML だとイベントは onclick など HTML そのままで boolean 型属性はなくプロパティ依存で data という名前の属性だと JavaScript の値を設定できます

onclick などイベントがそのままなのは良いと思うのですが 属性の有無を指定できないのは不便です
hyperHTML ではプロパティが存在する属性名だと属性ではなくプロパティを書き換えます

function render(x){
hyperHTML.bind(document.body)`<div innerHTML=${x}></div>`
}
render("表示されます")
// <div>表示されます</div>

innerHTML 属性を変えるとプロパティが変わり子要素が変更されます

hidden や disabled に true/false を渡せばプロパティが書き換わるので hidden などの属性の有無も変わります
ただし プロパティが存在しないといけないので div に selected を付けたり disabled を付けたりすると

<div disabled="false"></div>

のように文字列として設定されます
カスタム要素で有無に意味のある属性を作るときはプロパティと対応させないといけません

また boolean 型以外のプロパティも定義済みでないといけなく自由にプロパティを設定できません
特別に data という属性にのみプロパティを設定できます

定義されてるもののみしかプロパティは更新できず それ以外は属性の更新になって 属性の有無が意味のある場合はプロパティとして設定できるようになっているべき というのは正しいですしちゃんとした作りにするならこれで問題ないといえばそうなのですが自由度がないというか 「.」 や 「?」 つければ好きなプロパティを好きに設定できる lit-html のほうが好みです

replace したいとき

カスタム要素を作る場合 リスナなども含めて完全に初期化するというのがけっこう大変で そういう機能は作らず初期化するなら新規要素を作って replace するという作りにしてるものがあります
一番簡単で完璧に初期化できる方法ですからね

例えばこういうのです

customElements.define(
"my-elem",
class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
}

set text(value) {
const p = document.createElement("p")
p.textContent = value
this.shadowRoot.append(p)
}
}
)

function render(text) {
bind(document.body)`
<div>
<h1>header</h1>
<my-elem text=${text}></my-elem>
</div>
`
}

render("default")

function refresh(){
const current = document.querySelector("my-elem")
current.replaceWith(document.createElement("my-elem"))
}

my-elem の text プロパティが更新されるごとにその文字を innerHTML に追加していきます
クリア機能はないので refresh 関数のように新しいのを作って置き換えます

refresh 関数の実行前は hyperHTML を使って text を置き換えると innerHTML が追加されていきますが refresh 関数の実行後には render 関数を呼び出しても効果がありません
たぶん DOM から外れた方の要素を更新しているのだと思います

hyperHTML に限らずこういう差分更新をするツールだとツール外で DOM を更新すると困る場合が多いです
作りによってはその要素への参照を管理しているので別のオブジェクトになると正しく動きません
毎回クエリセレクタで取得となると無駄な探索時間を取りますから 自身で管理する前提なら参照をずっと保持するのもありえます

一応毎回作り直すのなら参照されないのかと思ってやってみると

${
wire()`<my-elem attr=${attr}></my-elem>`
}

text が変更されても保持されず毎回変わりますし refresh 関数を実行するとエラーが出るようになりました

DOM 操作したいとき

↑の replace 以外にも直接 DOM 操作で済ませたいときにはやっぱりちょっと不便です
要素をドラッグで並び替えたり validate 時の処理とかユーザが操作する系の部分のちょっとした処理くらいは DOM 上で済ませて確定ボタン押されてから更新したいです

こういう DOM を管理するツールの管理外におきたいところはコンポーネント化してしまうのが簡単です
例えば replace だと

customElements.define(
"instant-element",
class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
}

connectedCallback() {
this.init()
}

get component() {
return this.getAttribute("component")
}

set component(value) {
this.setAttribute("component", value)
this.refresh()
}

set data(value) {
this.init()
for (const [k, v] of Object.entries(value)) {
this.shadowRoot.firstElementChild[k] = v
}
}

init() {
if (!this.shadowRoot.firstElementChild) {
this.refresh()
}
}

refresh(value) {
if (this.component) {
this.shadowRoot.innerHTML = `<${this.component}>`
}
}
}
)

function render(text) {
bind(document.body)`
<div>
<h1>header</h1>
<instant-element data="${{ text }}" component="my-elem"></instant-element>
</div>
`
}

render("default")

とすればできます
instant-element タグの component に replace で入れ替えたいコンポーネントを指定します

instant-element の内側は hyperHTML は関与しないので instant-element が内側を置き換えても問題ありません
instant-element では refresh メソッドを使うことで新しいものに置き換えるようにしています

これで部分的に DOM を直接触っても競合しないようにできますし この手のツールのデメリットもほぼ無いと思います

自作のどうしよう

ここまで調べるともう hyperHTML でいい気もしてきてせっかく作った自作のは使わないかもしれません
一日もせず消えていく悲しさです
まあ色々調べるきっかけになったので良しとしましょう