46

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

  • central event bus
  • hardcoded slides within a carousel
  • implement the next slide methods on page level and pass the current index to the control (as I'd have to do this every time I use the carousel)
Andrew France
  • 4,758
  • 1
  • 25
  • 27
Frnak
  • 6,601
  • 5
  • 34
  • 67

10 Answers10

67

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>
tony19
  • 125,647
  • 18
  • 229
  • 307
Decade Moon
  • 32,968
  • 8
  • 81
  • 101
  • are there any downsides to this - it seems to work very well – Frnak Jun 20 '18 at 08:13
  • Just a head-up, if you are using a wrapper component instead of a simple button like the example above, possibly, you will need a v-on="$listeners" to forward all event listeners. – mejiamanuel57 Mar 05 '20 at 21:23
  • 1
    @mejiamanuel57 If you need to have multiple listeners, you can pass a prop to the slot that contains an object with all event names as keys, and the functions they should run as values. For example: `:on="{ input: onInput, click: onClick}"`, with onXxxxx being methods in the wrapper component. You can then use scoped slots and the directive `v-on=` to assign the listeners and their respective handlers to the component in your slot (``). Vuetify also does this. https://stackoverflow.com/questions/55188478/meaning-of-v-slotactivator-on – Excalibaard Jul 27 '20 at 09:54
  • use vue Provide / Inject feature maybe another method https://vuejs.org/guide/components/provide-inject.html – Aborn Jiang Oct 29 '22 at 08:46
16

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

yukashima huksay
  • 5,834
  • 7
  • 45
  • 78
10

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.

Daniel Ramirez
  • 101
  • 1
  • 3
  • Great answer!!! Unfortunately the answer is so underrated here. Solved the problem without any complexity and the `EventListener` component is reusable for the same situation in the whole project. Thanks. – Rafik Farhad May 31 '21 at 20:22
6

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>
tony19
  • 125,647
  • 18
  • 229
  • 307
Syed
  • 15,657
  • 13
  • 120
  • 154
1

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.

Joery
  • 741
  • 7
  • 5
1

simple method

export default {
    computed: {
        defaultSlot() {
            return this.$scopedSlots.default();
        }
    },
    methods: {
       this.defaultSlot.forEach(vnode => {
           vnode.componentInstance.$on('someevent', (e) => {
              console.log(e)
           });
                
       });
    }
}
Ryan
  • 22,332
  • 31
  • 176
  • 357
user9547708
  • 357
  • 2
  • 5
0

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.

Harshal Patil
  • 17,838
  • 14
  • 60
  • 126
  • This seems to be quite inflexible as I cannot know which buttons are in the carousel (maybe there are other components with a lot of buttons that do different things). I'd need to provide constants or something that is attached to the button in order to identify what should happen - right? – Frnak Jun 20 '18 at 08:09
  • 1
    @FrankProvost, Unfortunately, that's how it works. We are using Vue.js for fairly large application from last one and half year. We came across this requirement very few times. If you don't like this, there are other ways but it won't have the free structure as you need. Also, consider injecting function as a prop or use some combination of scoped and multiple slots. – Harshal Patil Jun 20 '18 at 14:25
0

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.


Dharman
  • 30,962
  • 25
  • 85
  • 135
Mike C
  • 11
  • 4
0

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))
  • "...but vue3 does not have this option..." How does Vue3 inhibit you from using an event bus? The typical event bus setup occurs independent of Vue. Worst-case, you use window.event as an event bus; nothing to do with Vue. I've never used a plugin for an event bus. – Kalnode Mar 27 '22 at 14:04
0

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>

Riza Khan
  • 2,712
  • 4
  • 18
  • 42