hyperHTML 使ってみました
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ 自作の作ってると色々気になって調べた結果 使い方がわかってきたのでこっちでいいかもしれない
◆ でも lit-html がでたらそっちにいきたい
◆ でも lit-html がでたらそっちにいきたい
前記事では hyperHTML のドキュメントやサンプルみても使い方わかりづらいし 複雑そうで使ってて困りそうだから自分が必要な機能だけのそれっぽいものを作ったのですが いざ作ってみると lit-html や hyperHTML はどうなってるだろうとか思うことが多くて結局色々動かしたりソースコード見たりすることになってました
いったい何のためにつくったんだろうね
何をどうすれば動いてどういう仕組みになってるのかがわかりづらいです
lit-html だとテンプレートを作る機能と DOM を更新する機能が分かれています
テンプレートを作って それとそれを表示する親要素を設定することで画面を更新します
hyperHTML だと先に bind を使ってどこに表示するかを決めてしまい bind で受け取る関数をタグとしてテンプレートストリングを使えば DOM が更新されます
これで DOM が変わるというのが直感的じゃないと言うかわかりにくさの元でした
というのもタグ呼び出しって String.raw などのように何かを取得するためのものって印象が強くて実際には呼び出しているものの getter みたいなもので関数を実行するイメージがあまりないです
なので副作用はなくただ値を取得できるものと感じてしまうのにこれで DOM が変わるというのが気持ち悪い部分です
なのでそこを lit-html 風にしてしまうとけっこうわかりやすくなります
render の中のコードをみると apply にしたくなりますが この bind は独自のメソッドで 外部公開されていない render 関数の render.bind を中で呼び出しています
なので apply 化はできません
これらの関数を使って
とできます
テンプレートの書き方 (属性名の規則) やリピートの方法などは hyperHTML のままです
それでも lit-html の example 程度ならそのまま動きます
hyperHTML.wire や hyperHTML.bind でそれぞれの機能が使えますが hyperHTML 関数自体が色々な書き方ができる関数となっていて定義がこうです
もう読む気がでないですね
引数に渡す値によって色々中でやることが変わるのですが 何が起きるかわかりづらいしバグの元なのでこれは触れないことにしました
実際短く書けるだけなので使わなくても困らないと思います
ちなみに content, hyper.wire, weakly の処理はすべて hyper/wire.js にあります
これだけだと多分意味なくて bind に渡すテンプレートに含めます
一応文字列から HTML 要素化できるので
はできるけど この用途にしては必要ないことを内部で色々やってるので 気持ち的にはこういうのを自分で作って使いたいです
毎回作り直すので同じテンプレートでも別物です
引数が同じオブジェクトなら同じ参照が返ってきます
なので一度変更したら それ以降のものも変更されています
f2(window) で返ってくる値は ab がついたものになってます
第二引数に
のように指定します
コロンがあるのは html, svg の指定するタイプと同じ引数にしているからです
分ければいいのにとしか思えませんがなんかこうなってます
html か svg で内部処理が変わるみたいで wire は作られる段階ではどの要素が親になるかわからないのでここで指定が必要みたいです
デフォルトは html なので svg を使うとき以外は気にしなくて良いものです
あえて指定するとこうなります
わざわざ id を指定するくらいなので 別テンプレートでも id が同じなら同じ値になるのかと思ったのですが
テンプレートが違えば再作成されて同じテンプレートなら同じものが使われます
これだけだと id の必要性がわからなかったのですが 合間に別のテンプレートを作ると違いがありました
f3 のあとに f4 を作ってもう一度 f3 を作った時に f3 どうしでも別物です
それぞれに id を指定すると
合間に別のテンプレートを作っても同じものになります
ただこれって id を文字列で指定しなくても テンプレートストリング自体をキーにして自動で内部でやってくれれば良いことだと思います
なんでわざわざ自分でやらないといけないでしょうね
テンプレートストリングを受け取る前の
の時点で id が確定しますが それによるメリットも特に思いつきません
同じテンプレートだけど、異なるものにしたいときに id を変える というは思いついたのですがそういう場合って第一引数の方のオブジェクト変えれば十分な気がします
こういうよくわからなさがあるので lit-html の方がわかりやすくていいなと感じます
とりあえず同じ object に複数テンプレート使うなら id 必須です
lit-html だと 「.」 から始まるとプロパティの書き込みで 「@」 から始まるとリスナの設定で 「?」 から始まると属性の有無の設定で それ以外は通常の属性になります
hyperHTML だとイベントは onclick など HTML そのままで boolean 型属性はなくプロパティ依存で data という名前の属性だと JavaScript の値を設定できます
onclick などイベントがそのままなのは良いと思うのですが 属性の有無を指定できないのは不便です
hyperHTML ではプロパティが存在する属性名だと属性ではなくプロパティを書き換えます
innerHTML 属性を変えるとプロパティが変わり子要素が変更されます
hidden や disabled に true/false を渡せばプロパティが書き換わるので hidden などの属性の有無も変わります
ただし プロパティが存在しないといけないので div に selected を付けたり disabled を付けたりすると
のように文字列として設定されます
カスタム要素で有無に意味のある属性を作るときはプロパティと対応させないといけません
また boolean 型以外のプロパティも定義済みでないといけなく自由にプロパティを設定できません
特別に data という属性にのみプロパティを設定できます
定義されてるもののみしかプロパティは更新できず それ以外は属性の更新になって 属性の有無が意味のある場合はプロパティとして設定できるようになっているべき というのは正しいですしちゃんとした作りにするならこれで問題ないといえばそうなのですが自由度がないというか 「.」 や 「?」 つければ好きなプロパティを好きに設定できる lit-html のほうが好みです
一番簡単で完璧に初期化できる方法ですからね
例えばこういうのです
my-elem の text プロパティが更新されるごとにその文字を innerHTML に追加していきます
クリア機能はないので refresh 関数のように新しいのを作って置き換えます
refresh 関数の実行前は hyperHTML を使って text を置き換えると innerHTML が追加されていきますが refresh 関数の実行後には render 関数を呼び出しても効果がありません
たぶん DOM から外れた方の要素を更新しているのだと思います
hyperHTML に限らずこういう差分更新をするツールだとツール外で DOM を更新すると困る場合が多いです
作りによってはその要素への参照を管理しているので別のオブジェクトになると正しく動きません
毎回クエリセレクタで取得となると無駄な探索時間を取りますから 自身で管理する前提なら参照をずっと保持するのもありえます
一応毎回作り直すのなら参照されないのかと思ってやってみると
text が変更されても保持されず毎回変わりますし refresh 関数を実行するとエラーが出るようになりました
要素をドラッグで並び替えたり validate 時の処理とかユーザが操作する系の部分のちょっとした処理くらいは DOM 上で済ませて確定ボタン押されてから更新したいです
こういう DOM を管理するツールの管理外におきたいところはコンポーネント化してしまうのが簡単です
例えば replace だと
とすればできます
instant-element タグの component に replace で入れ替えたいコンポーネントを指定します
instant-element の内側は hyperHTML は関与しないので instant-element が内側を置き換えても問題ありません
instant-element では refresh メソッドを使うことで新しいものに置き換えるようにしています
これで部分的に DOM を直接触っても競合しないようにできますし この手のツールのデメリットもほぼ無いと思います
一日もせず消えていく悲しさです
まあ色々調べるきっかけになったので良しとしましょう
いったい何のためにつくったんだろうね
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 でいい気もしてきてせっかく作った自作のは使わないかもしれません一日もせず消えていく悲しさです
まあ色々調べるきっかけになったので良しとしましょう