0

I'm getting the width of the HTML element which is passed as slot like this:

<template>
    <div>
    <button @mouseover="state.show = true" @mouseleave="state.show = false">
      hover me
    </button>
    <div v-if="state.show">
        <slot name="test"></slot>      
    </div>
  </div>
</template>

<script setup>
import { reactive, watch, useSlots, nextTick } from 'vue'

const slots = useSlots();
const state = reactive({
  show: false
})  

watch(() => state.show, () => {
  if (state.show) {
    nextTick(() => {
      console.log(slots.test()[0].el.offsetWidth)
    })
  }
})
  
</script>

This print correct value only on first hover of button. When hovered more than one time, it logs 0. Why? How to get width of <slot name="test"></slot> every time I hover button?

Playground with above example

I also tried with getBoundingClientRect() on onUpdated vue hook which is executed after DOM changes:

<script setup>
import { reactive, watch, useSlots, nextTick, onUpdated } from 'vue'

const slots = useSlots();
const state = reactive({
  show: false
})  
onUpdated(() => {
    if (state.show && slots.test() && slots.test()[0] && slots.test()[0].el) {
              console.log(slots.test()[0].el.getBoundingClientRect())   
    }

})
</script>

With the same result - it shows correct width only on first hover. Playground with this example

Ken White
  • 123,280
  • 14
  • 225
  • 444
BT101
  • 3,666
  • 10
  • 41
  • 90

2 Answers2

1

It looks like a bug. I'd advise opening an issue about it on Vue's repo. 1

It's related to the fact you add/remove the element from DOM, which indicates slots.test() doesn't update its el property throughout the component lifecycle.

Replacing v-if with v-show seems to fix the problem.


Update: upon component creation, a vnode is created from the current contents of the slot. The vnode keeps the initial slot base element in el. Once contents are removed, el becomes null and is not re-populated with realtime slot contents.

Considering, the only solution seems to be not to replace the slot, as suggested above.

If we need to change the contents of the slot and want the ability to calculate its dimensions, we should use a wrapper which is there throughout the entire lifecycle of the component and only replace its contents, rather than the entire slot. It kind of defeats the purpose of the slot, which is designed to allow dynamic content, but I don't see any other workaround (other than not using slots at all, of course).


1 - according to Vue team, it's not a bug. I find that debatable, on general principles, as the same method seems to output inconsistent results. However, the answer clearly indicates the behavior is intended and unlikely to change.

tao
  • 82,996
  • 16
  • 114
  • 150
  • Seem like the slots do not update and reference the wrong element. – Duannx Sep 23 '22 at 02:43
  • I don't want v-show i need to use v-if – BT101 Sep 23 '22 at 07:24
  • No it's not a bug https://github.com/vuejs/core/issues/6723#event-7445639206 – BT101 Sep 23 '22 at 09:11
  • I don't want you to use `v-show`. I pointed out it solves the problem you've presented. If your real problem is different than the one you've presented, consider updating your question. If you still believe you need help solving it, that is. – tao Sep 23 '22 at 10:06
1

Thank you @tao for your input but the real solution is different. Changing between v-show and v-if is changing the actual logic of the script which I don't want - I want to completly remove element from DOM therefore I'll use v-if.

However as pointed by vue team this is usage issue not vue bug. In order to achieve what I wanted I need to use render function.

<template>
 <div>
    <button @mouseover="state.show = true" @mouseleave="state.show = false">
      hover me
    </button>
    <div v-if="state.show">
      <RenderTestSlot />      
    </div>
  </div>
</template>

<script setup>
import { reactive, watch, useSlots, nextTick, computed, h, Fragment } from 'vue'

const slots = useSlots();
const state = reactive({
  show: false
})  

const RenderTestSlot = computed(() => h(Fragment, slots.test ? slots.test() : []));

watch(() => state.show, () => {
  if (state.show) {
    nextTick(() => {
      console.log(RenderTestSlot.value.children[0].el.offsetWidth)
    })
  }
})
  
</script>
BT101
  • 3,666
  • 10
  • 41
  • 90
  • Interesting solution. However, it seems to disable the default slot behavior, which is to render its contents when the slot is not provided. If you place any content inside `` and remove ` – tao Sep 24 '22 at 01:29