Search code examples
vue.jsvuejs3vue-composition-apivue-slot

Get width of slot from useSlot()


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


Solution

  • 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>