Download the PHP package toolbelt/inertia-table without Composer
On this page you can find all versions of the php package toolbelt/inertia-table. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download toolbelt/inertia-table
More information about toolbelt/inertia-table
Files in toolbelt/inertia-table
Package inertia-table
Short Description Server-driven data tables for Laravel and Inertia.js.
License MIT
Homepage https://github.com/thienbd203/inertia-table
Informations about the package inertia-table
Musing Inertia Table
Server-driven data tables for Laravel and Inertia.js. Define the table once in PHP—columns, sorting, search, filters and actions—and render it in Vue with one component.
Toolbelt keeps the server authoritative. The browser can only request capabilities declared by the table, URL state is namespaced per table, and query execution is powered by Spatie Laravel Query Builder.
[!WARNING] The package is actively developed before
v1.0. Please expect API changes between minor releases.
Highlights
- PHP-first definitions for columns, filters, row actions and bulk actions.
- Allowlisted search, sort and filter queries—never raw client input in SQL.
- A ready-to-use Vue
<DataTable>built from shadcn-vue-style source and Reka UI primitives. - Text, numeric, set, boolean and date filters, including single-date and date-range calendars.
- Per-table query-string state, Inertia partial reloads, pagination, column visibility, sticky headers/columns and all-results selection across pages.
- Scoped saved views with defaults, sharing, optimistic locking and live dirty-state feedback.
- Signed synchronous or queued exports for all, filtered or selected rows, plus optional XLSX/PDF adapters.
- Presentation helpers for badges, dates, images, links, tooltips, alignment and Tailwind classes.
- Slots and headless composables when the default renderer needs an escape hatch.
- Built-in English and Vietnamese interface messages with per-app and per-table overrides.
Requirements
| Layer | Requirement |
|---|---|
| PHP | 8.3+ |
| Laravel | 12 or 13 |
| Inertia | Laravel 2 or 3; Vue 3.4+ |
| Query engine | Spatie Laravel Query Builder 7 |
| Frontend peers | Tailwind CSS 4.1+, Reka UI 2.10+, @lucide/vue 1.30+ |
Installation
Install the Laravel core and Vue renderer:
Releases
The Laravel package is distributed through Packagist and the Vue renderer through npm. A pushed Git tag in the form vX.Y.Z is the release trigger. The tag must match the version in package.json.
Before the first release, submit https://github.com/thienbd203/inertia-table to Packagist and add an NPM_TOKEN repository secret with permission to publish @musing/inertia-table-vue. The release workflow runs PHP and JavaScript checks, then publishes the Vue package with npm provenance.
Publish configuration only when you want to change pagination or debounce defaults:
Saved views are opt-in. Publish and run their migration before enabling them on a table:
Tailwind CSS v4
The renderer contains Vue source that Tailwind must scan. Add this to the host application's stylesheet:
The package uses your existing shadcn/Tailwind CSS variables. It does not require an application @/components/ui alias.
Quick start
Generate a dedicated table class when the table needs row actions, bulk actions, Saved Views or exports:
The command writes to app/Tables, infers a singular model when --model is
omitted, and refuses to replace an existing class unless --force is passed.
Create a table definition. This is the source of truth for what users can do.
Pass it directly to the Inertia page:
Then render it. For most screens this is all the frontend code required.
Anonymous tables
Simple read-only tables can be defined inline without a dedicated class. The resource may be an Eloquent model class or an existing Eloquent builder; the builder is cloned before each resolution so the table cannot mutate the controller's query.
Table::build() also accepts search, pagination, debounceTime,
withQueryBuilder, emptyState and stickyHeader. Set pagination: false to
return the complete normalized result and remove page controls. Anonymous
tables intentionally do not declare actions, exports or Saved Views; use a
generated class when server-managed behavior or persisted state is required.
Internationalization
The Vue renderer defaults to English and ships with Vietnamese messages. Configure it once on the Vue app:
The configuration uses Vue provide/inject, so each SSR application instance keeps its own locale. A table can override the app defaults without changing other tables:
locale controls calendars and locale-sensitive UI. messages controls package-owned interface text. Application-owned labels such as column names, filter options and action labels should still be translated by the host application.
Laravel-owned defaults follow app()->getLocale() automatically. Publish the language files when an application needs to customize them:
Every Vietnamese frontend key is type-checked against the English catalog. Unknown interpolation placeholders are left visible rather than silently removed. See docs/customization.md for the ownership rules, icon overrides, slots, stable CSS hooks and headless customization API.
Columns
Built-in types: TextColumn, NumberColumn, NumericColumn, BadgeColumn, BooleanColumn, DateColumn, DateTimeColumn, ImageColumn and ActionColumn.
All content columns support common presentation methods such as sortable(), searchable(), toggleable(), visible(), headerClass(), cellClass(), tooltip(), alignment, wrapping and truncation.
ActionColumn::asDropdown() groups each row's actions behind one accessible
menu trigger. Dynamic action(<key>) slots work in both the inline and dropdown
renderers.
Sticky header and columns
Enable a sticky header on one table with a property or the fluent API. The
default renderer gives sticky-header tables a 70vh scroll viewport so the
header has a vertical scroll container; override
--tb-sticky-header-max-height on the wrapper when a screen needs another
height.
stickable() lets the user pin or unpin a column from its header menu.
sticky() makes the column permanently pinned and works with every column
type, including ActionColumn:
The pin side is inferred from the column's visible position. Adjacent pinned columns stack measured widths, hidden columns retain their pin preference, and offsets are recalculated after visibility or responsive width changes. Logical CSS insets mirror the leading/trailing groups in RTL layouts. Pin state is namespaced in the table URL and included in Saved Views; it never changes the search/filter identity used by bulk selection.
Use a custom sort for expressions or application-specific ordering:
Badges and images
Navigation
Cell and row URLs can be a string or a Url object. The object carries Inertia navigation options to the renderer.
Tables deliberately do not make rows clickable by default. Handle the optional row-click event when a screen needs it:
Empty states and row data attributes
Return an EmptyState when a genuinely empty base table should offer more than
the generic no-results message. It supports a title, message, optional icon,
metadata, normalized data-* attributes and URL actions:
The server only serializes this definition when the unfiltered base query is
empty. A search or filter that happens to match no rows keeps the ordinary
No results found UI. The emptyState slot remains available to replace the
default renderer.
Add safe per-row DOM hooks without leaking arbitrary HTML attributes by
returning keys without the data- prefix. The callback receives both the model
and the transformed row data:
Only scalar or null values are accepted. Package-owned data-selected and
data-row-clickable state cannot be overwritten.
Search and filters
Mark columns as searchable() for global search. Override the resolved list on the table when necessary:
Available filter classes and their default behaviour:
| Filter | Typical clauses |
|---|---|
TextFilter |
contains, starts with, equals, not equals |
NumericFilter |
comparison, equals and range clauses |
SetFilter |
in, not in, equals, not equals |
BooleanFilter |
true / false |
DateFilter |
before, after, equals, and date ranges |
SetFilter presents a multi-select UI for in and not_in. DateFilter presents an inline calendar for a single date and a two-month range calendar for between and not_between.
For application-specific query logic, use applyUsing() and retain the declared option allowlist:
SelectFilter is available as a deprecated alias for SetFilter.
Relationship queries
Declared columns and filters accept nested Eloquent paths. Dot notation is never
read directly from client input: only paths present in columns() or filters()
can become query constraints.
Search and filters use nested whereHas constraints, so has-many matches do not
duplicate base rows. Direct columns are qualified with the model table to avoid
ambiguous-column errors. Nullable relationship filters include a missing relation
when using is_not_set.
To-one relationship sorting uses the optional Eloquent Power Joins adapter and a left join, preserving rows whose relationship is null:
To-many sorting stays duplicate-safe by ordering on a correlated MIN for
ascending order or MAX for descending order. Use sortUsing() when a domain
needs different aggregation or ordering semantics. The global adapter is
configured at inertia-table.relationship_sorter.
Tables can customize the same Spatie query builder used by results, explicit and all-matching selections, and every export scope:
When the base query or hook adds joins, the package selects the base model and deduplicates by its qualified primary key so pagination totals and exported rows remain stable. Application-owned joined projections and custom sort callbacks remain responsible for their own SQL portability.
Actions
Actions are server-declared and can be row-level, bulk or both. Authorization, visibility, disabled state and action labels may vary per model.
handle() invokes the closure once per selected model in chunks, which is useful when model events must run. Use chunkSize() to override the default chunk size of 1,000. During bulk execution, unselectable models and models whose row action is unauthorized, disabled or hidden are skipped. handleSelection() invokes the closure once with a typed Selection, which is useful for a set-based update over a large filtered result; because it operates directly on the query, its callback owns any additional per-model eligibility constraints that cannot be represented by selectableQuery().
before() and after() wrap the handler once per action request and receive the Selection. An after() callback may return a response or URL, and after('/topics/archived') provides a direct redirect. Use authorize() for request-level authorization and authorized() for model-level row authorization. Handler actions automatically receive a signed internal POST endpoint under the configured action_path; action scope and availability are checked again when that endpoint runs.
Managed endpoints use Laravel's normal response contract consistently: returned Response/Responsable values pass through, successful handlers without one redirect back, unavailable actions return 403, disabled row actions return validation errors, and unexpected exceptions remain visible to Laravel's exception handler.
The Selection query always starts from Table::query() and applies selectableQuery(). For an all-results selection, it rebuilds the query through the table's declared search/filter allowlist and applies the unchecked keys from except. Useful APIs are query(), count(), get(), firstOrFail() and memory-safe each().
Declare bulk eligibility at both query and row level. The query scope gives the frontend an exact selectableTotal without loading every model; the row check disables individual checkboxes. Keep both rules equivalent whenever possible:
An unselectable row may still expose row actions. Selectability only defines the bulk-selection boundary.
Use endpoint() when an existing application route should own the action instead:
Omit both handle() and endpoint() for a frontend-owned action. The component emits custom-action with (action, keys, onFinish, selection); call onFinish() after the custom work completes. Existing handlers can keep using the first three arguments.
The header checkbox immediately selects every selectable result matching the current search and filters, across all pages. Its label and selected count use the exact server-provided selectableTotal; there is no intermediate "current page" selection step. The checkbox is empty, indeterminate, or checked, and clicking the indeterminate state always resolves to select-all. Individual rows can then be unchecked and are tracked in selection.except. Shift-clicking a row checkbox applies the target checkbox state to the contiguous range from the previously clicked row on the current page while skipping disabled checkboxes.
Explicit bulk selections keep the existing { ids: [...] } request payload. Selecting all matching results sends a selection descriptor instead of attempting to load every ID into the browser:
Managed handlers resolve this descriptor through Selection automatically. Application-owned endpoint() routes remain responsible for resolving it safely and must not apply raw client attributes directly to SQL.
Confirmation text supports :count for row and bulk actions, plus scalar row attributes such as :name. This keeps destructive confirmation copy honest without loading all matching IDs into the browser:
The title and message may instead contain singular, plural, and optional all-matching variants. An all-matching selection uses the third variant and falls back to the plural variant when it is omitted:
Action icons are intentionally library-agnostic. Register your Lucide resolver once:
Exports
Declare one or more authorized export options on the table. Native CSV has no additional dependency and defaults to the full base query:
allRows() uses Table::query() without applying the browser state.
filtered() runs the current search, filters and sort through the same server
normalization as the visible table. selected() reuses the typed Selection,
including all-matching selections and exclusions. A selected export enables row
checkboxes even when the table has no bulk actions, and starting a download never
clears the current selection.
Columns are exportable by default except ActionColumn. Customize the resolved
value or exclude a column without changing its onscreen renderer:
Declared exportable columns are used by default. Call visibleColumnsOnly() on
an export when it should follow the normalized column visibility state. Native
CSV streams the Eloquent cursor, emits UTF-8 with a BOM by default, and protects
spreadsheet formula prefixes. Use meta(['delimiter' => ';', 'bom' => false])
to customize CSV output.
XLSX and PDF use the optional Laravel Excel adapter:
The base package does not require Laravel Excel. Requesting one of those formats
without it returns a clear validation error. Custom formats implement
Musing\InertiaTable\Contracts\Exporter and are registered under
inertia-table.exporters.<type>.
Call queue() when the export should run outside the request. The worker rebuilds
the table and query from a normalized, serializable snapshot; it never receives a
live request, query builder, table instance or definition closure:
Queue connection, name, delay, disk, path and expiry fall back to
inertia-table.queue. Every dispatch carries an idempotency key, so duplicate
submissions for the same actor and scoped export reuse the existing job. Completed
files are deleted after expiry, and partial files are removed when generation
fails. chain() accepts follow-up job objects.
The default context captures the authenticated actor and restores it in the
worker before authorization is checked again. Multi-tenant applications can add
scalar tenant identifiers with scopeAttributes() and provide an
ExportContext implementation via context() to restore and release tenant
state. Definitions removed or materially changed after dispatch fail safely
instead of exporting with different semantics.
The Vue renderer submits signed POST requests and reads Laravel's CSRF token from
either <meta name="csrf-token"> or the XSRF-TOKEN cookie. It exposes
export-success, export-queued and export-error events; custom renderers can
use the same controller directly:
Queued dispatches expose queuedExport with dispatched, processing, ready,
failed or expired state. The package does not start a hidden polling loop.
Applications can deliver status through their existing notifications or realtime
channel and call updateQueuedExport(status); a ready status with a URL renders a
download action. An explicit redirectAfterDispatch() is followed immediately.
Slots and headless API
The default renderer is intended to cover normal tables. Use slots only for targeted customisation.
Useful slots include topbar, beforeSearch, afterSearch, beforeActions, afterActions, filters, table, thead, tbody, footer, loading, emptyState, confirmation, cell(attribute), header(attribute), filter(attribute), image(attribute) and image-fallback(attribute).
Use filter(attribute) when an option source needs application-owned behavior such as remote search, pagination or creating a missing option. The slot receives filter, state, value, update, setDisplayValue, close, table and actions:
Declare the stored value with a regular server-side filter. For example, an integer foreign key can use a clause-less numeric filter:
The package only owns the selected filter value and URL state in this case. The application owns the endpoint, loading state, debounce, result pagination and option creation.
For a fully custom renderer, use the composables instead:
Laravel resources include the model's Eloquent primary key as stable row metadata, so selection also works for UUIDs and primary keys not named id. When rendering an application-owned resource, override the identity explicitly:
The equivalent headless option is useActions(table, { rowKey: (topic) => topic.uuid }). Selection persists across pagination and is cleared when the active search or filters change.
URL state and multiple tables
Every table gets an isolated query-string namespace. Several table resources may live on one Inertia page without overwriting one another.
Toolbelt translates this state to Spatie's query contract internally. Invalid columns, sorts, clauses, filter values and page sizes are ignored or replaced by safe defaults before the query executes.
Saved views
Enable saved views by returning a Views definition from the table. The default
scope belongs to the authenticated Laravel user:
The toolbar then provides the view switcher and create, update, rename, delete,
default and share operations allowed by the server. View state contains sort,
filters, column visibility, pinned-column metadata and page size. Search remains
ephemeral unless includeSearch() is enabled:
attributes() isolates otherwise identical tables by application context, such
as tenant or workspace. scopeTableName() additionally isolates multiple named
instances of the same PHP table class. Use scopeUser(false) for application-wide
views, userResolver() for a non-standard identity source, and modelClass() for
a TableView subclass. Fine-grained policies are available through
authorizeCreate(), authorizeUpdate(), authorizeDelete(), authorizeShare()
and authorizeDefault().
State precedence is explicit URL values over the selected view, then the user's
default view, then table defaults. Stored values are normalized against the
table's current columns, filters and per-page allowlist whenever they are read,
so stale definitions cannot restore undeclared query capabilities. CRUD uses
signed, CSRF-protected routes and a lock_version; concurrent stale edits are
rejected instead of silently overwriting a newer view.
For a custom renderer, compose the view controller with the same table instance:
Development
The design and resource contract are described in docs/architecture.md. Public compatibility guarantees are documented in docs/api-stability.md.
License
The MIT License. See LICENSE.md.
All versions of inertia-table with dependencies
illuminate/contracts Version ^12.0||^13.0
inertiajs/inertia-laravel Version ^2.0||^3.0
spatie/laravel-package-tools Version ^1.16
spatie/laravel-query-builder Version ^7.0