givanov95 / laravel-data-table
A server-side DataTable builder for Laravel with a matching Vue 3 + Inertia frontend.
Requires
- php: ^8.3
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/pagination: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- spatie/laravel-query-builder: ^7.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- givanov95/laravel-git-hooks: ^1.1
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.5|^12.0|^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v4.2.1
- v4.2.0
- v4.1.1
- v4.1.0
- v4.0.1
- v4.0.0
- v3.5.1
- v3.5.0
- v3.4.0
- v3.3.0
- v3.2.1
- v3.2.0
- v3.1.0
- v3.0.0
- v2.0.0
- v1.1.0
- v1.0.1
- v1.0.0
- v0.2.0
- dev-feat/50-translatable-fallback-locales
- dev-fix/51-page-past-the-end
- dev-feat/48-restore-hook-archive-allowed
- dev-fix/46-enum-cast-locale
- dev-feat/38-column-filters
- dev-fix/40-exact-match-postgres-row-key
- dev-fix/33-archive-opt-in-post-restore
This package is auto-updated.
Last update: 2026-10-28 20:35:47 UTC
README
A server-side DataTable builder for Laravel that ships with a matching Vue 3 + Inertia frontend. Backend and frontend live in the same repository and are published as two separate packages:
| Package | Installer |
|---|---|
givanov95/laravel-data-table |
composer require givanov95/laravel-data-table |
@givanov95/vue-data-table |
npm install @givanov95/vue-data-table |
The two are designed to talk to each other through a single JSON payload, so you write your table definition once in PHP and the Vue component renders it.
Installation
Backend (Laravel 12–13 / PHP 8.3+)
composer require givanov95/laravel-data-table
The service provider is auto-discovered. Publish the config if you want to customise the request parameter keys:
php artisan vendor:publish --tag=data-table-config
This creates config/data-table.php.
Frontend (Vue 3 + Inertia)
npm install @givanov95/vue-data-table
Register the plugin once (e.g. in app.ts / app.js). Both options are
optional — without them the components fall back to identity translations and
a global route() helper if one exists.
import { createApp } from "vue"; import { DataTablePlugin } from "@givanov95/vue-data-table"; createApp(App) .use(DataTablePlugin, { // Hook up your i18n helper: translator: (key) => window.__(key), // Hook up ziggy or whatever produces URLs: route: window.route, // Optional: debounce delay for filter/search reloads (ms) reloadDebounceMs: 1200, }) .mount("#app");
Configuration
config/data-table.php:
return [ 'translatable_relation' => 'translations', 'translatable_column' => 'key', 'translatable_locale_column' => 'locale', 'translatable_text_column' => 'text', 'global_filter' => 'filter.global', 'per_page' => 'perPage', 'trashed' => 'filter.trashed', 'ordering' => 'ordering', 'time_zone' => null, // null: next to global_filter (filter.timeZone) 'column_filters' => 'filter', // filter[status]=paid 'default_per_page' => 15, 'max_per_page' => 100, ];
These keys map directly to the HTTP query parameters the frontend sends, and
both DataTable and QueryBuilderTable read them (trashed only when the
table calls allowTrashed()). ordering is the parameter that carries
ordering[key] and ordering[direction]. The Vue components bundled with the package send the
defaults above, so changing a key means pairing it with your own frontend.
The page size read from the request is bounded: anything above max_per_page
is cut to it (so ?perPage=100000 cannot load the whole table), and anything
that is not a number or is below 1 (empty, text, an array, zero, negative)
falls back to default_per_page. default_per_page is capped by
max_per_page too. This applies to both DataTable and QueryBuilderTable.
?perPage=-1is not "all rows" — it gets the default page size. Export all rows server-side instead.- A
max_per_pagebelow 1 is treated as a misconfiguration and falls back to 50; it does not turn the bound off. - If your frontend offers
perPageOptionslarger than 100, raisemax_per_pageaccordingly. An option abovemax_per_pagecannot be honoured: the server answers withmax_per_pagerows, and the per-page select shows the page size actually applied (asDefault (100), the label of the "Default" entry carrying the number) instead of the option that was chosen. - A
DataTableParamsyou build and pass toprocess()yourself is used as-is.
Backend usage
Basic example
use Givanov95\DataTable\DataTable; use App\Models\User; public function index() { $table = (new DataTable(User::query())) ->setColumn('id', '#', searchable: true, orderable: true) ->setColumn('name', __('Name'), searchable: true, orderable: true) ->setColumn('email', __('Email'), searchable: true, orderable: true) ->setColumn('action', __('Action')) ->process(); return Inertia::render('Users/Index', [ 'dataTable' => fn () => $table, ]); }
Sorting requested by the client (ordering[key], ordering[direction]) is only
applied to columns registered with orderable: true (for a RelationColumn, its
relation.column path). Any other key is ignored and the default ordering
(id DESC) is used. An ordering you set yourself with
setOrdering(new Ordering(...)) is applied as-is.
setColumn accepts both shorthand positional arguments and a fully
constructed Column object — pick whichever reads better:
use Givanov95\DataTable\Columns\Column; $table ->setColumn(new Column('id', '#', searchable: true, orderable: true)) ->setColumn('action', __('Action'));
Lightweight spatie table (QueryBuilderTable)
For lean listings that don't need the column-type system, QueryBuilderTable
wraps spatie/laravel-query-builder
and emits the same { data, meta, columns, state } contract, so the same
frontend consumes it. It handles global search, sorting, the trashed toggle,
a perPage cap, and an optional transform() for whitelisting output (keep PII
out of the listing).
use App\Models\User; use Givanov95\DataTable\QueryBuilderTable; $payload = QueryBuilderTable::for(User::query()->with('roles:id,name'), request()) ->columns([ ['key' => 'id', 'label' => __('ID'), 'sortable' => true], ['key' => 'first_name', 'label' => __('First name'), 'sortable' => true, 'searchable' => true], ['key' => 'email', 'label' => __('Email'), 'sortable' => true, 'searchable' => true], ]) ->allowTrashed() ->defaultSort('id', 'desc') ->transform(fn (User $user) => [ 'id' => $user->id, 'first_name' => $user->first_name, 'email' => $user->email, 'roles' => $user->roles->map(fn ($r) => ['id' => $r->id, 'name' => $r->name])->all(), 'deleted_at' => $user->deleted_at?->toDateTimeString(), ]) ->toArray();
The request keys (search, trashed, ordering and per-page) come from
config/data-table.php, like in DataTable. Spatie rejects a filter[...] it
does not know with InvalidFilterQuery, so QueryBuilderTable accepts the
filters named by the configured keys when they sit under Spatie's filter
parameter ('global_filter' => 'filter.search' accepts filter[search]), and
keeps tolerating filter[global], filter[timeZone] and (with allowTrashed())
filter[trashed]. A search key outside that parameter ('global_filter' => 'q')
works too.
Filters by column take 'filter' in columns(): 'text' (contains), 'exact'
(one or several values, never one the column cannot hold), 'select' (with
'options', as for setFilterOptions()) or 'date' (a range of calendar days, in
the server's zone). They are applied by the table, not by Spatie, so
filter[title]=Smith, John stays one value.
->columns([ ['key' => 'status', 'label' => __('Status'), 'filter' => 'select', 'options' => ['active' => __('Active'), 'blocked' => __('Blocked')]], ['key' => 'created_at', 'label' => __('Created'), 'filter' => 'date'], ])
allowTrashed(ability: 'viewArchive') shows the soft-deleted rows only to users
Gate::allows('viewArchive', User::class); for the rest, and for a model without
SoftDeletes, the trashed toggle is accepted and ignored (no 400, no 500).
The page size is bounded by data-table.max_per_page (see
Configuration); call ->maxPerPage(50) to override it for a
single table (it throws InvalidArgumentException below 1). Without
?perPage the page size is data-table.default_per_page, like in DataTable;
->defaultPerPage(25) overrides it for a single table.
Use DataTable for column-rich tables; use QueryBuilderTable for lean listings
that want spatie's query power behind a small, reusable facade.
Relation columns
use Givanov95\DataTable\Columns\RelationColumn; $table->setRelationColumn( new RelationColumn('user.name', __('User'), searchable: true, orderable: true) );
Ordering joins each relation of the path, so an orderable relation column takes
BelongsTo, HasOne and MorphOne relations (keys and morph class from the
relation). Any other relation (HasMany, BelongsToMany, MorphTo, a
…OfMany()) would repeat rows, so setRelationColumn() throws
UnsupportedRelationException for an orderable column on it. A searchable-only
column takes any relation (it is a whereHas).
Translatable columns
Designed to plug into the common translations morphMany pattern
(translatable_id, translatable_type, locale, key, text). The relation
and the key / locale / text columns are configurable (translatable_*); the
table, the model's key and the morph class (a morph map alias, too) come from
the relation. The translations the columns show are loaded with the page (only
their keys and locales), unless the query already loads the relation
(with('translations'), setRelation('translations')):
use Givanov95\DataTable\Columns\TranslatableColumn; $table->setTranslatableColumn( new TranslatableColumn( locale: app()->getLocale(), translationKey: 'title', label: __('Title'), searchable: true, orderable: true, ) );
fallbackLocales lists languages tried in order when a row has no text (or an
empty one) in locale, e.g. an admin list that also shows records not yet
translated into the admin's language. Enums are taken as their value:
new TranslatableColumn(Locale::Bg, 'title', __('Title'), searchable: true, orderable: true, fallbackLocales: [Locale::En, Locale::De]);
- The row shows the first language with a text that is not empty. A language the column does not list (or its enum cast does not know) is skipped, without an error.
- Search, global and by column, looks at the text in every language of the column.
- When the query loads the translations itself (
with('translations')), that load must include the fallback languages, or a row cannot show them. - Ordering is by the shown text. Rows without one (or with an empty text) come last in both directions, the same on MySQL, PostgreSQL and SQLite; this holds for a column without fallbacks too.
Special column types
// Enum filtering by case name $table->setEnumColumn('status', App\Enums\OrderStatus::class); // Numeric-only filtering (currency / numeric input) $table->setPriceColumn('price'); // Date filtering with timezone-aware parsing $table->setDateColumn('created_at', 'd.m.Y H:i:s');
The search box takes a date column as d.m.Y, d.m, H:i, H:i:s,
d.m.Y H:i or d.m.Y H:i:s (with the column's delimiters). The composable
sends the browser's time zone with the search (time_zone key), and for a
column whose format has a time (H), what the user types is read in that zone:
d.m.Yfinds the rows of that day in the user's zone (a row at 23:30 UTC is found by the next day's date in Sofia),d.m.Y H:ithe rows of that minute. These compare the stored value (>=/<), on every driver.H:i/H:i:sare moved to the server's zone with today's offset.d.m(no year) and the columns without a time in their format are compared as typed, in the server's zone.
The server's zone is app.timezone; the stored values are taken to be wall-clock
times in it. A zone PHP does not know (a typo, or an old alias such as
Europe/Kiev on some PHP builds) is ignored: the server's zone is used.
Supported databases
Date filtering formats the column in SQL, so it needs a driver-specific expression. It is supported on:
| Database | Driver | SQL used |
|---|---|---|
| MySQL / MariaDB | mysql, mariadb |
DATE_FORMAT(column, …) |
| PostgreSQL | pgsql |
TO_CHAR(column, …) |
| SQLite | sqlite |
strftime(…, column) |
An exactMatch column (and an exact price) never hands the database a value
its type rejects: PostgreSQL fails the whole query on id = 'abc', where MySQL
and SQLite find nothing. A term the column cannot hold matches nothing; any
other is the usual typed comparison, so results and indexes are unchanged.
- On PostgreSQL the column's real type decides, read from the schema once per
table and process: integers by width, numbers, booleans (
t,1,yes…), uuids, and ISO dates and times (2026-10-28,2026-10-28 10:00). A type not known here (jsonb, an array…) is compared as text. - Elsewhere a column numeric by its cast (
int,decimal:2,float…, or an integer key) takes only numbers (an integer within 32 bits).
On any other driver (e.g. SQL Server) filtering a date column throws
Givanov95\DataTable\Exceptions\UnsupportedDriverException. The other filters
(text, enum, price) are plain Eloquent where clauses and do not depend on it.
Eager-loading relations
$table->setRelation('translations'); $table->setRelation('user', ['id', 'name']);
Custom query hooks
$table->process(null, function ($query) { $query->where('owner_id', auth()->id()); }); // Or for free-form mutations: $table->advancedSearch(fn ($q) => $q->whereJsonContains('tags', 'featured'));
Filters by column
A column declared filterable takes its own filter from the request, next to
the search: filter[<key>] (the parameter is column_filters). The filters AND
with each other and with the search. The kind of filter follows the column:
$table // A select of the enum's cases: by value, labelled by the case's label() when it has one. ->setColumn('status', __('Status'), filterable: true) ->setEnumColumn('status', BookingStatus::class) // A select of the app's options: a list of ['value' => …, 'label' => …], a // value => label map, or a closure returning either (run once). ->setColumn('staff_id', __('Staff'), filterable: true) ->setFilterOptions('staff_id', fn () => $business->staff()->pluck('name', 'id')->all()) // A range of days: filter[starts_at][from]=2026-10-01&filter[starts_at][to]=2026-10-31. ->setColumn('starts_at', __('Starts'), orderable: true, filterable: true) ->setDateColumn('starts_at', 'd.m.Y H:i') // Anything else is a text: contains, or equals with exactMatch. ->setRelationColumn(new RelationColumn('customer.name', __('Customer'), filterable: true));
- Select: one value (
filter[status]=paid) or several (filter[status][]=paid&filter[status][]=open). Only offered values are compared; none offered matches nothing. - Date range: ISO days, as a date input sends them. Either end may be left out, and
totakes the whole day. For a column whose format has a time (H), the days are the user's, in the time zone the composable sends with the search. A date that does not exist is ignored. - Text: the same comparison as the search, for the column's type (price,
exactMatch, relation, translation). - A key that is not a filterable column is ignored. A filterable column cannot be named like another key under the same parameter (
global,trashedortimeZonewith the defaults), andsetColumn()throws if it is.
toArray() describes each column's filter (columns[].filterable, columns[].filter: {type: 'select', options: [{value, label}]}, {type: 'date'} or {type: 'text'}) and gives the applied ones in state.filters.
Archive and restore
The archive (soft-deleted rows) is off unless the table turns it on. Without
allowTrashed(), filter[trashed]=true is ignored and the live rows are listed.
$table->allowTrashed(); // anyone who sees the list $table->allowTrashed(ability: 'viewArchive'); // Gate::allows('viewArchive', Post::class)
With an ability, users the gate refuses get the live rows (the parameter is
ignored, not a 403). A model without SoftDeletes never shows an archive, so
calling allowTrashed() on it is harmless. toArray()['state']['trashed'] (and
$table->trashed in the default serialization) is what was applied, and
state.trashed_allowed ($table->trashedAllowed) whether this user may see the
archive at all: the bundled components show the archive toggle only then.
Restoring is a POST route of the app, never a query parameter of the list (a
GET that changes data can be triggered from another site). Build the table the
same way as for the list and call restore():
// routes/web.php Route::post('posts/{id}/restore', [PostController::class, 'restore'])->name('posts.restore'); // PostController public function restore(int $id) { return $this->posts->table()->restore($id, fn ($q) => $q->where('team_id', auth()->user()->team_id)); }
restore($id, $scope) looks the record up through the table's query and
$scope (pass the same narrowing as the callback of process()), among the
archived rows only, so a record the table does not list is a 404. It then
checks the restore policy (authorize('restore', $record)), restores the
record and redirects back with a success flash message. It is a 404 when the
table has no allowTrashed() or the model has no SoftDeletes, and a 403 when
the ability of allowTrashed() refuses.
The app's own rules go in $before, which runs after all those checks, with the
record, right before it is restored. It can refuse by throwing, and attributes
it sets are saved by the restore in the same write. restoreRecord() does the
same and returns the record, leaving the response (and its message) to the app:
$staff = $table->restoreRecord($id, before: function (Staff $staff) use ($business) { if (! $business->hasFreeStaffSeat()) { throw ValidationException::withMessages(['staff' => __('No free seat on the plan.')]); } }); return back()->with('success', __('Staff member restored.'));
On the frontend, pass the route to the composable (restoreUrl) or to the
DataTable component (restore-url); see below.
Frontend usage
<script setup lang="ts"> import { DataTable } from "@givanov95/vue-data-table"; import type { DataTableType } from "@givanov95/vue-data-table"; defineProps<{ dataTable: DataTableType<{ id: number; name: string; email: string }>; }>(); </script> <template> <DataTable :data-table="dataTable" :global-search="true" :per-page-options="[15, 30, 50]" > <template #cell(action)="{ item }"> <a :href="`/users/${item.id}/edit`">Edit</a> </template> </DataTable> </template>
Component props
| Prop | Type | Description |
|---|---|---|
dataTable |
DataTableType<T> |
The payload returned by (new DataTable(...))->process() |
propName |
string (default dataTable) |
Inertia prop key for partial reloads |
globalSearch |
boolean |
Show the global search input |
showTrashed |
boolean |
Show the "trashed" toggle (needs allowTrashed()) |
restoreUrl |
string |
POST URL (use ?id) of the restore button on archived rows; no button without it |
rowKey |
string |
Field that fills ?id (default: the model's key from the payload, else id) |
advancedFilters |
boolean |
Reserve space for the advanced-filters slot |
selectedRowIndexes |
(string | number)[] |
Highlight matching rows |
selectedRowColumn |
string |
Column to match against selectedRowIndexes |
rowClickLink |
string |
URL template (use ?id placeholder) for row clicks |
perPageOptions |
number[] |
Render a per-page dropdown |
Slots
#additionalContent— content inside the toolbar (e.g. "Create" buttons)#advancedFilters— content inside the advanced-filters toolbar slot#cell(<column-key>)— custom renderer for a column; receives{ value, item }#cell(<relation.column>)— custom renderer for relation columns
TanStack Table + shadcn-vue
For apps built on shadcn-vue (Laravel's Vue starter kit and the ones derived
from it), the package has a second frontend: a composable that drives the
payload of ->process()->toArray() with TanStack Table
in manual mode, and a shadcn-vue UI for it.
The two parts are delivered the way each one has to be:
| Part | What | How |
|---|---|---|
| Logic | useLaravelDataTable() and the payload types |
npm: @givanov95/vue-data-table/tanstack — updated with npm update |
| UI | ServerDataTable, DataTable, DataTableColumnHeader, features.ts |
published into the app — it imports the app's own @/components/ui/* |
Install
composer require givanov95/laravel-data-table npm install @givanov95/vue-data-table @tanstack/vue-table php artisan vendor:publish --tag=data-table-shadcn
The UI lands in resources/js/components/ui/data-table/. It needs these
shadcn-vue components in the app: button, input, label, select,
switch, table and dropdown-menu, and the @lucide/vue icons.
Update both together, so the UI and the composable stay on one version:
composer require givanov95/laravel-data-table:^4.0 npm install @givanov95/vue-data-table@^4.0 php artisan vendor:publish --tag=data-table-shadcn --force
The published files are the package's: --force overwrites them. Change them
here (props, slots) rather than in the app.
Texts
Every text of the UI goes through the package's translator, with the English text as the key. Hook up the app's translator once, after its own i18n plugin:
import { DataTablePlugin } from "@givanov95/vue-data-table/tanstack"; app.use(TranslationPlugin, translations) // the app's own __() .use(DataTablePlugin, { translator: (key, replacements) => app.config.globalProperties.__(key, replacements) });
Without a translator the English keys are shown, with :placeholders filled
in. The keys, for the app's lang/*.json:
| Key | bg | nl |
|---|---|---|
Search… |
Търсене… | Zoeken… |
No results. |
Няма резултати. | Geen resultaten. |
Show archived |
Покажи архивираните | Toon gearchiveerde |
Showing :from–:to of :total |
Показани :from–:to от :total | :from–:to van :total |
Rows per page |
Редове на страница | Rijen per pagina |
First page |
Първа страница | Eerste pagina |
Previous page |
Предишна страница | Vorige pagina |
Next page |
Следваща страница | Volgende pagina |
Last page |
Последна страница | Laatste pagina |
Columns |
Колони | Kolommen |
Clear filters |
Изчисти филтрите | Filters wissen |
No options |
Няма опции | Geen opties |
:label from |
:label от | :label vanaf |
:label to |
:label до | :label tot |
The translator receives :from, :to and :total as replacements; it must
replace the longest names first (Laravel does), or :to eats the start of
:total. fillPlaceholders() of the package does it right.
Usage
<script setup lang="ts"> import { h } from "vue"; import { useLaravelDataTable } from "@givanov95/vue-data-table/tanstack"; import type { LaravelDataTablePayload } from "@givanov95/vue-data-table/tanstack"; import { DataTableColumnHeader, ServerDataTable } from "@/components/ui/data-table"; import type { DataTableColumnDef } from "@/components/ui/data-table"; type Row = { id: number; name: string }; const props = defineProps<{ dataTable: LaravelDataTablePayload<Row> }>(); const table = useLaravelDataTable<Row>(() => props.dataTable); const columns: DataTableColumnDef<Row>[] = [ { accessorKey: "name", header: ({ column }) => h(DataTableColumnHeader, { column, title: "Name" }), }, ]; </script> <template> <ServerDataTable :columns="columns" v-bind="table"> <template #toolbar><!-- the page's own filters --></template> </ServerDataTable> </template>
useLaravelDataTable(payload, options) options: propName (default
dataTable), alsoReload (other props to refresh with the table, e.g. the
page's own filters), searchDebounceMs (1200), perPageOptions
([10, 20, 50, 100]; the applied size is always offered) and restoreUrl
('/posts/?id/restore' or (id) => route('posts.restore', id)): onRestore(id, visitOptions?)
POSTs there (with the page's own onSuccess / onError / onFinish, if given) and
does nothing without it. restoringId holds the row being restored until its POST
ends, so a button can show it; a second click meanwhile does nothing.
trashedAllowed says whether the server allows this user the archive. The table follows
the server's applied state (state.sort, state.search, state.trashed), so
back/forward and shared links stay right; a new sort, page size, search or
archive goes back to page 1. clearSearch(data) empties the search box and
reloads once, together with the page's own filter changes. The request keys
come from the payload's params (config/data-table.php), so changing a key
there needs nothing on the frontend.
A RawOrdering hides the sort from state.sort (the rows are not in the order
of a column then); pass what it amounts to when there is one:
new RawOrdering('created_at DESC, id DESC', new Ordering('created_at', 'DESC')).
The filters by column come with the composable: filterColumns (the columns the
server filters by, with their kind and options), filters (the values, as the
server applied them), setColumnFilter(key, value) (a select or a date range is
sent at once, a text waits like the search box; an empty value removes the
filter), clearColumnFilters() and hasColumnFilters. Each change goes back to
page 1. For TanStack, the same filters are state.columnFilters and
onColumnFiltersChange.
ServerDataTable draws them in the toolbar, after the search box: a select is a
button with the column's label that opens a list of checkboxes (and shows how many
are ticked), a date range is two date fields with the column's label, and a text
is a field. "Clear filters" shows while any filter is on. On a narrow screen the
toolbar wraps and the date range takes its own line. :show-filters="false"
leaves them out, to draw the page's own in #toolbar.
ServerDataTable props beyond the composable's: showTrashed,
searchPlaceholder, emptyText, trashedLabel, cellClass (classes on
every body cell, e.g. align-top for tall rows) and showFilters (true).
DataTable is the client-side counterpart for rows already in the page (no
server round trip): search, sorting, column toggle and the same footer as
ServerDataTable ("Showing 1–10 of 25", page numbers, per-page select). Props:
columns, data, pageSize (10), pageSizeOptions ([10, 20, 50, 100], []
hides the select), enableGlobalFilter, enableColumnToggle,
searchPlaceholder, emptyText, columnsLabel. Prefer ServerDataTable for
anything that can grow: DataTable loads every row.
Backend API reference
DataTable
__construct(Builder $builder, ?Request $request = null)setColumn(string|Column $keyOrColumn, ?string $label = null, bool $searchable = false, bool $orderable = false, bool $exactMatch = false, bool $filterable = false): selfsetFilterOptions(string $columnKey, array|Closure $options): selfsetRelationColumn(RelationColumn $column): selfsetTranslatableColumn(TranslatableColumn $column): selfsetEnumColumn(string $key, class-string<\BackedEnum> $enumClass): selfsetPriceColumn(string $key): selfsetDateColumn(string $key, string $format, string $dateDelimiter = '.', string $timeDelimiter = ':'): selfsetRelation(string $relationString, ?array $columnsToSelect = null): selfsetOrdering(Ordering $ordering): selfsetRawOrdering(?RawOrdering $rawOrdering): selfprocess(?DataTableParams $params = null, ?callable $callbackBeforePaginate = null): selfallowTrashed(bool $allow = true, ?string $ability = null): selfrestore(int|string $id, ?callable $scope = null): RedirectResponseadvancedSearch(callable $callback): selfgetData(): CollectiongetPaginator(): PaginatorgetBuilder(): BuildergetColumnByKey(string $key): ?Column
Column classes
| Class | Purpose |
|---|---|
Column |
Plain column (key, label, searchable, orderable…) |
RelationColumn |
Dot-notated relation column ('user.name') |
TranslatableColumn |
Pulls value from the configured translations table |
EnumColumn |
Internal — registered via setEnumColumn |
PriceColumn |
Internal — registered via setPriceColumn |
DateColumn |
Internal — registered via setDateColumn |
Repository layout
laravel-data-table/
├── composer.json # PHP package manifest
├── package.json # NPM package manifest
├── tsconfig.json
├── src/ # PHP source
│ ├── DataTable.php
│ ├── DataTableConfig.php
│ ├── DataTableParams.php
│ ├── DataTableServiceProvider.php
│ ├── ColumnFilter.php
│ ├── Columns/
│ ├── Exceptions/
│ ├── Support/
│ └── config/data-table.php
├── stubs/shadcn/ # shadcn-vue UI, published with --tag=data-table-shadcn
└── resources/
└── js/ # Vue / TypeScript source
├── index.ts
├── install.ts
├── config.ts
├── Table.vue
├── tanstack/ # useLaravelDataTable (npm: @givanov95/vue-data-table/tanstack)
├── components/
├── icons/
├── types/
└── utils/
License
MIT