Frnak
Frnak

Reputation: 6812

Emit event from content in slot to parent

I'm trying to build a flexible carousel control that allows inner content elements to force changing a slide, aswell as the carousel controls itself to change slides

A sample structure in my page looks like

<my-carousel>
  <div class="slide">
    <button @click="$emit('next')">Next</button>
  </div>

  <div class="slide">
    <button @click="$emit('close')">Close</button>
  </div>
</my-carousel>

The template for my carousel is like

<div class="carousel">
  <div class="slides" ref="slides">
    <slot></slot>
  </div> 
  <footer>
   <!-- other carousel controls like arrows, indicators etc go here -->
  </footer>
</div>

And script like

...
created() {
 this.$on('next', this.next)
}
...

Accessing the slides etc is no problem, however using $emit will not work and I can't seem to find a simple solution for this problem.

I want to component to be easily reusable without having to use

Upvotes: 57

Views: 66407

Answers (10)

yukashima huksay
yukashima huksay

Reputation: 6248

Just replace $emit('next') with $parent.$emit('next').

Please note that this solution is just a quick one-liner but is not recommended, using $parent to access the parent component can be considered an anti-pattern because it creates a tight coupling between child and parent components. The use of $parent makes the child component dependent on the structure of its parent, and any changes to the parent component can break the child component.

Here are a few reasons why using $parent is

  1. Brittleness: If the parent-child relationship changes (e.g., you restructure your components), using $parent might lead to unexpected behavior or errors. This makes your code more brittle and less maintainable.

  2. Reusability: Components that rely on their parent's structure are less reusable. Ideally, a child component should be able to function independently of its parent, making it more versatile and easier to use in different contexts.

  3. Readability and Maintainability: Code that uses $parent can be harder to read and understand because it introduces hidden dependencies. This can make the codebase more challenging to maintain and debug.

Upvotes: 18

Riza Khan
Riza Khan

Reputation: 3158

I found a pretty clean way of doing it in Vue3:

Consider this base Modal component:

<template>
    <PrsButton v-bind="$attrs" @click="modal = true" />

    <q-dialog v-model="modal">
        <q-card :style="[size]">
            <q-card-section>
                <p class="text-h6">{{ title }}</p>
            </q-card-section>
            <q-card-section>
                <slot />
            </q-card-section>
            <q-card-actions align="right">
                <PrsButton
                    v-close-popup
                    outline
                    color="purple"
                    label="Cancel"
                />
                <slot name="actions" v-bind="{ closeModal }" />
            </q-card-actions>
        </q-card>
    </q-dialog>
</template>

<script setup>
import { ref, computed, defineProps } from "vue"

const modal = ref(false)
const props = defineProps({
    title: {
        type: String,
        default: "",
    },
    size: {
        type: String,
        default: "medium",
        validator(val) {
            return ["small", "medium", "large"].includes(val)
        },
    },
})

const size = computed(() => {
    let style = {}
    switch (props.size) {
        case "small":
            style = { width: "300px" }
            break
        case "medium":
            style = { width: "700px", "max-width": "80vw" }
            break
        case "large":
            style = { "min-width": "95vw" }
            break
    }

    return style
})

const closeModal = () => {
    modal.value = false
}
</script>

<script>
export default {
    inheritAttrs: false,
}
</script>

In the named slot actions, I've used v-bind to pass a method called closeModal which I receive in the child like this:

<template>
    <PrsModal size="medium" title="Edit Location">
        <PrsRow>
            <PrsInput />
        </PrsRow>
        <template #actions="item">
            <PrsButton label="Save Changes" @click="doSomething(item)" />
        </template>
    </PrsModal>
</template>

<script setup>
const doSomething = (item) => {
    const { closeModal } = item
    closeModal()
}
</script>

Upvotes: 2

Syed
Syed

Reputation: 16543

Check scoped slot. Assuming your carousel component has fnNext and fnClose methods:

Carousel template:

<div class="carousel">
  <div class="slides" ref="slides">
    <slot name="slide-ctrls" :events="{ fnNext, fnClose }"></slot>
  </div> 
  <footer>
    <!-- Other carousel controls like arrows, indicators etc go here -->
  </footer>
</div>

Carousel example usage:

<my-carousel>
  <template slot="slide-ctrls" slot-scope="{ events: { fnNext, fnClose } }">
    <div class="slide">
      <button @click="fnNext">Next</button>
    </div>

    <div class="slide">
      <button @click="fnClose">Close</button>
    </div>
  </template>
</my-carousel>

OR, use v-slot (much cleaner and latest way of doing things):

<my-carousel>
  <template v-slot:slide-ctrls="{ events: { fnNext, fnClose } }">
    <div class="slide">
      <button @click="fnNext">Next</button>
    </div>

    <div class="slide">
      <button @click="fnClose">Close</button>
    </div>
  </template>
</my-carousel>

Just in case if you like to see much expanded form of code instead of es6, though this seems bit confusing but this shows you where and how things are passed/used.

<div class="carousel">
  <div class="slides" ref="slides">
    <slot name="slide-ctrls" :events="{ atClickNext: fnNext, atClickClose: fnClose }"></slot>
  </div> 
  <footer>
    <!-- Other carousel controls like arrows, indicators etc go here -->
  </footer>
</div>

Carousel example usage:

<my-carousel>
  <template v-slot:slide-ctrls="{ events: { atClickNext: handleClickNext, atClickClose: handleClickClose } }">
    <div class="slide">
      <button @click="handleClickNext">Next</button>
    </div>

    <div class="slide">
      <button @click="handleClickClose">Close</button>
    </div>
  </template>
</my-carousel>

Upvotes: 6

Decade Moon
Decade Moon

Reputation: 34306

Slots are compiled against the parent component scope, therefore events you emit from the slot will only be received by the component the template belongs to.

If you want interaction between the carousel and slides, you can use a scoped slot instead which allows you to expose data and methods from the carousel to the slot.

Assuming your carousel component has next and close methods:

Carousel template:

<div class="carousel">
  <div class="slides" ref="slides">
    <slot :next="next" :close="close"></slot>
  </div> 
  <footer>
    <!-- Other carousel controls like arrows, indicators etc go here -->
  </footer>
</div>

Carousel example usage:

<my-carousel v-slot="scope">
  <div class="slide">
    <button @click="scope.next">Next</button>
  </div>

  <div class="slide">
    <button @click="scope.close">Close</button>
  </div>
</my-carousel>

Upvotes: 81

kaveh zhian
kaveh zhian

Reputation: 21

if you using insertia solution is eventBus but vue3 does not have this option so you have to install external library such as mitt : https://github.com/developit/mitt the process is to rewrite app.js to use mitt globally... app.js:

import mitt from 'mitt';
const emitter = mitt();
createInertiaApp({
setup({ el, app, props, plugin }) {
const VueApp = createApp({ render: () => h(app, props) });
VueApp.config.globalProperties.emitter = emitter;
VueApp.use(plugin)
.use(emitter)
.mixin({ methods: { route } })
.mount(el);
},
});

then you can use it globally in child and parent even with persistent layout and having slot because emmiter dont care about regular emit and event it use this feature globally without relation between components after all for example in any component(ex child component):

this.emitter.emit('foo', { count: number })

in any component(ex parent component):

this.emitter.on('foo', e => console.log('foo', e))

Upvotes: 0

Mike C
Mike C

Reputation: 31

I know this is an older post, however, it is ranking well on Google - so thought I would detail the workaround that I found. If there is a better way of achieving this, I would welcome the feedback.

In an attempt to explain the solution, I will use a calendar example...


Here is my scenario

A generic calendar -> calendar-year -> calendar-month -> calendar-day

Within calendar-day, there is a slot (calendar-day-cell) allowing a parent to present a custom view of the day. This slot is passed up the line to the parent 'calendar' component.

Within my scenario, I have an availability-calendar that uses 'calendar', and overrides the calendar-day-cell passing in a component availability-calendar-day.

The availability-calendar-day emits "available-date-selected" and in this case, the 'calendar' is not required to know of this event. Within the stack, only the availability-calendar component needs to consume this.

Template:

<template> <!-- availability-calendar -->
  <calendar> 
    <template #calendar-day-cell>
      <availability-calendar-day @available-date-selected="dateSelected">

Script:

{
  name: 'availability-calendar',
  methods:
  {
    dateSelected(date) {
      // ...
    }

The problem

The emit from availability-calendar-day was not reaching availability-calendar. This is because it was not replicated up the 'calendar' stack. I.e. the emit was only emitting to the 'calendar-day' component (that defines the slot).

The solution

This is not a purist solution, however, it did work for me and I welcome any other comments for a workaround.

Given that components defined within a slot template accept props from the declaring component, I bypassed the event process altogether and passed the desired method into the component as a function.

Using the previous example, the template now looks like this:

<template> <!-- availability-calendar -->
  <calendar> 
    <template #calendar-day-cell>
      <availability-calendar-day :dateSelectedHandler="dateSelected">

Then, within 'availability-calendar-day', the method was changed from this.$emit('available-date-selected') to this.dateSelectedHandler(). To support this within a Typescript compiled component, the prop was typed as a Function.


Upvotes: 0

user9547708
user9547708

Reputation: 397

simple method

export default {
    computed: {
        defaultSlot() {
            return this.$scopedSlots.default();
        }
    },
    methods: {
       this.defaultSlot.forEach(vnode => {
           vnode.componentInstance.$on('someevent', (e) => {
              console.log(e)
           });
                
       });
    }
}

Upvotes: 1

Joery
Joery

Reputation: 781

I found out this can be done using $root.

<h1>Regular html document content</h1>
<parent-component>
  <h2>Some parent html that goes inside the slot</h2>
  <child-component></child-component>
</parent-component>

parent component:

<template>
    <div>
        <slot></slot>
        <h3>extra html that is displayed</h3>
    </div>
</template>
<script>
export default {

    created() {
        this.$root.$on('child-event', this.reactOnChildEvent);
    },

    methods: {
        this.reactOnChildEvent: function(message) {
            console.log(message);
        }
    }
};
</script>

child component:

<template>
    <div>
      <button @click="$root.$emit('child-event', 'hello world')">
         click here
      </button>
    </div>
</template>

However, if possible, used scoped slot as mentionned above.

Upvotes: 1

Daniel Ramirez
Daniel Ramirez

Reputation: 131

My Solution

Just create an event listener component (e.g. "EventListener") and all it does is render the default slot like so:

EventListener.vue

export default {
    name: 'EventListener'
    render() {
        return this.$slots.default;
    }
}

Now use this <event-listener> component and wrap it on your <slot>. Child components inside the slot should emit events to the parent like so: this.$parent.$emit('myevent').

Attach your custom events to the <event-listener @myevent="handleEvent"> component.

Carousel template:

<div class="carousel">
  <event-listener @next="handleNext" @close="handleClose">
     <div class="slides" ref="slides">
       <slot></slot>
     </div> 
  </event-listener>
  <footer>
   <!-- other carousel controls like arrows, indicators etc go here -->
  </footer>
</div>

Carousel example:

<my-carousel>
  <div class="slide">
    <button @click="$parent.$emit('next')">Next</button>
  </div>

  </div class="slide">
    <button @click="$parent.$emit('close')">Close</button>
  </div>
</my-carousel>

Note: The <event-listener> component must only have one child vnode. It cannot be the <slot>, so we just wrapped it on the div instead.

Upvotes: 13

Harshal Patil
Harshal Patil

Reputation: 21030

It is not possible to listen to events emitted from the slot content by the contained component. In your case, <my-carousel> cannot listen to events next and close. Slot contents are compiled against parent component scope.

As a workaround you can do this:

<div class="carousel">
    <!-- Listen to click event here -->
    <div class="slides" @click="doSomething($event)" ref="slides">
        <slot></slot>
    </div> 
    <footer>
        <!-- other carousel controls like arrows, indicators etc go here -->
    </footer>
</div>

And inside doSomething you can find which button was clicked by using $event.target. Read more about this issue at https://github.com/vuejs/vue/issues/4332 and https://github.com/vuejs/vue/issues/4781

There is one more advanced way of doing this and that is writing custom render function. You wrap click handler passed by a parent into carousel render function and pass a new function to the slot content. But it is something to be done extremely rarely and would consider it close to an anti-pattern.

Upvotes: 0

Related Questions