Search code examples
typescriptvue.jsvuejs3vue-componentvue-composition-api

How to call the methods exposed by components in slots with Vue 3 Composition API


There are some components (child component) in a slot (in parent component) and I want to call the methods on them. I tried the following steps:

  • Retrieve the child components as objects with useSlots
  • Expose the method to be called in child component with defineExpose
  • Call the method on the child component objects.

However, the method is not present on the object as expected.

Example:

// Parent.vue

<template>
    <div><slot/></div>
</template>

<script lang="ts" setup>
const slots = useSlots() as any;
const children = slots.default();

function callMethods() {
    console.log(children);
}
</script>
// SomeComponent.vue
<template>
    // ...
</template>

<script lang="ts" setup>
// ...

function methodToBeCalledByParent() {
    console.log(123);
}

defineExpose({
    methodToBeCalledByParent
});
</script>

And at some place other than above two I put this code:

<parent>
    <some-component/>
    <some-component/>
</parent>

Goal: Enable callMethods to call methodToBeCalledByParent in some-component respectively through children object.

When callMethods is called, there should be an array of two some-component printed in the console. While the number is correct and the basic structure of the component is also present, the content of each children doesn't seem right. There are so many nulls (maybe this is expected?) and most importantly the exposed methodToBeCalledByParent cannot be found in the object.

output

I've searched through the Internet and found that the method can be called directly once you retrieved the component object. So I'm confused. There must be something wrong with my code. Could anyone tell me the right (or better) way to do this?


Solution

  • Component instances may be available on vnode objects but this is internal data that isn't supposed to be used in production.

    This requires to add template refs to vnodes from the slot, this can be done in render function:

      setup() {
        const slots = useSlots();
        const slotRefs = reactive([]);
    
        return () => {
          const vnodes = slots.default?.() || [];
          slotRefs.length = vnodes.length;
    
          return vnodes.map((vnode, index) => 
            cloneVNode(vnode, { ref: (el) => slotRefs[index] = el })
          )
        }
      }
    

    This is a simplified version of the case when children need to be conditionally processed.

    The approach is similar in React, where the concept of Vue render function was borrowed from.