inertify/table
inertify/table
Headless table tooling for Laravel + Inertia + Vue with:
- Pagination
- Sorting
- Filtering
- Multi-table support on one page (query keys are table-scoped)
- No UI opinions (you render your own markup)
Install
composer require inertify/table
If you publish config:
php artisan vendor:publish --tag=inertify/table-config
Optional Vue package build output:
npm install @inertify/table-vue
Publish to Packagist
- Make sure your package name in
composer.jsonis final (already set toinertify/table). - Commit and push
mainto a public Git repository. - Create a semantic version tag and push it:
git tag v1.0.0
git push origin v1.0.0
- Sign in to Packagist, click Submit, and paste your repository URL.
- In repository settings (GitHub/GitLab), add Packagist webhook so updates happen automatically.
- After indexing, install with Composer:
composer require inertify/table
Recommended release flow
- Merge changes to
main - Run tests (
composer test) - Tag release (
vX.Y.Z) - Push tag (
git push origin vX.Y.Z) - Verify package page on Packagist
Publish Vue package to npm (automated)
This repository includes GitHub Actions workflow at .github/workflows/publish-npm.yml.
It publishes @inertify/table-vue when you push a tag v*.
One-time setup
- In npm org
inertify, create a granular access token with publish permissions for@inertify/table-vueand 2FA bypass for automation. - In GitHub repo settings, add secret
NPM_TOKENwith that token value.
Release commands
npm version patch
git push origin main --follow-tags
The workflow validates that tag version matches package.json version, builds package, and publishes to npm.
Laravel API
use Inertia\Inertia;
use App\Models\User;
use Inertify\Table\Column;
use Inertify\Table\Filter;
use Inertify\Table\Table;
public function index()
{
$table = Table::make('users')
->columns([
Column::make('name'),
Column::make('email'),
Column::make('role'),
Column::make('created_at')->type('date'),
])
->sorts(['name', 'email', 'created_at'])
->filters(['name', 'email', 'created_at'])
->defaultSort('-created_at');
return Inertia::render('Users/Index', [
...$table->payload(
query: User::query(),
rowsKey: 'users',
metaKey: 'meta'
),
]);
}
Filter inference from column type
When filters([...]) receives a string key, the package infers the filter type from Column::type(...):
number/int/float/decimal=>Filter::numberRange(...)date/datetime/timestamp=>Filter::dateRange(...)boolean/bool=>Filter::exact(...)- everything else =>
Filter::partial(...)
Use explicit Filter::... entries in filters([...]) when you need custom behavior (for example Filter::select(...) with options or callback filters).
Inertia macro shortcut
The package registers a Inertia::tablePayload(...) macro:
return Inertia::render('Users/Index', [
...Inertia::tablePayload(
name: 'users',
query: User::query(),
configure: fn ($table) => $table
->sorts(['name', 'email'])
->filters([Filter::partial('name')]),
rowsKey: 'users',
metaKey: 'meta',
),
]);
Vue Headless API
Composables-first (recommended)
import {
useTable,
useTableFilters,
useTableSorting,
useTablePagination,
useTableSelection,
} from "@inertify/table-vue";
const table = useTable(props.meta, {
only: ["users", "meta"],
});
const filters = useTableFilters(table);
const sorting = useTableSorting(table);
const pagination = useTablePagination(table);
const selection = useTableSelection(table);
Each composable also supports inject fallback when used inside HeadlessTableProvider:
const filters = useTableFilters();
const sorting = useTableSorting();
const pagination = useTablePagination();
const selection = useTableSelection();
Provider/inject (optional)
<script setup lang="ts">
import {
HeadlessTableProvider,
HeadlessTableFilters,
HeadlessTableSorting,
HeadlessTablePagination,
} from "@inertify/table-vue";
defineProps<{ meta: any }>();
</script>
<template>
<HeadlessTableProvider :meta="meta" :only="['users', 'meta']">
<HeadlessTableFilters
v-slot="{ filters, getFilterValue, setFilterValue, applyFilters }"
>
<!-- Render your inputs/selects from filters metadata -->
</HeadlessTableFilters>
<HeadlessTableSorting v-slot="{ toggleSort, isSortedBy, activeDirection }">
<!-- Render sortable headers -->
</HeadlessTableSorting>
<HeadlessTablePagination
v-slot="{ page, lastPage, previous, next, setPerPage, perPageOptions }"
>
<!-- Render pager controls -->
</HeadlessTablePagination>
</HeadlessTableProvider>
</template>
Direct table API
import { useTable } from "@inertify/table-vue";
const table = useTable(props.meta, {
only: ["users", "meta"],
});
table.toggleSort("name");
table.setFilter("role", "admin");
table.visit();
table.toggleRowSelected(1);
table.areAllRowsSelected([1, 2, 3]);
table.clearSelection();
Row selection
Use HeadlessTableSelection for renderless row-selection state and helpers:
<HeadlessTableSelection
v-slot="{
isRowSelected,
toggleRowSelected,
toggleAllRowsSelected,
selectionCount,
}"
>
<!-- Use these helpers to build checkbox/select-all UI -->
</HeadlessTableSelection>
Selection state is client-side and automatically clears when table meta is refreshed.
Column-based head/cell rendering
HeadlessTableHeads and HeadlessTableCells support slot overrides by:
- Column name:
column-{key}(example:column-created_at) - Column type:
type-{type}(example:type-date,type-number)
Precedence is: column-name slot → type slot → default slot.
Column type is resolved from column.meta.type first, then inferred from filter input (date-range => date, number-range => number).
Renderless components
HeadlessTable and HeadlessPagination expose slot props only, so you can build any UI design system.
<HeadlessTable :meta="meta" v-slot="{ state, toggleSort, setFilter, visit }">
<button @click="toggleSort('name')">Sort by name</button>
<input :value="state.filters.name ?? ''" @input="setFilter('name', $event.target.value, { submit: false })" />
<button @click="visit()">Apply</button>
</HeadlessTable>
Example app (shadcn-vue)
A complete usage example with Laravel + Inertia + shadcn-vue components is available in:
examples/laravel-vue-shadcn
Query format
For table name users, the default query keys are:
users_pageusers_per_pageusers_sort(nameor-name)users_filters[name]=...
Range filters use nested from / to values:
users_filters[age][from]=18users_filters[age][to]=65users_filters[created_at][from]=2026-01-01users_filters[created_at][to]=2026-01-31
Filter::numberRange(...) and Filter::dateRange(...) apply inclusive bounds (>= from, <= to).
Customize this in config/inertify-table.php.