inertify/table

Headless tables for Laravel + Vue + Inertia with pagination, sorting, and filtering.
19
Install
composer require inertify/table
Latest Version:v1.5.3
PHP:^8.2
License:MIT
Last Updated:Apr 1, 2026
Links: GitHub  ·  Packagist
Maintainer: enkot

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

  1. Make sure your package name in composer.json is final (already set to inertify/table).
  2. Commit and push main to a public Git repository.
  3. Create a semantic version tag and push it:
git tag v1.0.0
git push origin v1.0.0
  1. Sign in to Packagist, click Submit, and paste your repository URL.
  2. In repository settings (GitHub/GitLab), add Packagist webhook so updates happen automatically.
  3. 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

  1. In npm org inertify, create a granular access token with publish permissions for @inertify/table-vue and 2FA bypass for automation.
  2. In GitHub repo settings, add secret NPM_TOKEN with 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_page
  • users_per_page
  • users_sort (name or -name)
  • users_filters[name]=...

Range filters use nested from / to values:

  • users_filters[age][from]=18
  • users_filters[age][to]=65
  • users_filters[created_at][from]=2026-01-01
  • users_filters[created_at][to]=2026-01-31

Filter::numberRange(...) and Filter::dateRange(...) apply inclusive bounds (>= from, <= to).

Customize this in config/inertify-table.php.