Mar 13, 2025
0
Level 1
Laravel 12 Vue inertia shadcn ssr datatable
Has anybody got working shadcn datatable with server-side rendering? I got pagination working but if I search then it doesnt update table but in network tab it is getting correctly data.
<script setup lang="ts">
import { ref, watch, watchEffect, defineProps, computed } from 'vue'
import {
useVueTable,
getCoreRowModel,
getSortedRowModel,
FlexRender,
SortingState,
ColumnFiltersState,
ExpandedState,
VisibilityState
} from '@tanstack/vue-table'
import { Input } from '@/components/ui/input'
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell
} from '@/components/ui/table'
import { cn, valueUpdater } from '@/lib/utils'
import DataTablePagination from './DataTablePagination.vue'
import { PaginationMeta } from '@/types/models'
import { useDebounceFn } from '@vueuse/core'
import { router } from '@inertiajs/vue3'
interface DataTableProps<T> {
data: T[],
columns: any[],
globalFilterInitial?: string,
noDataText?: string,
paginationMeta: PaginationMeta,
routePrefix: string,
}
const props = defineProps<DataTableProps<any>>()
// Reactive table data reference.
const tableData = ref(props.data)
const searchTerm = ref('')
const globalFilter = ref(props.globalFilterInitial || '')
// Table state variables.
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
// Create the table instance.
const table = useVueTable({
data: tableData.value,
columns: props.columns,
manualFiltering: true,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
// Omit getFilteredRowModel since filtering is handled server‑side.
onGlobalFilterChange: updaterOrValue => {
console.debug('Global filter change triggered:', updaterOrValue)
valueUpdater(updaterOrValue, globalFilter)
},
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
get globalFilter() { return globalFilter.value },
},
})
// Update the table's data when props.data changes.
// Use the function form to merge new data with existing options.
watchEffect(() => {
tableData.value = Array.isArray(props.data) ? [...props.data] : props.data
console.log('Table data updated:', tableData.value)
table.setOptions(prev => ({ ...prev, data: tableData.value }))
})
// Debounced search function that sends searchableColumns along with the search query.
// If the search term is empty, navigate back to the index route.
const doSearch = useDebounceFn(() => {
console.debug('Performing search with term:', searchTerm.value)
if (searchTerm.value.trim() !== "") {
const query: Record<string, any> = { search: searchTerm.value }
router.get(
route(`${props.routePrefix}.search`, query),
{},
{ preserveState: true, preserveScroll: true, replace: true }
)
} else {
router.get(
route(`${props.routePrefix}.index`),
{},
{ preserveState: true, preserveScroll: true, replace: true }
)
}
}, 300)
watch(searchTerm, (newVal) => {
globalFilter.value = newVal
doSearch()
})
// Computed property for rows (useful for debugging).
const rows = computed(() => table.getRowModel().rows)
watch(rows, (newRows) => {
console.debug('Rows computed updated. Count:', newRows.length)
})
</script>
<template>
<div>
<!-- Global Search Input -->
<div class="mb-4">
<Input placeholder="Search..." v-model="searchTerm" class="max-w-sm" />
</div>
<!-- Table -->
<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" :class="cn(
{ 'sticky bg-background/95': header.column.getIsPinned() },
header.column.getIsPinned() === 'left' ? 'left-0' : 'right-0'
)">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="rows.length">
<template v-for="row in rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() ? 'selected' : ''">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id" :class="cn(
{ 'sticky bg-background/95': cell.column.getIsPinned() },
cell.column.getIsPinned() === 'left' ? 'left-0' : 'right-0'
)">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
<pre class="p-4 text-sm">{{ JSON.stringify(row.original, null, 2) }}</pre>
</TableCell>
</TableRow>
</template>
</template>
<TableRow v-else>
<TableCell :colspan="props.columns.length" class="text-center">
{{ props.noDataText || 'No results.' }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- Pagination and other controls -->
<DataTablePagination :table="table" :meta="props.paginationMeta" :routePrefix="props.routePrefix" />
</div>
</template>
Please or to participate in this conversation.