Reputation: 14666
I'm using VueJs 3 to create a dynamic table component, which involves sending down loop index variable to all child components inside a slot
the usage of the component as following
<template>
<h1>my table</h1>
<MyTable
:source="[
{ fname: 'Jhon', lname: 'Smith' },
{ fname: 'Arthur', lname: 'Cromer' },
]"
>
<MyTableColumn cellTitle="First Name" cellValue="fname"></MyTableColumn>
<MyTableColumn cellTitle="Last Name" cellValue="lname"></MyTableColumn>
</MyTable>
</template>
The above shows what i am trying to achieve in which you may set the table data source (array) and any number of columns where each column accepts a title and cell value.
I'm not sure how to send the iteration index (v-for index) down to all components within a slot.
here is the MyTable.vue template
<template>
<table>
<tr v-for="(row, index) in $props.source" :key="index">
{{
(currentRow = index)
}}
<slot></slot>
</tr>
</table>
</template>
I've tried scoped slots but did not help. and tried the provide/inject technique by providing the index from MyTable.vue and injecting it in each instance of MyTableColumn.vue, but this hasn't worked as expected because the injected variable always has the last index. not sure if it requires usage of render function.
I've have spent the last couple of days trying to find a solution with no luck.
here you can access the full code demo which has the unexpected behaviour
Upvotes: 5
Views: 10442
Reputation: 90013
Let's start with a simple slot example:
<template>
content which always renders
<slot name="slotName" :foo="'bar'">
content to be rendered if slot is not present
</slot>
some more content which always renders
</template>
Given the above slotted component, the following markup:
<Slotted>
<template #slotName="{foo}">
replacement content for slot - {{ foo }}
</template>
</Slotted>
would produce
content which always renders
replacement content for slot - bar
some more content which always renders
, while <Slotted />
would produce:
content which always renders
content to be rendered if slot is not present
some more content which always renders
Implementing the above into a table, we define the rendered columns as fields and define a slot for each field's cell and one for each field's header. They're dynamic.
To replace any column cell or column header, provide a template for that column's cells. Inside default content, nest a general slot for all cells (#cell
) and one for all headers (#header
). If provided, will be used for any cell (or header) which doesn't have a specified template. If not provided, default content.
In code:
Table.vue
<template>
<table>
<thead>
<tr>
<th v-for="{label, key} in fields" :key="key">
<slot :name="`header(${key})`"
v-bind="{ label, propName: key }">
<slot name="header">
{{ label }}
</slot>
</slot>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in rows" :key="index">
<td v-for="field in fields" :key="field.key">
<slot :name="`cell(${field.key})`"
v-bind="{
item,
index,
value: item[field.key],
field
}">
<slot name="cell"
v-bind="{
item,
index,
value: item[field.key],
field
}">
{{ item[field.key] }}
</slot>
</slot>
</td>
</tr>
</tbody>
</table>
</template>
Use it as:
<Table :rows="rows" :fields="fields">
<template #cell(fname)="{ value, index }">
<span style="color: red" v-text="`${index} - ${value}" />
</template>
<template #header(fname)="{ label }">
<span style="color: blue" v-text="label" />
</template>
<template #header="{ label }">
<!-- applies to all header cells which don't have
a #header({key}) template provided.
The ones which do (eg: `#header(fname)`) override this. -->
<span :style="{fontStyle: 'italic'}" v-text="label" />
</template>
</Table>
The beauty of this approach is that now you can use this <Table />
to render any items, without having to change the <Table />
component. You only have to provide the slots which should render something other than the default content.
Note: Inside each slot you have access to whatever was bound to the slot scope. In this case { item, index, value, field }
for cells and { label, key }
for header cells. Obviously, you're not limited to those. You can bind whatever you want to them.
Upvotes: 2
Reputation: 222334
The alternative to using scoped slots and associated boilerplate code is to add props to children in render function, this is done by modifying vnode objects, similarly to this answer.
MyTable
can have a limited set of components that are expected to be used as its children and may receive special treatment, e.g. MyTableColumn
.
() => {
const tableChildren = slots.default?.() || [];
const tableColumns = tableChildren.filter(vnode => vnode.type === MyTableColumn);
const tableHeaders = tableColumns.map(vnode => vnode.props.cellTitle);
return h('table', {}, [
h('tr', {}, [
...tableHeaders.map(header => h('td', {}, header)),
]),
...props.source.map((row, index) => {
return h('tr', {}, [
...tableColumns.map(vnode => {
const name = vnode.props.cellValue
return {
...vnode,
props: {
...vnode.props,
index,
value: row[name]
}
};
})
])
})
...
Here additional index
and value
props are added to MyTableColumn
vnodes prior to rendering.
This can be changed by making MyTableColumn
not being rendered at all, just providing necessary information about columns to MyTable
and optionally custom layout for them. This approach is more popular in React ecosystem, but can also be seen in Primevue DataTable, for instance. It can be seen that Column
components have no templates and so aren't rendered.
Upvotes: 2
Reputation: 138216
You can use scoped slots to pass data from MyTable.vue
to its slot children:
MyTable.vue
, bind the value of index
and row
to the <slot>
:<tr v-for="(row, index) in $props.source" :key="index">
<slot :index="index" :row="row"></slot>
</tr>
App.vue
, access MyTable.vue
's slot props via v-slot
on a <template>
, and bind them to the MyTableColumn
's props:<MyTable
:source="[
{ fname: 'Jhon', lname: 'Smith' },
{ fname: 'Arthur', lname: 'Cromer' },
]"
>
👇
<template v-slot="{ index, row }">
👇 👇
<MyTableColumn cellTitle="First Name" :index="index" :cellValue="row.fname"></MyTableColumn>
👇 👇
<MyTableColumn cellTitle="Last Name" :index="index" :cellValue="row.lname"></MyTableColumn>
</template>
</MyTable>
MyTable.vue
, add a headers
prop to contain an array of column titles, and render them above the table body:defineProps({
headers: Array,
})
<table>
<tr>
<td v-for="header in headers">{{ header }}</td>
</tr>
<!-- table body... -->
</table>
App.vue
, bind the desired column titles to <MyTable>.headers
:<MyTable :headers="['First Name', 'Last Name']" ⋯>
Upvotes: 10