lit-html と Component 続き
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ コンポーネントにルート要素をもたせるようにした
◆ lit-html テンプレートを render するホスト要素
◆ lit-html テンプレートを render するホスト要素
前の記事の続きです
WebComponents が使えない状況でコンポーネントぽいことしたいけどどういう風に作るかって話です
前回のは関数もクラスも CustomElement にあたる要素はなしで ${} で埋め込むところに直接コンポーネントのルートが入るようになってました
ですが よくよく考えてみると CustomElement ならそこに要素があります
なので 要素を自分で作って良い気がしました
そうするとそれに対して lit-html の render を実行できます
トップレベルの 1 つの render で全部を行わないので this を個々に設定できたり document のツリーにアタッチされたりデタッチされたことを検出してそのタイミングで何かの処理をすることもできます
普通にやると 関数なので実行ごとに全部新規に作られるか 前の要素を使い回すなら全部同じになります
毎回別の要素になると DOM の入れ替え操作が多くなりますし 毎回作るコストも掛かります
全部同じだと同じコンポーネントを複数箇所で使えません
これは関数である以上仕方ないので ユーザが渡す引数を使って同じコンポーネントであることを判別することで対処します
hyperHTML の wire に近い感じです
コンポーネント関数に参照を渡して その参照に対して作ったルート要素を保存します
すでに参照に対する要素があったらそれに対して render を行いその要素を返します
store はオブジェクトで キーがコンポーネント名 バリューが WeakMap です
WeakMap にはユーザが渡したオブジェクトをキーとして バリューにルート要素を保存します
elemstore ではそれを管理します
コンポーネント名と参照オブジェクトを渡すとルート要素を取得し まだなければ作ります
最後のエクスポートしてる関数はコンポーネントとして使いやすいようにする関数です
lit-html の render 処理などはここでやるので 各コンポーネントのファイルはテンプレートを返す関数を作るだけで済みます
元々の関数のやり方で作ってたエクスポートする関数と近い関数を作るだけで済みます
コンポーネントを作る側はこういうフォーマットになります
コンポーネントを使う側はこういうフォーマットになります
ref を設定して得られる関数に data と update を入れて呼び出します
作られるコンポーネントのルート要素はこういう div になります
class に component-host がついて data-component にコンポーネント名が入ります
これが新しいやり方のファイルです
以前のはこれです
これが今回のファイルです
以前のはこれです
tab_data をなくして ref を使ってます
ref はそのコンポーネントを一意に判別するオブジェクトなので そのコンポーネント独自のデータを入れるオブジェクトとしても使えます
ref として active_tab.data を渡せばコンポーネントの判別兼データ保持場所としても使えて無駄がありません
今回のファイルはこれです
前のはこれです
関数内は最後のタブ内のコンポーネント表示部分が変わったくらいです
host として得られる div も操作できますし コンポーネント用のデータを保持したいときに div に対して WeakMap を作ったりしなくても最初から ref として対応するオブジェクトが渡されてます
コンポーネントがデータを持つことで コンポーネント感は増したものの クラスのやり方と同じようになって data にすべてのデータがあるわけじゃないので全体の状態を見るのはできなくなります
今回の例だと ref てして渡してるのが data の一部なので可能ですが関係ないものを渡すとできなくなります
ここまでクラスのやり方ぽくなるならクラスでやったほうが良い気もします
すでにインスタンスが存在するので一意にするための ref とかは不要です
ルート要素も WeakMap を使って保存したりしなくても プロパティに保存すればよいです
なのでほとんど base-class.js を変更するだけでした
参考用に元はこうでした
ルート要素を作る機能を追加してコンストラクタで作成します
また render メソッドは lit-html の render をルート要素に実行してその要素を返すようにします
元々はテンプレートを返していたのですが それは template getter に移動しました
あとルート要素に設定するためにコンポーネント名が必要になったので component_name getter も作りました
各コンポーネントは component_name getter をつくって render を get template に置き換えるだけで済みます
変更前
残りも同じように変更するだけです
と クラスの方は簡単に変更できました
ここまで来るとクラスのほうがシンプルなので良いかなと思えます
関数の方でもデータの持ち方がコンポーネントごとに持つようになってますし
WebComponents が使えない状況でコンポーネントぽいことしたいけどどういう風に作るかって話です
前回のは関数もクラスも CustomElement にあたる要素はなしで ${} で埋め込むところに直接コンポーネントのルートが入るようになってました
ですが よくよく考えてみると CustomElement ならそこに要素があります
なので 要素を自分で作って良い気がしました
そうするとそれに対して lit-html の render を実行できます
トップレベルの 1 つの render で全部を行わないので this を個々に設定できたり document のツリーにアタッチされたりデタッチされたことを検出してそのタイミングで何かの処理をすることもできます
関数版にコンポーネントのルート要素を作る
コンポーネントのルート要素を作ると言っても 関数の返り値で同じコンポーネントならば毎回同じ要素が返ってくる必要があります普通にやると 関数なので実行ごとに全部新規に作られるか 前の要素を使い回すなら全部同じになります
毎回別の要素になると DOM の入れ替え操作が多くなりますし 毎回作るコストも掛かります
全部同じだと同じコンポーネントを複数箇所で使えません
これは関数である以上仕方ないので ユーザが渡す引数を使って同じコンポーネントであることを判別することで対処します
hyperHTML の wire に近い感じです
コンポーネント関数に参照を渡して その参照に対して作ったルート要素を保存します
すでに参照に対する要素があったらそれに対して render を行いその要素を返します
component.js
こういうモジュールを作りますimport { render } from "https://unpkg.com/lit-html?module"
const store = {}
const elemstore = (component_name, ref) => {
const wmap = (store[component_name] = store[component_name] || new WeakMap())
if (!wmap.has(ref)) {
const element = document.createElement("div")
element.className = "component-host"
element.dataset.component = component_name
wmap.set(ref, element)
}
const element = wmap.get(ref)
return element
}
export default (component_name, template) => ref => (...args) => {
const host = elemstore(component_name, ref)
render(template(host, ref, ...args), host)
return host
}
store はオブジェクトで キーがコンポーネント名 バリューが WeakMap です
WeakMap にはユーザが渡したオブジェクトをキーとして バリューにルート要素を保存します
elemstore ではそれを管理します
コンポーネント名と参照オブジェクトを渡すとルート要素を取得し まだなければ作ります
最後のエクスポートしてる関数はコンポーネントとして使いやすいようにする関数です
lit-html の render 処理などはここでやるので 各コンポーネントのファイルはテンプレートを返す関数を作るだけで済みます
元々の関数のやり方で作ってたエクスポートする関数と近い関数を作るだけで済みます
コンポーネントを作る側はこういうフォーマットになります
export default component("component-name", (host, ref, data, update) => { /* return template */ })
コンポーネントを使う側はこういうフォーマットになります
html`
${tab1(ref)(data, update)}
`
ref を設定して得られる関数に data と update を入れて呼び出します
作られるコンポーネントのルート要素はこういう div になります
<div class="component-host" data-component="option">
class に component-host がついて data-component にコンポーネント名が入ります
tab1.js
シンプルな tab1.js で比較してみますこれが新しいやり方のファイルです
import { html } from "https://unpkg.com/lit-html?module"
import component from "./component.js"
export default component("tab1", (host, ref, data, update) => {
return html`
<div data-tab-name="tab1">
<h2>tab1</h2>
<p style=${`color: ${data.color}`}>${data.text}</p>
</div>
`
})
以前のはこれです
import { html } from "https://unpkg.com/lit-html?module"
export default (data, update, tab_data) => {
return html`
<div data-tab-name="tab1">
<h2>tab1</h2>
<p style=${`color: ${data.color}`}>${data.text}</p>
</div>
`
}
option.js
option.js も比べてみますこれが今回のファイルです
import { html } from "https://unpkg.com/lit-html?module"
import component from "./component.js"
export default component("option", (host, ref, data, update) => {
const editing = (ref.editing = ref.editing || {
title: data.title,
color: data.color,
text: data.text,
})
const h = {
input(eve) {
const name = eve.target.name
if (!name) return
editing[name] = eve.target.value
update()
},
save(eve) {
data.title = editing.title
data.color = editing.color
data.text = editing.text
update()
},
}
return html`
<div data-tab-name="option">
<h2>option</h2>
<table @input=${h.input}>
<tr>
<th>Title</th>
<td><input name="title" .value=${editing.title} /></td>
</tr>
<tr>
<th>Color</th>
<td><input name="color" .value=${editing.color} /></td>
</tr>
<tr>
<th>Text</th>
<td><input name="text" .value=${editing.text} /></td>
</tr>
</table>
<button @click=${h.save}>Save</button>
</div>
`
})
以前のはこれです
import { html } from "https://unpkg.com/lit-html?module"
export default (data, update, tab_data) => {
const editing = (tab_data.editing = tab_data.editing || {
title: data.title,
color: data.color,
text: data.text,
})
const h = {
input(eve) {
const name = eve.target.name
if (!name) return
editing[name] = eve.target.value
update()
},
save(eve) {
data.title = editing.title
data.color = editing.color
data.text = editing.text
update()
},
}
return html`
<div data-tab-name="option">
<h2>option</h2>
<table @input=${h.input}>
<tr>
<th>Title</th>
<td><input name="title" .value=${editing.title}></td>
</tr>
<tr>
<th>Color</th>
<td><input name="color" .value=${editing.color}></td>
</tr>
<tr>
<th>Text</th>
<td><input name="text" .value=${editing.text}></td>
</tr>
</table>
<button @click=${h.save}>Save</button>
</div>
`
}
tab_data をなくして ref を使ってます
ref はそのコンポーネントを一意に判別するオブジェクトなので そのコンポーネント独自のデータを入れるオブジェクトとしても使えます
ref として active_tab.data を渡せばコンポーネントの判別兼データ保持場所としても使えて無駄がありません
tab-container.js
コンポーネントを埋め込む側の処理として tab-container.js を比べてみます今回のファイルはこれです
import { html } from "https://unpkg.com/lit-html?module"
import tab1 from "./tab1.js"
import tab2 from "./tab2.js"
import option from "./option.js"
import component from "./component.js"
const tabs = { tab1, tab2, option }
export default component("tab-container", (host, ref, data, update) => {
if (!data.active_tab) {
data.active_tab = { name: "tab1", data: {} }
}
const h = {
switchTab(eve) {
const elem = eve.target.closest("[data-tab]")
data.active_tab = { name: elem.dataset.tab, data: {} }
update()
},
}
const tab_list = ["tab1", "tab2", "option"]
return html`
<div class="tab-container">
<div class="tab-head-container" @click=${h.switchTab}>
${tab_list.map(e => {
return html`
<div class=${`tab-head ${data.active_tab.name === e ? "active" : ""}`} data-tab=${e}>
${e}
</div>
`
})}
</div>
<div class="tab-body-container">
${tabs[data.active_tab.name](data.active_tab.data)(data, update)}
</div>
</div>
`
})
前のはこれです
import { html } from "https://unpkg.com/lit-html?module"
import tab1 from "./tab1.js"
import tab2 from "./tab2.js"
import option from "./option.js"
const tabs = { tab1, tab2, option }
export default (data, update) => {
if (!data.active_tab) {
data.active_tab = { name: "tab1", data: {} }
}
const h = {
switchTab(eve) {
const elem = eve.target.closest("[data-tab]")
data.active_tab = { name: elem.dataset.tab, data: {} }
update()
},
}
const tab_list = ["tab1", "tab2", "option"]
return html`
<div class="tab-container">
<div class="tab-head-container" @click=${h.switchTab}>
${tab_list.map(e => {
return html`
<div class=${`tab-head ${data.active_tab.name === e ? "active" : ""}`} data-tab=${e}>
${e}
</div>
`
})}
</div>
<div class="tab-body-container">
${tabs[data.active_tab.name](data, update, data.active_tab.data)}
</div>
</div>
`
}
関数内は最後のタブ内のコンポーネント表示部分が変わったくらいです
コンポーネントぽくなった
ルート要素が存在することで少しコンポーネント感が増しましたhost として得られる div も操作できますし コンポーネント用のデータを保持したいときに div に対して WeakMap を作ったりしなくても最初から ref として対応するオブジェクトが渡されてます
コンポーネントがデータを持つことで コンポーネント感は増したものの クラスのやり方と同じようになって data にすべてのデータがあるわけじゃないので全体の状態を見るのはできなくなります
今回の例だと ref てして渡してるのが data の一部なので可能ですが関係ないものを渡すとできなくなります
ここまでクラスのやり方ぽくなるならクラスでやったほうが良い気もします
クラス版にコンポーネントのルート要素を作る
やってみるとこっちはすごく簡単でしたすでにインスタンスが存在するので一意にするための ref とかは不要です
ルート要素も WeakMap を使って保存したりしなくても プロパティに保存すればよいです
なのでほとんど base-class.js を変更するだけでした
base-class.js
base-class.js の変更後はこうなりましたimport { html, render } from "https://unpkg.com/lit-html?module"
export default class BaseClass {
constructor(data, update) {
this.data = data
this.update = update
this.root = this.createRoot()
}
createRoot() {
const element = document.createElement("div")
element.className = "component-host"
element.dataset.component = this.component_name
return element
}
get component_name() {
return ""
}
get template() {
return html``
}
render() {
render(this.template, this.root)
return this.root
}
}
参考用に元はこうでした
import { html } from "https://unpkg.com/lit-html?module"
export default class BaseClass {
constructor(data, update) {
this.data = data
this.update = update
}
render() {
return html``
}
}
ルート要素を作る機能を追加してコンストラクタで作成します
また render メソッドは lit-html の render をルート要素に実行してその要素を返すようにします
元々はテンプレートを返していたのですが それは template getter に移動しました
あとルート要素に設定するためにコンポーネント名が必要になったので component_name getter も作りました
各コンポーネントは component_name getter をつくって render を get template に置き換えるだけで済みます
tab1.js
変更後import { html } from "https://unpkg.com/lit-html?module"
import BaseClass from "./base-class.js"
export default class extends BaseClass {
get component_name() {
return "tab1"
}
get template() {
return html`
<div data-tab-name="tab1">
<h2>tab1</h2>
<p style=${`color: ${this.data.color}`}>${this.data.text}</p>
</div>
`
}
}
変更前
import { html } from "https://unpkg.com/lit-html?module"
import BaseClass from "./base-class.js"
export default class extends BaseClass {
render() {
return html`
<div data-tab-name="tab1">
<h2>tab1</h2>
<p style=${`color: ${this.data.color}`}>${this.data.text}</p>
</div>
`
}
}
残りも同じように変更するだけです
と クラスの方は簡単に変更できました
ここまで来るとクラスのほうがシンプルなので良いかなと思えます
関数の方でもデータの持ち方がコンポーネントごとに持つようになってますし