user1190143
user1190143

Reputation:

Vue 3 Composition API - 'child' data in a tabs-like component

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.

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

Answers (1)

user10706046
user10706046

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

Related Questions