Vue-next: Mounted slot vnode.el is null

Created on 11 Mar 2020  路  6Comments  路  Source: vuejs/vue-next

Version

3.0.0-alpha.8

Reproduction link

https://codepen.io/jbruni/pen/GRJQZpb

Steps to reproduce

  • There is a console.log at line 11 of the JavaScript code. It outputs the default slot's el.
  • If you open the Console (either codepen's built-in or browser's dev tool), you'll see the output is null.
  • It shouldn't be null. This is the bug happening. No more steps are required to reproduce.

What is expected?

The output should be the mounted slot's vnode el - corresponding to the <div>Hi, there</div> element.

What is actually happening?

The mounted slot's vnode el is null, instead of the DOM element.


The issue is related to the dynamic <component> used as the slot content.

If you go to line 17 of the Javascript code, and change #parent to #parent2, you will see the output in Console changes to <div>See you</div>. This is an example of the expected result. In this case, the working #parent2 template contains a simple div as the slot content, rather than a dynamic <component>.

NOTE: I am using vue-next version 3.0.0-alpha.8, but this version is not available for selection in the Version dropdown of new-issue.vuejs.org

Most helpful comment

You can use the cloneVNode helper which accepts a second argument for extra props to be merged:

import { cloneVNode } from 'vue'

// in render fn
const slotContent = slots.default ? slots.default() : []
const transformedSlotContent = slotContent.map(vnode => cloneVNode(vnode, {
  onClick: () => { /* ... */ }
}))

However, I can imagine working on something like D&D can be a bit tough without free access to the DOM, so maybe wrapping slots with a wrapper element is still the easier way to do it.

All 6 comments

It looks like this other issue (closed automatically) is related: https://github.com/vuejs/vue-next/issues/814

Duplicate of vuejs/vue#9580

You can do this but I'm not sure if there's a built-in way to then render the vnode in the template, you'd have to use a render function.

const slot = computed(() => slots.default())
onMounted(() => console.log(slot[0].el))
return { slot }

This is expected, because calling slot functions technically always return fresh vnodes. The reason why #parent2 works is because the static slot content has been hoisted so it is returning the same (already mounted) vnode.

The idea here is that:

  1. You should not be relying on vnode.el in userland code. It's not documented public API and there is no guarantee about what the "expected behavior" is. The documented public API for accessing mounted DOM is via template refs.

  2. Maybe you shouldn't be messing with slot DOM nodes in the first place. Prefer transforming the slot vnodes at the render function level instead of attempting to mutate the end DOM imperatively.

  3. If you do have a use case that requires imperatively manipulating slot DOM nodes, what you can do is wrapping the slot with a div, and access that div (and its child nodes) via template ref. Still, I'd avoid this unless there is absolutely no better way around it.

Thanks, @yyx990803

What would be the recommended approach for adding a draggable behaviour which could be attached to any component?

For the consumer perspective:

  • a wrapper component, like <draggable><consumer-something /></draggable> ?
  • a custom directive, like <consumer-something v-draggable /> ?

My intention here, with the wrapper component approach, was to attach event listeners to the slot's mounted DOM. Draggable would be a "renderless", behaviour-only component.

Can I attach DOM event listeners by "_transforming the slot vnodes at the render function level_"?

If not, this means I must go by "_wrapping the slot with a div_"? Sounds like the solution for this "draggable" idea... :thinking: I don't see a better way around it.

Anyway, it will not be a "renderless" component anymore, because it will render this wrapping div.

I wonder if I really wanted a "renderless" solution... Something very trivial, like triggering an alert upon a mouseenter in the slot DOM element... Isn't this feasible?

(Well... "renderless" term may be misleading... because, after all, the component renders the slot within it...)

You can use mergeProps to add event listeners to a vnode.

You can use the cloneVNode helper which accepts a second argument for extra props to be merged:

import { cloneVNode } from 'vue'

// in render fn
const slotContent = slots.default ? slots.default() : []
const transformedSlotContent = slotContent.map(vnode => cloneVNode(vnode, {
  onClick: () => { /* ... */ }
}))

However, I can imagine working on something like D&D can be a bit tough without free access to the DOM, so maybe wrapping slots with a wrapper element is still the easier way to do it.

Was this page helpful?
0 / 5 - 0 ratings