Reputation:
I have a TabGroup/TabItem component for both Vue 3 and Vue 2, but it's written in the Options API.
I'm creating a new carousel component, which shares a lot of the features, but I'm trying to write it in the Composition API as that's how we now write everything in our projects.
I am essentialy trying to access data on the child components, but I don't see an obvious way of doing it. I want to be able to determine which of the children is active within the group component, so it can manage things like keyboard navigation and so on.
carousel-group
. Plus I don't want anyone to have to add a ref to every panel just to use the component.carousel-group
, because every panel can contain any content, and I want that to be as easy as possible to set up.For some background, the basic setup is this:
<carousel-group>
<carousel-panel>
Panel content
</carousel-panel>
</carousel-group>
As part of the setup process I can access the children, similar to what I would do in the Options API, and get a list of the child panels.
panels.value = slots.default().filter(child => child.type.name === 'carousel-panel');
Each carousel-panel
has instance data such as active
, which is what I want to access. But getting the panels via the default slot doesn't come with any of those properties.
Any ideas would be great, as I'm a bit stumped at the moment. I don't really want to have to do this using the Options API.
Upvotes: 3
Views: 2441
Reputation:
What you're trying to do is called "compound components". You need to use provide/inject (equivalent of React Context in Vue) and let carousel-group know that a carousel-panel has been put inside.
I do this with registerChild in a table component. I took this from my repo at https://github.com/sethidden/vue-compound-components/tree/master/example-table
<!-- MyTable.vue -->
<script setup>
import {ref, provide, defineProps } from 'vue'
const props = defineProps({
items: {
type: Array,
required: true,
}
})
const registerChild = (child) => registeredChildren.value.push(child);
const unregisterChild = (child) => {
registeredChildren.value =
registeredChildren.value.filter(
registeredChild => registeredChild !== child
);
};
const registeredChildren = ref([]);
provide('TheParent', { registerChild, unregisterChild })
</script>
<template>
columns: {{ registeredChildren.map(x=>x.itemPropertyName) }}
<table>
<thead>
<tr>
<th v-for="column in registeredChildren" :key="column">
<component :is="column.content" ref="column"/>
{{ column.sortable ? 'sortable' : ''}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="item, i in items" :key="i">
<td v-for="column, j in registeredChildren" :key="j">
{{ item[column.itemPropertyName] }}
</td>
</tr>
</tbody>
</table>
<!-- we want the mounted() lifecycle to fire on the components that are in the slot, but we dont want to show them -->
<div v-show="false">
<slot/>
</div>
</template>
<!-- MyTableColumn.vue -->
<script setup>
import {onMounted,onBeforeUnmount,inject, defineProps, h, render, useContext} from 'vue'
const props = defineProps({
itemPropertyName: {
type: String,
required: true,
},
sortable: {
type: Boolean,
default: false
}
})
const { registerChild, unregisterChild } = inject('TheParent');
const ctx = useContext();
const childInfo = {
itemPropertyName: props.itemPropertyName,
sortable: props.sortable,
// since ctx.slots.default is a render fn,
// we pass slot content from here to the parent component
// then render it in the parent using <component :is=""/>
content: ctx.slots.default
}
registerChild(childInfo);
onBeforeUnmount(() => {
unregisterChild(childInfo);
})
</script>
<template>
<div>
<slot/>
</div>
</template>
Obviously this is a table component, but you get the idea - you can track which panels are inside a carousel-group by registering them on mount, using some property from the parent thanks to inject()
Another thing is that you want to manage which carousel-panel is active. Which panel is active would obviously be kept in carousel-group, but you also need to toggle visibility of panels using v-for="panes in registeredChildren" v-if="activePane === pane.id"
, but that can be achieved by something like:
<carousel-group>
<carousel-pane id="1"/>
<carousel-pane id="2"/>
<carousel-pane id="3"/>
</carousel-group>
Then have the carousel-group's internal data property start with the first pane from registeredChildren then after 5s change to second and so on.
https://github.com/3nuc/vue-compound-components/blob/master/example-dropdown/Example.vue
Upvotes: 2