Open8

Beako.jsにRefsをどのように導入するかの検討

Vueの$refs

Vueの$refsはこんな感じアクセスしますね。非常に簡単です。

<canvas ref='target'></canvas>
Vue2
this.$refs.target
Vue3 Composition API
const target = ref(null)

最初Vue同じ感じにしようと思ったのですが、この$refsにはずいぶん悩まされていたのを思い出しました。Vue2までの話です。

  1. いつ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
  }
})
  1. リアクティブではない

こんなことが起こります。
【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であればキャッチできました。

ログインするとコメントできます