Reputation: 6812
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
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
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.
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.
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
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
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
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
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
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
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
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
Reputation: 131
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.
<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>
<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
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