Ref Element of Component in Vue.js

The Problem

In Vue.js we use ref to get the real DOM element of a node, like:

1
2
3
4
5
const dom = ref()
// Output: a <div> element
watch(dom, (v) => console.log(v))

<div ref="dom"></div>

However when the refed node is a custom component, we’ll get the component instance instead:

1
2
3
4
5
const dom = ref()
// Output: a component instance
watch(dom, (v) => console.log(v))

<Comp ref="dom"></Comp>

And we can use the $el property of the instance to get the underlying DOM element:

1
2
3
4
5
const dom = ref()
// Output: the root element of the component
watch(dom, (v) => console.log(v.$el))

<Comp ref="dom"></Comp>

So far so good, unless your component has a v-if directive on the root:

1
2
3
4
// Comp.vue
<template>
<div v-if="flag">...</div>
</template>

If flag is not truthy when the component is mounted, we will get a comment node by accessing the $el property:

1
2
// Output: a comment node like <!-- -->
watch(dom, (v) => console.log(v.$el))

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
2
3
4
// binding.value should be a ref
export const vRefElement = (el, binding) => {
binding.value.value = el;
};

And use it like:

1
2
const dom = ref()
<Comp v-ref-element="dom" />

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
2
3
4
5
const dom = ref()
const domWrap = { dom }
// Output(after flag becomes truthy): <div>
watch(dom, (v) => console.log(v))
<Comp v-ref-element="domWrap.dom" />

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

为什么我们需要 __esModule

ES Modules(ESM)是 ES2015 规范中定义的 JavaScript 模块格式,而早在此之前社区就已经探索出了诸如 CommonJS 和 AMD 之类的模块格式,其中影响力最大的就是被 Node.js 采用的 CommonJS。在 ESM 规范出现之前,JS 社区里的三方包几乎全部采用 CJS 模块格式编写,所以当 ESM 出现时,社区必须考虑的一个问题就是 ES Modules 如何与 CommonJS Modules 互操作(interoperate)。

…→

简单回顾一次技术分享

前两周在公司内部做了一次线上的技术分享,分享前做了很多准备工作,包括花了近两个工作日的时间收集资料和制作 Keynote,以及在组内试讲、收集反馈。虽然最后的效果还是不尽如人意,但也还能接受。后来回顾了一下当时的录屏,结合组内同学的反馈以及自己的体验,总结一下这次技术分享中的不足。

…→

个人任务管理工作流探索

最近一直在用 Microsoft To Do 记录自己要做的事,但是用多了之后越发觉得不爽,于是有了这篇文章。

现状

目前我每天用 To Do 和公司内部 Jira 管理自己要做的事。

…→

写在人生的一个小小节点

大约是四年前,我写了一篇文章,叫做《大学的十六分之一》。当时的我第一次离开家乡那座小城,独自来到北京读书;高中时对大学的憧憬和计划一点点地变得模糊,日常生活被懒惰和消极侵占,但又仍然保持一点清醒,所以才能写下那些文字。可惜的是后来再也没有类似的场景。前些日子从学校毕业的时候,看到别人的大学四年总结,我却想不到能写些什么。我不是一个好学生,也没做些有趣的事,甚至没能成为一个合格的人。

…→

重新开始记录

我在中学阶段曾两度写文章(也许用 post 更为合适)阐述“博客的意义”,却没想到会在大学时被自己遗忘殆尽。

…→