Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Inertiajs Tables Laravel Query Builder Laravel Package

protonemedia/inertiajs-tables-laravel-query-builder

Inertia.js tables powered by Laravel Query Builder. Build interactive data tables with sorting, filtering, searching, and pagination, with server-side query handling and a Vue/React front end.

View on GitHub
Deep Wiki
Context7

Getting Started

This package puts a table definition on the Laravel side and a ready-made <Table> Vue 3 component on the Inertia side, with sorting, global/per-column search, filters, and pagination wired to spatie/laravel-query-builder. It is archived (last release Feb 2023), so treat the install as a pinned, frozen dependency rather than something you'll upgrade freely.

Minimal setup:

composer require protonemedia/inertiajs-tables-laravel-query-builder
npm install @protonemedia/inertiajs-tables-laravel-query-builder

It depends on spatie/laravel-query-builder and is built for Vue 3 + Inertia + Tailwind. Pin the spatie major that your version was tested against; later majors have shifted APIs.

Register the components globally in resources/js/app.js:

import { Table, TableCell } from '@protonemedia/inertiajs-tables-laravel-query-builder';

createInertiaApp({
  setup({ app, props, plugin }) {
    const vue = createApp({ render: () => h(App, props) }).use(plugin);
    vue.component('Table', Table);
    vue.component('TableCell', TableCell);
    return vue.mount(el);
  },
});

Add the package to your Tailwind content globs or every class inside the component gets purged:

content: [
  './resources/**/*.{vue,js}',
  './node_modules/@protonemedia/inertiajs-tables-laravel-query-builder/**/*.vue',
],

First use case — a sortable, searchable list page:

// app/Http/Controllers/UserController.php
use ProtoneMedia\LaravelQueryBuilderInertiaJs\InertiaTable;
use Spatie\QueryBuilder\QueryBuilder;

public function index()
{
    $users = QueryBuilder::for(User::class)
        ->allowedFilters(['name', 'email', 'role'])
        ->allowedSorts(['name', 'email', 'created_at'])
        ->defaultSort('-created_at')
        ->paginate(15)
        ->withQueryString();

    return Inertia::render('Users/Index', ['users' => $users])
        ->table(function (InertiaTable $table) {
            $table->column('name', 'Name', searchable: true, sortable: true);
            $table->column('email', 'Email', sortable: true);
            $table->column('role', 'Role');
            $table->column('created_at', 'Created', sortable: true);
            $table->column('actions', '');

            $table->filter('role', 'Role', [
                'admin' => 'Admin',
                'user'  => 'User',
            ]);
        });
}
<!-- resources/js/Pages/Users/Index.vue -->
<template>
  <Table :resource="users">
    <template #cell(actions)="{ item }">
      <Link :href="`/users/${item.id}/edit`">Edit</Link>
    </template>
  </Table>
</template>

<script setup>
import { Link } from '@inertiajs/vue3';
defineProps({ users: Object });
</script>

Where to look first: the backend InertiaTable API (column, filter, search/date/period helpers) and the props the <Table> component expects — then verify the actual query string your browser sends (?sort=-created_at&filter[role]=admin&page=2) so you know exactly which params the query builder must allow.

Implementation Patterns

Centralize the table definition. Inline closures in controllers get duplicated fast. Put each table in its own class and reuse it, or use the package's AppendsToTable trait so a controller/resource returns a configured InertiaTable. Either way, define columns once and render <Table> from a wrapper component so styling changes happen in one place.

Mirror allowed filters/sorts exactly. The table definition drives the UI; allowedFilters / allowedSorts drive what the query builder actually accepts. They are two separate lists — a column marked sortable: true with no matching allowedSorts entry silently does nothing (or 400s, depending on your exception handler). Keep them adjacent and review both in the same diff.

Custom cells are the extension point. Keep id/created_at as plain columns, and give anything interactive (actions, badges, avatars, status) a key like actions or status plus a #cell(key) slot. The slot receives the row (typically { item }) — log it once if you're unsure of the exact prop name in your version.

Filters with dynamic options. Filter options can be closures, so build them from the DB rather than hardcoding:

$table->filter('team_id', 'Team', fn () => Team::orderBy('name')->pluck('name', 'id')->all());

For date ranges, use the package's date/period filter helpers, then handle the corresponding filter[...] keys on the query-builder side with AllowedFilter::scope(...) or AllowedFilter::callback(...).

Relationship and computed columns. Sort/filter on relations via spatie's AllowedSort::custom(...) / AllowedFilter::scope(...), and eager load with ->with('team') or ->withCount('posts') in the same query builder chain. Because the component just reads fields off each row, anything you append or count becomes a normal column key.

Multiple tables on one page. Use a named table so the query params don't collide (sort vs table2_sort, etc.), and make sure each query builder instance only allows its own params. Verify in the network tab before shipping.

Filter state belongs in the URL. Always chain ->withQueryString() so pagination links retain filters, sorting, and search — that's what makes the table shareable and back-button friendly.

Gotchas and Tips

  • Archived since Feb 2023. No fixes for newer Inertia/Laravel/Tailwind majors, and 443 stars means a small community. Before adopting: install it, run one real table, and confirm it works on your Laravel/Inertia/Vue versions. Otherwise vendor the Vue component or reimplement the pattern on spatie/laravel-query-builder directly — the server-side definition style is the valuable part, not the component.
  • Tailwind purge is the #1 "it renders unstyled" cause. If the table looks like bare HTML, the content glob is missing (or you're on Tailwind 4 where content config moved to CSS @source).
  • Slot names must match column keys exactly. A typo in #cell(actions) versus column('action', ...) fails silently — you just get the raw value.
  • Search is a param, not magic. searchable: true produces a search input/param; you must accept it on the query builder (a global search scope or per-column AllowedFilters). Confirm the param name in devtools before debugging Eloquent.
  • Don't transform the paginator into a plain array. The component needs data, links, and meta. ->through(fn ($u) => [...]) preserves the paginator shape; ->get()->map(...) does not.
  • Debugging workflow: reproduce the request manually with ?filter[role]=admin&sort=-name&page=2 in the URL. If the raw query string works but the component doesn't, it's a Vue/slot problem; if the query string doesn't work, it's allowedFilters/allowedSorts.
  • Performance: searchable columns typically become LIKE '%term%', which defeats indexes on big tables. Prefer indexed exact filters, or restrict the search scope; paginate at 15–25 rows.
  • Escape hatch: wrap <Table> in your own <DataTable> component and extend InertiaTable with your own helper methods. That way, if/when you migrate off the archived package, your pages and controllers barely change.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings