I'm trying Laravel 12 with the new starter kit Vue and the shadcn-vue components. Here is my problem: I need a reactive datatable using Inertia. To achieve that reactivity I have to disable preserveState, however, by disabling it get lost the arrow icons indicating the active sort ordering on the header column table. If preserveState is enabled (true) then the arrow icons works fine but reactivity on data is lost despite the Inertia's request still works with the data right sorted.
Here is what I've done so far:
<script setup lang="ts">
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import AppLayout from '@/layouts/AppLayout.vue';
import IndexLayout from '@/layouts/IndexLayout.vue';
import { valueUpdater } from '@/lib/utils';
import { BreadcrumbItem, Can, Pagination, Permission } from '@/types';
import { Head, router } from '@inertiajs/vue3';
import {
ColumnDef,
ColumnFiltersState,
ExpandedState,
FlexRender,
getCoreRowModel,
getFilteredRowModel,
SortingState,
useVueTable,
} from '@tanstack/vue-table';
import { ChevronDown, ChevronsUpDown, ChevronUp } from 'lucide-vue-next';
import { h, ref } from 'vue';
import DropdownAction from './partials/DropdownAction.vue';
interface Props {
can: Can;
filters: object;
permissions: Pagination<Permission>;
}
const props = defineProps<Props>();
// const model = defineModel<Props>();
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Permisos',
href: '/dashboard',
},
];
const columns: ColumnDef<Permission>[] = [
{
id: 'select',
header: ({ table }) =>
h(Checkbox, {
modelValue: table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:modelValue': (value: any) => table.toggleAllPageRowsSelected(!!value),
ariaLabel: 'Select all',
}),
cell: ({ row }) =>
h(Checkbox, {
modelValue: row.getIsSelected(),
'onUpdate:modelValue': (value: any) => row.toggleSelected(!!value),
ariaLabel: 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'name',
header: ({ column, table }) => {
const isSorted = column.getIsSorted();
const isSortedDesc = column.getIsSorted() === 'desc';
return h(DropdownMenu, () => [
h(DropdownMenuTrigger, { asChild: true }, () => [
h(Button, { variant: 'outline', class: 'ml-auto' }, () => [
'Nombre',
isSorted
? isSortedDesc
? h(ChevronDown, { class: 'ml-2 h-4 w-4' })
: h(ChevronUp, { class: 'ml-2 h-4 w-4' })
: h(ChevronsUpDown, { class: 'ml-2 h-4 w-4' }),
]),
]),
h(DropdownMenuContent, { align: 'start' }, () => [
h(
DropdownMenuCheckboxItem,
{ key: `${column.id}Up`, checked: isSorted && !isSortedDesc, onSelect: () => column.toggleSorting(false) },
() => ['Ordenar ASC'],
),
h(
DropdownMenuCheckboxItem,
{ key: `${column.id}Down`, checked: isSorted && isSortedDesc, onSelect: () => column.toggleSorting(true) },
() => ['Ordenar DESC'],
),
h(DropdownMenuCheckboxItem, { key: `${column.id}Clr`, checked: false, onSelect: () => table.setSorting(() => <SortingState>([])) }, () => [
'Restablecer',
]),
]),
]);
},
cell: ({ row }) => h('div', row.getValue('name')),
},
{
accessorKey: 'description',
header: ({ column, table }) => {
const isSorted = column.getIsSorted();
const isSortedDesc = column.getIsSorted() === 'desc';
return h(DropdownMenu, () => [
h(DropdownMenuTrigger, { asChild: true }, () => [
h(Button, { variant: 'outline', class: 'ml-auto' }, () => [
'Descripción',
isSorted
? isSortedDesc
? h(ChevronDown, { class: 'ml-2 h-4 w-4' })
: h(ChevronUp, { class: 'ml-2 h-4 w-4' })
: h(ChevronsUpDown, { class: 'ml-2 h-4 w-4' }),
]),
]),
h(DropdownMenuContent, { align: 'start' }, () => [
h(
DropdownMenuCheckboxItem,
{ key: `${column.id}Up`, checked: isSorted && !isSortedDesc, onSelect: () => column.toggleSorting(false) },
() => ['Ordenar ASC'],
),
h(
DropdownMenuCheckboxItem,
{ key: `${column.id}Down`, checked: isSorted && isSortedDesc, onSelect: () => column.toggleSorting(true) },
() => ['Ordenar DESC'],
),
h(DropdownMenuCheckboxItem, { key: `${column.id}Clr`, checked: false, onSelect: () => table.setSorting(() => <SortingState>([])) }, () => [
'Restablecer',
]),
]),
]);
},
cell: ({ row }) => h('div', row.getValue('description')),
},
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const permission = row.original;
const can = props.can;
return h(DropdownAction, {
permission,
can,
onExpand: row.toggleExpanded,
});
},
},
];
const sorting = ref<SortingState>([]);
const columnFilters = ref<ColumnFiltersState>([]);
const globalFilter = ref('');
const rowSelection = ref({});
const expanded = ref<ExpandedState>({});
function handleSortingChange(item: any) {
if (typeof item === 'function') {
const sortValue = item();
const sortBy = sortValue[0]?.id ? sortValue[0].id : '' ;
const sortDirection = sortBy ? sortValue[0]?.desc ? 'desc' : 'asc' : '';
const data: {[index: string]: any} = {};
data[sortBy] = sortDirection;
router.visit(route('permissions.index'), {
data,
only: ['permissions'],
preserveScroll: true,
preserveState: true,
onSuccess: (page) => {
console.log(page);
sorting.value = sortValue;
// model.value = page.props.permissions.data;
}
});
}
}
const table = useVueTable({
data: props.permissions.data,
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
manualSorting: true,
enableSortingRemoval: true,
pageCount: props.permissions.per_page,
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: (updaterOrValue) => handleSortingChange(updaterOrValue),
onColumnFiltersChange: (updaterOrValue) => valueUpdater(updaterOrValue, columnFilters),
onGlobalFilterChange: (updaterOrValue) => valueUpdater(updaterOrValue, globalFilter),
onRowSelectionChange: (updaterOrValue) => valueUpdater(updaterOrValue, rowSelection),
state: {
get sorting() {
return sorting.value;
},
get columnFilters() {
return columnFilters.value;
},
get globalFilter() {
return globalFilter.value;
},
get rowSelection() {
return rowSelection.value;
},
get expanded() {
return expanded.value;
},
},
});
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Permisos" />
<IndexLayout title="Permisos">
<div class="w-full">
<div class="flex items-center py-4">
<Input
class="max-w-sm"
placeholder="Filter emails..."
:model-value="globalFilter ?? ''"
@update:model-value="(value) => (globalFilter = String(value))"
/>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto"> Columns <ChevronDown class="ml-2 h-4 w-4" /> </Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())"
:key="column.id"
class="capitalize"
:model-value="column.getIsVisible()"
@update:model-value="
(value: any) => {
column.toggleVisibility(!!value);
}
"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() && 'selected'">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>
<TableRow v-else>
<TableCell :colspan="columns.length" class="h-24 text-center"> No results. </TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} de {{ table.getFilteredRowModel().rows.length }} fila(s) seleccionadas.
</div>
<div class="space-x-2">
<Button variant="outline" size="sm" :disabled="!table.getCanPreviousPage()" @click="table.previousPage()"> Anterior </Button>
<Button variant="outline" size="sm" :disabled="!table.getCanNextPage()" @click="table.nextPage()"> Siguiente </Button>
</div>
</div>
</div>
</IndexLayout>
</AppLayout>
</template>
The official documentation says that defineModel should be used to mutate the data, but still I didn't get it to work properly.
Please, any help will be very appreciate it. Thanks in advance.