Beako.jsにRefsをどのように導入するかの検討
Refsとは、仮想DOMを使って実際に描画されたDOMノードにアクセスする手段です。
Ref と DOM - React
子コンポーネントインスタンスと子要素へのアクセス - Vue
主に仮想DOM内の要素に他のライブラリの機能を導入するために使うかと思います。私はVueにおいて、グラフやQRコードを表示させるライブラリによく使っていました。
Vueの$refs
Vueの$refsはこんな感じアクセスしますね。非常に簡単です。
<canvas ref='target'></canvas>
this.$refs.target
const target = ref(null)
最初Vue同じ感じにしようと思ったのですが、この$refsにはずいぶん悩まされていたのを思い出しました。Vue2までの話です。
- いつDOMノードにアクセスできるようになるかはっきり分からない。
一番苦労したのはこれです。多くの解説サイトにmounted
が呼び出されたときにはアクセスできると書かれているのですが、実際にはそうと限りません。例えば次のように他のコンポーネントにラップされていて、コンポーネントを動的インポートで読み込んでいた場合、mounted
が呼び出されたときにはまだアクセスできないことがありました。
Vue.component('my-component, {
components: {
SubComponent: () => import('./sub_component.vue')
},
template: '<sub-component><canvas ref='target'></canvas></sub-component>',
mounted () {
this.$refs.target // undefined
}
})
- リアクティブではない
こんなことが起こります。
【Vue.js】v-forの中でrefを使用していて、要素を並べ替えたら、$refs配列の順番がずれた問題
Beako.js(Shadow DOM)の場合はVueの問題1はもっと深刻です。
<sub-component>
<canvas ref='target'></canvas>
</sub-component>
Shadow DOMを使ったカスタム要素でこのように書いていたとしても、canvas要素はシャドウツリーで覆い隠され、実際には画面に描画されません。
Beako.jsの場合はサブコンポーネント内のシャドウツリーの要素として扱われますので、このテンプレートを書いた親コンポーネントが描画されたときにはまだ描画されていませんし、アクセスするには、Element.shadowRootを介さなければなりません。
サブコンポーネント内要素にrefをつけるのはBadだよ、ということなのですが、アクセスできるように見えてアクセスできないというのは多くのトラブルを招きそうです。Vue風のアクセス手段にするのはNGかもしれません。
ちなみにslotを使っている場合は問題ありません。
<sub-component>
<canvas slot="header" ref='target'></canvas>
</sub-component>
<canvas ref='target'></canvas>
refは参照という意味なのでしょうが、この用途で使う名称としてふさわしいのでしょうか。
<canvas name='target'></canvas>
nameのほうが自然な気がします。ただ、name属性はHTMLに既に存在します。
<canvas id='target'></canvas>
idがそもそもこの用途ではないでしょうか。
Shadow DOMですので、nameもidも同じシャドウツリー内で被っていなければ基本的に問題ありません。
しかし、@expandで親コンポーネントから受け取ったテンプレートを展開するとき、そこにname、idが紛れ込むかもしれません。親コンポーネントが子コンポーネントにテンプレートを渡すとき、idは削除されるというのがよいのでしょうか。
Beako.jsではシャドウルートへアクセスできますので、現時点ですでにidやセレクタで要素を特定できます。ですがmountedイベントはまだありませんので、要素が存在しているかどうかは保証していません。
<!DOCTYPE html>
<meta charset="UTF-8">
<body></body>
<script type="module">
import { hack } from 'https://unpkg.com/beako@v0.9.12/beako.js'
hack(
document.body,
`
<div id="target"></div>
<button onclick="func()">Select</button>
`,
({ root }) => {
return {
func: () => console.log(root.getElementById('target'))
}
}
)
</script>
idは属性名からして、一つだけであることを保証していますので、for文の中で使ってはいけないことは明らかです。しかし、@for文の中の要素にアクセスしたいときもありますし、@if-@elseでidの要素を切り替えるなら、リアクティブでなければなりませんが、それも未対応です。やはり適していないようです。
仮想DOMのライブラリなので、仮想DOMをトレースする方法でを提供するのはどうでしょうか。気軽さが無くなりますが自由度が高いアクセスが可能になります。
({ on }) => {
const refs = watch({}, 'target', el => console.log(el))
on('patched', event => { // DOMが更新されたときに発火
trace(event.detail.tree, vNode => { // 仮想DOM内の要素をひとつづつ辿る
if (vNode?.props?.ref) { // refプロパティがあったら
refs[vNode.props.ref] = vNode.node
}
})
})
}
未検証ですがシャドウツリー内で発生したイベントもキャッチできれば、問題1も解決する可能性ありますね。
もしrefsオブジェクトだけを提供するなら、内部ではこういった実装になりそうです。
Shadow DOM内で発生したイベントもcomposed: true
であればキャッチできました。