@toniel/laravel-tanstack-datatable
v0.1.9
Published
Vue 3 DataTable components for Laravel pagination with TanStack Query and shadcn-vue
Maintainers
Readme
Laravel TanStack DataTable
Vue 3 DataTable components for Laravel pagination with TanStack Query and TanStack Table.
Companion Package: This package works together with
@toniel/laravel-tanstack-pagination- the core composables library.
Installation
npm install @toniel/laravel-tanstack-datatable @toniel/laravel-tanstack-pagination
# or
yarn add @toniel/laravel-tanstack-datatable @toniel/laravel-tanstack-pagination
# or
pnpm add @toniel/laravel-tanstack-datatable @toniel/laravel-tanstack-pagination
# or
bun add @toniel/laravel-tanstack-datatable @toniel/laravel-tanstack-paginationPeer Dependencies
This package requires the following peer dependencies:
npm install vue @tanstack/vue-query @tanstack/vue-tableQuick Start
Basic Usage
<script setup lang="ts">
import { usePagination } from '@toniel/laravel-tanstack-pagination'
import { DataTable } from '@toniel/laravel-tanstack-datatable'
import { createColumnHelper } from '@tanstack/vue-table'
import axios from 'axios'
// Define your data type
interface User {
id: number
name: string
email: string
}
// Create columns
const columnHelper = createColumnHelper<User>()
const columns = [
columnHelper.accessor('id', {
header: 'ID',
}),
columnHelper.accessor('name', {
header: 'Name',
}),
columnHelper.accessor('email', {
header: 'Email',
}),
]
// Use pagination composable
const {
tableData,
pagination,
search,
currentPerPage,
sortBy,
sortDirection,
isLoading,
error,
handlePageChange,
handlePerPageChange,
handleSearchChange,
handleSortChange,
refetch,
} = usePagination(
(filters) => axios.get('/api/users', { params: filters }),
{ queryKey: 'users', defaultPerPage: 10 }
)
</script>
<template>
<DataTable
:data="tableData"
:columns="columns"
:pagination="pagination"
:is-loading="isLoading"
:error="error"
:search="search"
:current-per-page="currentPerPage"
:sort-by="sortBy"
:sort-direction="sortDirection"
title="Users"
item-name="users"
@page-change="handlePageChange"
@per-page-change="handlePerPageChange"
@search-change="handleSearchChange"
@sort-change="handleSortChange"
@retry="refetch"
/>
</template>Features
Core Features
- Search with debounce
- Pagination with Laravel meta
- Sorting (client & server-side)
- Custom Filters with state persistence across pages
- Row selection with bulk actions
- Dark mode support
- Loading states & error handling
- Fully customizable via slots
Component Props
DataTable Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| data | Array | [] | Table data array |
| columns | ColumnDef[] | required | TanStack Table column definitions |
| pagination | LaravelPaginationResponse | null | Laravel pagination response |
| isLoading | boolean | false | Loading state |
| error | Error | null | Error object |
| search | string | '' | Search query |
| filters | Record<string, any> | {} | Custom filter state |
| currentPerPage | number | 10 | Current items per page |
| perPageOptions | number[] | [10,15,25,50,100] | Per page options |
| sortBy | string | null | Current sort column |
| sortDirection | 'asc'\|'desc' | 'asc' | Sort direction |
| enableRowSelection | boolean | false | Enable row selection |
| rowSelection | RowSelectionState | {} | Selected rows state |
| getRowId | Function | (row) => row.id | Get unique row ID |
| showSelectionInfo | boolean | true | Show selection info bar when rows are selected |
| showSearch | boolean | true | Show search input |
| showCaption | boolean | true | Show table caption |
| showPerPageSelector | boolean | true | Show per page selector |
| title | string | 'Items' | Table title |
| itemName | string | 'items' | Item name for pluralization |
| loadingText | string | 'Loading...' | Loading text |
| errorTitle | string | 'Error loading data' | Error title |
| emptyStateText | string | 'No items found' | Empty state text |
DataTable Events
| Event | Payload | Description |
|-------|---------|-------------|
| pageChange | number | Emitted when page changes |
| perPageChange | number | Emitted when per page changes |
| searchChange | string | Emitted when search input changes |
| sortChange | string | Emitted when sort column changes |
| filterChange | Record<string, any> | Emitted when filters change |
| retry | - | Emitted when retry button clicked |
| update:rowSelection | RowSelectionState | Emitted when row selection changes |
Slots
filters Slot
Add custom filters next to search input. The slot now provides the current filter state:
<DataTable :filters="myFilters" @filter-change="handleFilterChange" ...>
<template #filters="{ filters }">
<select v-model="filters.status" class="...">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<select v-model="filters.category" class="...">
<option value="">All Categories</option>
<option value="A">Category A</option>
<option value="B">Category B</option>
</select>
</template>
</DataTable>Slot Props:
filters- Current filter state object (use for v-model bindings inside the slot)
header Slot
Add action buttons (e.g., Create button):
<DataTable ...>
<template #header>
<button @click="openCreateModal" class="...">
Add User
</button>
</template>
</DataTable>bulk-actions Slot
Add bulk action buttons when rows are selected:
<DataTable
:enable-row-selection="true"
v-model:row-selection="selectedRows"
...
>
<template #bulk-actions="{ selectedIds, selectedData, clearSelection }">
<button @click="bulkDelete(selectedIds)" class="...">
Delete Selected
</button>
<button @click="clearSelection" class="...">
Clear
</button>
</template>
</DataTable>selection-info Slot
Fully customize the selection info bar (replaces the default blue bar):
<DataTable
:enable-row-selection="true"
v-model:row-selection="selectedRows"
...
>
<template #selection-info="{ selectedIds, selectedData, selectedCount, clearSelection }">
<div class="your-custom-class">
{{ selectedCount }} items selected
<button @click="handleBulkAction(selectedIds)">Action</button>
<button @click="clearSelection">Clear</button>
</div>
</template>
</DataTable>Slot props available:
selectedIds- Array of selected row IDsselectedData- Array of selected row data objectsselectedCount- Number of selected rowsclearSelection- Function to clear all selectionsselectAllCurrentPage- Function to select all rows on current pagedeselectAllCurrentPage- Function to deselect all rows on current page
Custom Filters
This package supports custom filters that persist correctly when changing pages.
Basic Usage with usePagination
The usePagination composable provides built-in filter management:
<script setup lang="ts">
import { ref } from 'vue'
import { usePagination } from '@toniel/laravel-tanstack-pagination'
import { DataTable } from '@toniel/laravel-tanstack-datatable'
// Use pagination composable with filter support
const {
tableData,
pagination,
isLoading,
search,
currentPerPage,
sortBy,
sortDirection,
customFilters,
handlePageChange,
handlePerPageChange,
handleSearchChange,
handleSortChange,
setFilter,
removeFilter,
refetch,
} = usePagination(
(filters) => axios.get('/api/users', { params: filters }),
{ queryKey: 'users', defaultPerPage: 10 }
)
// Set custom filters
const updateStatusFilter = (status: string) => {
if (status) {
setFilter('status', status)
} else {
removeFilter('status')
}
refetch()
}
const updateCategoryFilter = (category: string) => {
if (category) {
setFilter('category', category)
} else {
removeFilter('category')
}
refetch()
}
</script>
<template>
<DataTable
:data="tableData"
:columns="columns"
:pagination="pagination"
:is-loading="isLoading"
:search="search"
:current-per-page="currentPerPage"
:sort-by="sortBy"
:sort-direction="sortDirection"
:filters="customFilters"
@page-change="handlePageChange"
@per-page-change="handlePerPageChange"
@search-change="handleSearchChange"
@sort-change="handleSortChange"
@filter-change="handleFilterChange"
>
<template #filters="{ filters }">
<select
:value="filters.status || ''"
@change="updateStatusFilter(($event.target as HTMLSelectElement).value)"
>
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</template>
</DataTable>
</template>Using handleFilterChange
For bulk filter updates, use the handleFilterChange function:
<script setup lang="ts">
const {
customFilters,
handleFilterChange,
handlePageChange,
// ...
} = usePagination(fetchFn, { queryKey: 'users' })
// Bulk update filters
const applyFilters = (newFilters: Record<string, any>) => {
handleFilterChange(newFilters)
}
</script>
<template>
<DataTable
:filters="customFilters"
@filter-change="handleFilterChange"
...
/>
</template>Using Filters Slot with v-model
For cleaner v-model binding inside the filters slot:
<script setup lang="ts">
import { computed } from 'vue'
import { usePagination } from '@toniel/laravel-tanstack-pagination'
const {
customFilters,
handleFilterChange,
// ...
} = usePagination(fetchFn, { queryKey: 'users' })
// Create a writable computed for two-way binding
const filtersModel = computed({
get: () => customFilters.value,
set: (val) => handleFilterChange(val)
})
</script>
<template>
<DataTable
:filters="customFilters"
@filter-change="handleFilterChange"
>
<template #filters="{ filters }">
<!-- Direct v-model binding using filters from slot -->
<select v-model="filters.status">
<option value="">All Status</option>
<option value="active">Active</option>
</select>
</template>
</DataTable>
</template>Filter Methods Reference
| Method | Description |
|--------|-------------|
| setFilter(key, value) | Set a single filter key-value pair |
| removeFilter(key) | Remove a filter by key |
| handleFilterChange(filters) | Bulk update all filters at once |
| resetFilters() | Reset all filters to defaults |
| customFilters | Reactive ref containing current filter state |
Advanced Examples
With Row Selection
<script setup lang="ts">
import { ref } from 'vue'
import type { RowSelectionState } from '@tanstack/vue-table'
const selectedRows = ref<RowSelectionState>({})
const bulkDelete = async (ids: string[]) => {
await axios.delete('/api/users/bulk', { data: { ids } })
selectedRows.value = {}
}
</script>
<template>
<DataTable
:enable-row-selection="true"
v-model:row-selection="selectedRows"
...
>
<template #bulk-actions="{ selectedIds, clearSelection }">
<button @click="bulkDelete(selectedIds)">Delete</button>
<button @click="clearSelection">Clear</button>
</template>
</DataTable>
</template>With Custom Columns (Actions)
<script setup lang="ts">
const columns = [
columnHelper.accessor('id', { header: 'ID' }),
columnHelper.accessor('name', { header: 'Name' }),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => h('div', { class: 'flex gap-2' }, [
h('button', { onClick: () => edit(row.original) }, 'Edit'),
h('button', { onClick: () => remove(row.original) }, 'Delete'),
]),
}),
]
</script>With Checkbox Column
<script setup lang="ts">
import { Checkbox } from 'your-ui-library' // or native input
const columns = [
columnHelper.display({
id: 'select',
header: ({ table }) => h(Checkbox, {
checked: table.getIsAllPageRowsSelected(),
onUpdate: (val) => table.toggleAllPageRowsSelected(!!val),
}),
cell: ({ row }) => h(Checkbox, {
checked: row.getIsSelected(),
onUpdate: (val) => row.toggleSelected(!!val),
}),
}),
// ... other columns
]
</script>Using useRowSelection Composable
For more control over row selection, use the useRowSelection composable:
<script setup lang="ts">
import { ref } from 'vue'
import { useRowSelection } from '@toniel/laravel-tanstack-datatable'
import { createColumnHelper } from '@tanstack/vue-table'
interface User {
id: number
name: string
email: string
}
const tableData = ref<User[]>([])
const {
rowSelection,
selectedRowIds,
selectedRowData,
isAllCurrentPageSelected,
clearSelection,
toggleAllCurrentPage,
toggleRowSelection,
getSelectionColumn,
} = useRowSelection<User>({
data: tableData,
getRowId: (row) => String(row.id),
})
const columnHelper = createColumnHelper<User>()
const columns = [
getSelectionColumn({ size: 50 }), // Automatic checkbox column
columnHelper.accessor('name', { header: 'Name' }),
columnHelper.accessor('email', { header: 'Email' }),
]
const handleBulkDelete = async () => {
await axios.delete('/api/users/bulk', { data: { ids: selectedRowIds.value } })
clearSelection()
// refetch data...
}
</script>
<template>
<DataTable
:data="tableData"
:columns="columns"
:enable-row-selection="true"
v-model:row-selection="rowSelection"
:show-selection-info="false"
>
<template #selection-info="{ selectedIds, selectedCount, clearSelection }">
<div class="flex items-center gap-4 p-4 bg-red-50 rounded-lg">
<span>{{ selectedCount }} users selected</span>
<button @click="handleBulkDelete" class="btn-danger">Delete</button>
<button @click="clearSelection">Clear</button>
</div>
</template>
</DataTable>
</template>useRowSelection API
| Property/Method | Type | Description |
|-----------------|------|-------------|
| rowSelection | Ref<RowSelectionState> | Reactive selection state |
| selectedRowIds | Ref<string[]> | Array of selected row IDs |
| selectedRowData | Ref<T[]> | Array of selected row data |
| isAllCurrentPageSelected | Ref<boolean> | Whether all current page rows are selected |
| isSomeCurrentPageSelected | Ref<boolean> | Whether some (but not all) rows are selected |
| clearSelection | () => void | Clear all selections |
| toggleAllCurrentPage | () => void | Toggle select all on current page |
| toggleRowSelection | (id: string) => void | Toggle single row selection |
| selectRows | (ids: string[]) => void | Select multiple rows by IDs |
| deselectRows | (ids: string[]) => void | Deselect multiple rows by IDs |
| getSelectionColumn | (options?) => ColumnDef<T> | Get a checkbox column definition |
getSelectionColumn Options
getSelectionColumn({
headerClass: 'flex items-center justify-center',
cellClass: 'flex items-center justify-center',
checkboxClass: 'w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded',
size: 50,
})Styling
This package uses Tailwind CSS utility classes. Make sure Tailwind is configured in your project.
Dark Mode
Dark mode is automatically supported via Tailwind's dark: classes. Configure dark mode in your tailwind.config.js:
module.exports = {
darkMode: 'class', // or 'media'
// ...
}Custom Styling
You can override styles using Tailwind classes or custom CSS:
/* Custom table styles */
.data-table {
/* Your custom styles */
}Theming
The components use Tailwind CSS and support dark mode out of the box. You can customize colors by:
- Using Tailwind Config:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
// Customize colors here
}
}
}
}- CSS Variables:
:root {
--color-primary: ...;
--color-border: ...;
}Related Packages
@toniel/laravel-tanstack-pagination- Core composables (required)@tanstack/vue-query- Data fetching & caching@tanstack/vue-table- Table state management
Show Your Support
If this package helped you, please consider:
📄 License
🤝 Contributing
Contributions, issues and feature requests are welcome!
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CONTRIBUTING.md for more details.
🐛 Known Issues
Check the GitHub Issues page for known issues and feature requests.
📮 Contact
🙏 Acknowledgments
Built with these amazing libraries:
