Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/content/docs/2.components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ highlights:
When rendering components with `h`, you can either use the `resolveComponent` function or import from `#components`.
::

#### Grouped column cell classes :badge{label="Soon" class="align-text-top"}

Column groups can set `meta.class.td` to style all descendant cells. Functions receive the individual cell, and Tailwind utilities on nested groups and leaf columns take precedence over conflicting utilities from their parent groups.

### Meta

Use the `meta` prop as an object ([TableMeta](https://tanstack.com/table/v8/docs/api/core/table#meta)) to pass properties like:
Expand Down
25 changes: 23 additions & 2 deletions src/runtime/components/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ const appConfig = useAppConfig() as Table['AppConfig']
const data = createRef(props.data ?? [], props.watchOptions?.deep !== false)
// eslint-disable-next-line vue/no-dupe-keys
const meta = computed(() => props.meta ?? {})

const explicitGroupMeta = Symbol('explicitGroupMeta')
type ProcessedColumn = TableColumn<T> & { [explicitGroupMeta]?: true }

// TanStack resolves each column as `{ ...defaultColumn, ...columnDef }`, so any key set here shadows `defaultColumn`.
// The fallback `header` and `cell` are only injected when `defaultColumn` doesn't provide its own.
// eslint-disable-next-line vue/no-dupe-keys
Expand All @@ -272,10 +276,14 @@ const columns = computed<TableColumn<T>[]>(() => processColumns(props.columns ??

function processColumns(columns: TableColumn<T>[]): TableColumn<T>[] {
return columns.map((column) => {
const col = { ...column } as TableColumn<T>
const col: ProcessedColumn = { ...column }

if ('columns' in col && col.columns) {
col.columns = processColumns(col.columns as TableColumn<T>[])
// Record group metadata before TanStack fills in defaults.
if (col.meta) {
col[explicitGroupMeta] = true
}
}

if (!col.cell && !props.defaultColumn?.cell) {
Expand Down Expand Up @@ -608,6 +616,19 @@ function resolveValue<T, A = undefined>(prop: T | ((arg: A) => T), arg?: A): T |
return prop
}

function getCellClasses(cell: Cell<T, unknown>) {
const classes = [resolveValue(cell.column.columnDef.meta?.class?.td, cell)]

for (let column = cell.column.parent; column; column = column.parent) {
const columnDef = column.columnDef as ProcessedColumn
if (columnDef[explicitGroupMeta]) {
classes.unshift(resolveValue(columnDef.meta?.class?.td, cell))
}
}

return classes
}

function getColumnStyles(column: Column<T>): Record<string, string> {
const styles: Record<string, string> = {}

Expand Down Expand Up @@ -700,7 +721,7 @@ defineExpose({
:class="ui.td({
class: [
props.ui?.td,
resolveValue(cell.column.columnDef.meta?.class?.td, cell)
...getCellClasses(cell)
],
pinned: !!cell.column.getIsPinned()
})"
Expand Down
80 changes: 80 additions & 0 deletions test/components/Table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,63 @@ describe('Table', () => {
])
})

it('inherits cell classes from a column group', async () => {
const wrapper = await mountSuspended(Table<typeof data[number]>, {
props: {
data: data.slice(0, 1),
ui: { td: 'bg-default' },
columns: [{
accessorKey: 'id',
meta: { class: { td: 'font-bold' } }
}, {
header: 'Payment',
meta: { class: { td: 'bg-muted' }, colspan: { td: '2' }, rowspan: { td: '2' } },
columns: [{ accessorKey: 'email' }, { accessorKey: 'amount' }]
}]
}
})

const [id, ...paymentCells] = wrapper.findAll('tbody td')
expect(id!.classes()).toEqual(expect.arrayContaining(['bg-default', 'font-bold']))
expect(id!.classes()).not.toContain('bg-muted')
expect(paymentCells).toHaveLength(2)
for (const cell of paymentCells) {
expect(cell.classes()).toContain('bg-muted')
expect(cell.classes()).not.toContain('bg-default')
expect(cell.attributes('colspan')).toBeUndefined()
expect(cell.attributes('rowspan')).toBeUndefined()
}
})

it('merges dynamic cell classes from nested groups with child classes taking precedence', async () => {
const wrapper = await mountSuspended(Table<typeof data[number]>, {
props: {
data: [data[0]!, data[4]!],
columns: [{
header: 'Payment',
meta: { class: { td: ({ row }) => row.original.status === 'success' ? 'bg-default font-bold' : 'bg-default' } },
columns: [{
header: 'Details',
meta: { class: { td: 'bg-muted italic' } },
columns: [{ accessorKey: 'status' }, {
accessorKey: 'amount',
meta: { class: { td: 'bg-elevated' } }
}]
}]
}]
}
})

const [status, amount, failedStatus, failedAmount] = wrapper.findAll('tbody td')
expect(status!.classes()).toEqual(expect.arrayContaining(['bg-muted', 'font-bold', 'italic']))
expect(amount!.classes()).toEqual(expect.arrayContaining(['bg-elevated', 'font-bold', 'italic']))
expect(status!.classes()).not.toContain('bg-default')
expect(amount!.classes()).not.toContain('bg-default')
expect(amount!.classes()).not.toContain('bg-muted')
expect(failedStatus!.classes()).not.toContain('font-bold')
expect(failedAmount!.classes()).not.toContain('font-bold')
})

it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(Table, {
props: {
Expand Down Expand Up @@ -606,6 +663,29 @@ describe('Table', () => {

expect(wrapper.find('td').text()).toBe('own')
})

it.each([
{ name: 'applies default cell metadata only to leaves without their own metadata', explicitGroupMeta: false },
{ name: 'inherits explicit group metadata shared with defaultColumn', explicitGroupMeta: true }
])('$name', async ({ explicitGroupMeta }) => {
const meta = { class: { td: 'font-bold' } }
const wrapper = await mountSuspended(Table, {
props: {
data: [{ id: 1, amount: 10 }],
defaultColumn: { meta },
columns: [{
header: 'Payment',
...(explicitGroupMeta && { meta }),
columns: [{ accessorKey: 'id' }, { accessorKey: 'amount', meta: { class: { td: 'italic' } } }]
}]
}
})

const [id, amount] = wrapper.findAll('tbody td')
expect(id!.classes()).toContain('font-bold')
expect(amount!.classes()).toContain('italic')
expect(amount!.classes().includes('font-bold')).toBe(explicitGroupMeta)
})
})

it('reactive columns', async () => {
Expand Down
Loading