The Problem
In Vue.js we use ref
to get the real DOM element of a node, like:
1 | const dom = ref() |
However when the ref
ed node is a custom component, we’ll get the component instance instead:
1 | const dom = ref() |
And we can use the $el
property of the instance to get the underlying DOM element:
1 | const dom = ref() |
So far so good, unless your component has a v-if
directive on the root:
1 | // Comp.vue |
If flag
is not truthy when the component is mounted, we will get a comment node by accessing the $el
property:
1 | // Output: a comment node like <!-- --> |
Of course when flag
turns truthy the $el
property will be the rendered <div>
element. It’s all reasonable, except that the $el
property of a component instance is not reactive and it won’t trigger any watch
or computed
expressions!
So if you want to attach event listeners to the root element of a component (I know it sounds like bad-smell code but sometimes you have to do that) using pre-defined composables like useEventListener
, it may not work because it uses watch
under the hood. Basically we have no idea on the parent side when will the root element (with a v-if
) of a child component be rendered, unless you manually watch
the flag and emit a event from the child component.
The Solution
The solution is using custom directives. As mentioned in the doc, custom directive hooks will be passed the element the directive is bound to as an argument. And in fact, the directive hooks will only be triggered when there is a real HTML element, i.e. when the root element is actually rendered. So we can write a custom directive like this:
1 | // binding.value should be a ref |
And use it like:
1 | const dom = ref() |
Oops! An error:
1 | Uncaught (in promise) TypeError: Cannot set properties of undefined (setting 'value') |
The problem here is that Vue unwraps refs in template so the directive receives not the dom
ref itself but its inside value, which is undefined
! So to avoid this kind of unwrapping, we can wrap it in a plain object:
1 | const dom = ref() |
Now it works! The dom
ref will be filled with a <div>
when flag
becomes truthy and will trigger all relevant reactive computations.
See a demo here:
https://stackblitz.com/edit/vitejs-vite-3sqrri?file=src/App.vue