vue.jsvuejs3vue-composition-api

In Vue3 how to access a component's exposed bindings (via defineExpose) from a parent when the component was passed to a slot?


Lets say I have a Component.vue

<template>
<div>
    <div class="items">
        <slot name="items"></slot>
    </div>
    <div class="controller"></div>
        <slot name="controller></slot>
    </div>
</div>
</template>
<script lang="ts" setup>
// HERE I need to access the exposed properties of every component passed to the 'items' slot from the parent
</script>

and a ComponentChild.vue

<template>
<div ref="el" class="item">
   <slot></slot>
</div>
</template>
<script lang="ts" setup>
const el = ref<HTMLDivElement>()
const itemEl = ref<HTMLElement>()

onMounted(() => {
    itemEl.value = el.value!.children[0] as HTMLElement
})

defineExpose({
    itemElement: itemEl,
})
</script>

And here is the usage:

<template>
<div>
    <Component>
        <template #items>
            <ComponentChild v-for="item in items" :key="item.id">
                <div>The element I'm trying to access from Component exposed as itemElement in ComponentChild</div>
            </ComponentChild>
        </template>
        <template #controller>
            <div>...</div>
        </template>
    </Component>
</div>
</template>
<script lang="ts" setup>
import Component from "@/components/Component.vue"
import ComponentChild from "@/components/ComponentChild.vue"
</script>

The question is how to access that itemElement for each ComponentChild from Component.vue? Now I know this may not be the best way to access the element ref in this case but what if I want to expose something else (not an element ref) and want access it from Component. So I don't need alternatives.


Solution

  • This is done by using render function, this allows to modify the contents of a slot and includes adding the refs. The support for built-in directives (v-for, etc) needs to be explicitly provided as the structure of slot vnodes will be different:

    setup(props, { slots }) {
      const childInstances = shallowRef([]);
      let children = slots.items?.();
    
      if (children?.length === 1 && children[0].type === Fragment) {
        children = children[0].children;
      }
    
      return () => {
        return h(
          'div',
          { class: "items" },
          children?.map((vnode, index) => 
            h(vnode, { ref: instance => childInstances.value[index] = instance })
          )
        );
      };
    }
    

    As long as ComponentChild exposes nothing but root element, it may be unnecessary to use defineExpose; the element will be available as instance.$el.