Search code examples
vue.jsvuejs-slots

How to call a function in a slot component from parent?


I have a component (Child) that has a slot. It also has a button that I want to trigger a function in the component in the slot (GrandChild). Is that possible? And how to achieve that? I have tried using ref on the slot, but without success. Also looked on using a scoped slot. I'm using vue3 with Typescript.

The Child does not know what component that should be in the slot, but maybe that could be sent as a prop.

<!-- Parent.vue -->

<script setup lang="ts">
    import Child from './TestSlotsChild.vue';
    import GrandChild from './TestSlotsGrandChild.vue';
</script>

<template>
    <div class="parent">
        <h2>I'm the parent</h2>
        <Child>
            <GrandChild/>
        </Child>
    </div>
</template>
<!-- Child.vue -->

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

<template>
    <div class="child">
        <h3>I'm the child</h3>
        <slot >
        </slot>
        <button>Call myMethod in GrandChild</button>
    </div>
</template>
<!-- GrandChild.vue -->

<script setup lang="ts">
    import { ref } from 'vue';

    const called = ref(false);

    function myMethod() {
        called.value = true;
        console.log('myMethod called in grandchild component');
    }
</script>

<template>
    <div class="grandchild">
        <h4>I'm the grandchild</h4>
        <p v-if="called">Parent called <em>myMethod</em> of grandchild.</p>
    </div>
</template>

Edit: An additional issue I ran into was when I wanted to make a for loop in the parent. Then I got the following error on myMethod: "Property 'myMethod' does not exist on type '(CreateComponentPublicInstanceWithMixins<ToResolvedProps<{}, {}>, { myMethod: () => void; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}> | null)[]'.ts-plugin(2339)"

Is there a solution for that?

I guess I must have unique refs for each element in the array, but couldn't really get it to work.

<!-- Parent.vue -->

<script setup lang="ts">
    import { useTemplateRef, computed } from 'vue';
    import Child from './TestSlotsChild.vue';
    import GrandChild from './TestSlotsGrandChild.vue';

    const gc = useTemplateRef('grandchild');

    const someArray = computed(() => [
        {childComponent: Child,         grandChildComponent: GrandChild},
        {childComponent: Child,         grandChildComponent: GrandChild},
    ]);
</script>

<template>
    <div class="parent">
        <h2>I'm the parent</h2>

        <div v-for="(element, index) in someArray" :key="index">
            <component
                :is=element.childComponent
                @click:btn="gc?.myMethod()"
            >
                <!-- <component :is="dialog.component" ref="dialogContets2"/> -->
                <component :is="element.grandChildComponent" ref="grandchild"/>
            </component>
        </div >
    </div>
</template>


Solution

  • Expose the method from the GrandChild component in order to get access to it using a ref :

    <script setup lang="ts">
    import { ref } from 'vue';
    
    const called = ref(false);
    
    function myMethod() {
      called.value = true;
      console.log('myMethod called in grandchild component');
    }
    
    defineExpose({
      myMethod //<--- expose it this way
    })
    </script>
    
    <template>
      <div class="grandchild">
        <h4>I'm the grandchild</h4>
        <p v-if="called">Parent called <em>myMethod</em> of grandchild.</p>
      </div>
    </template>
    
    

    Then you can emit an event from the Child component :

    <script setup lang="ts">
    
    const emit = defineEmits(['click:btn'])
    
    </script>
    
    <template>
        <div class="child">
            <h3>I'm the child</h3>
            <slot>
            </slot>
            <button @click="emit('click:btn')">Call myMethod in GrandChild</button>
        </div>
    </template>
    
    

    Finally add a ref on the GrandChild component and call its exposed method using the emitted event from the Child component :

    <script setup>
    import { useTemplateRef } from 'vue';
    import Child from './Child.vue';
    import GrandChild from './GrandChild.vue';
    
    const gc = useTemplateRef('grandchild')
    
    </script>
    
    <template>
      <div class="parent">
        <h2>I'm the parent</h2>
        <Child @click:btn="gc.myMethod()">
          <GrandChild ref="grandchild" />
        </Child>
      </div>
    </template>