コンテキストというのは React のコンテキストみたいなもので 親で持っているデータのことです
WebComponents は DOM なので親をたどるのが簡単です
connectedCallback で接続されたときに
const shadow_root = this.getRootNode()
const host = shadow_root.host
のようにすれば ShadowRoot や その ShadowRoot を保持するホスト要素にアクセスできます
これを繰り返し期待のコンテキストを保持する要素を見つけて 見つかったらそこからデータを受け取るといいです
イメージ
const wmap = new WeakMap()
// コンテキストを識別するためのオブジェクトを作る
// WeakMap のキーにする
// 使わないけどデバッグ時のわかりやすさのために name を持たせておく
const createCtx = (name) => {
return { name }
}
// 要素にコンテキストデータをセットする
// CustomElements のコンストラクタで実行を想定
const setCtxData = (elem, ctx, data) => {
if (!wmap.has(elem)) {
wmap.set(elem, new WeakMap())
}
const ctxs = wmap.get(elem)
if (!ctxs.has(ctx)) {
ctxs.set(ctx, { value: null })
}
const container = ctxs.get(ctx)
container.value = data
}
// 一致するコンテキストを持つ祖先を探して
// 見つかればコンテキストのデータを取得する
const findCtxData = (elem, ctx) => {
const host = elem.getRootNode().host
if (!host) return null
const container = wmap.get(elem)?.get(ctx)
if (container) return container.value
return findCtxData(host)
}
これで良さそうかなと思ったのですが slot を使う場合に問題がありました
<foo-elem>
<bar-elem></bar-elem>
</foo-elem>
こういう構造の場合 bar-elem は foo-elem の ShadowDOM の中のどこかで slot を使って表示されます
この場合に foo-elem がコンテキストデータを持っていれば bar-elem は foo-elem からコンテキストデータを受け取りたいです
しかし getRootNode を使ってたどると bar-elem が最初に見るホスト要素は foo-elem と bar-elem の両方のホストです
foo-elem のように同じ ShadowDOM に属する祖先要素はスキップされてしまいます
これに対処するいい方法はないかなと探してると Lit ではイベントを使って親にリクエストを送り 親がコールバック関数を使って値を返す方法を使ってるようでした
その方針にしてみます
const setCtxData = (elem, ctx, data) => {
elem.addEventListener("ctx", event => {
if (event.detail.ctx === ctx) {
event.stopImmediatePropagation()
event.detail.callback(data)
}
})
}
const findCtxData = (elem, ctx) => {
let data
const event = new CustomEvent("ctx", {
detail: {
ctx,
callback: (d) => {
data = d
},
},
bubbles: true,
composed: true,
})
this.dispatchEvent(event)
return data
}
これで良さそうですが 親でコンテキストを更新したことに気づけません
コールバックで渡す値を EventTarget オブジェクトにして 受け取ったらそこにリスナをつけるなどでしょうか
class Context extends EventTarget {
_value = null
get value() { return this._value }
set value(v) {
this._value = v
this.dispatchEvent(new Event("change"))
}
constructor(value) {
super()
this.value = value
}
}
const setCtxData = (elem, ctx, data) => {
const ctx_data = new ContextData(data)
elem.addEventListener("ctx", event => {
if (event.detail.ctx === ctx) {
event.stopImmediatePropagation()
event.detail.callback(ctx_data)
}
})
return ctx_data
}
const findCtxData = (elem, ctx) => {
let ctx_data
const event = new CustomEvent("ctx", {
detail: {
ctx,
callback: (d) => {
ctx_data = d
},
},
bubbles: true,
composed: true,
})
elem.dispatchEvent(event)
return ctx_data
}
const subscribeExample = (elem, ctx) => {
const ctx_data = findCtxData(elem, ctx)
ctx_data.addEventListener("change", () => {
console.log("changed", ctx_data.value)
})
}
動く例
https://nexpr.gitlab.io/public-pages/webcomponents-context/example.html
foo-elem がコンテキストデータとして { num: 1 } を持っています
子孫の bar-elem は foo-elem のコンテキストデータを受け取り num を表示します
ボタンを押すと num の数値が 1 ずつ増えて bar-elem に反映されます