starfolksoftware / inertia-table
Inertia.js Front-end Components for Spatie's Laravel Query Builder
Requires
- php: ^8.1|^8.2
- illuminate/support: ^9.37|^10.0|^11.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.8
- inertiajs/inertia-laravel: ^0.6.9
- orchestra/testbench: ^7.0|^8.0
- phpunit/phpunit: ^9.5
README
Features
- Auto-fill: auto generates
thead
andtbody
with support for custom cells - Global Search
- Search per field
- Select filters
- Toggle columns
- Sort columns
- Pagination (support for Eloquent/API Resource/Simple/Cursor)
- Automatically updates the query string (by using Inertia's replace feature)
Compatibility
- Vue 3
- Laravel 9
- Inertia.js
- Tailwind CSS v3 + Forms plugin
- PHP 8.0+
Note: There is currently an issue with using this package with Vite!
Installation
You need to install both the server-side package and the client-side package. Note that this package is only compatible with Laravel 9, Vue 3.0, and requires the Tailwind Forms plugin.
Server-side installation (Laravel)
You can install the package via composer:
composer require starfolksoftware/inertia-table
The package will automatically register the Service Provider which provides a table
method you can use on an Interia Response.
Search fields
With the searchInput
method, you can specify which attributes are searchable. Search queries are passed to the URL query as a filter
. This integrates seamlessly with the filtering feature of the Laravel Query Builder package.
Though it's enough to pass in the column key, you may specify a custom label and default value.
use InertiaTable\InertiaTable; Inertia::render('Page/Index')->table(function (InertiaTable $table) { $table->searchInput('name'); $table->searchInput( key: 'framework', label: 'Find your framework', defaultValue: 'Laravel' ); });
Select Filters
Select Filters are similar to search fields but use a select
element instead of an input
element. This way, you can present the user a predefined set of options. Under the hood, this uses the same filtering feature of the Laravel Query Builder package.
The selectFilter
method requires two arguments: the key, and a key-value array with the options.
Inertia::render('Page/Index')->table(function (InertiaTable $table) { $table->selectFilter('language_code', [ 'en' => 'Engels', 'nl' => 'Nederlands', ]); });
The selectFilter
will, by default, add a no filter option to the array. You may disable this or specify a custom label for it.
Inertia::render('Page/Index')->table(function (InertiaTable $table) { $table->selectFilter( key: 'language_code', options: $languages, label: 'Language', defaultValue: 'nl', noFilterOption: true, noFilterOptionLabel: 'All languages' ); });
Columns
With the column
method, you can specify which columns you want to be toggleable, sortable, and searchable. You must pass in at least a key or label for each column.
Inertia::render('Page/Index')->table(function (InertiaTable $table) { $table->column('name', 'User Name'); $table->column( key: 'name', label: 'User Name', canBeHidden: true, hidden: false, sortable: true, searchable: true ); });
The searchable
option is a shortcut to the searchInput
method. The example below will essentially call $table->searchInput('name', 'User Name')
.
Global Search
You may enable Global Search with the withGlobalSearch
method, and optionally specify a placeholder.
Inertia::render('Page/Index')->table(function (InertiaTable $table) { $table->withGlobalSearch(); $table->withGlobalSearch('Search through the data...'); });
If you want to enable Global Search for every table by default, you may use the static defaultGlobalSearch
method, for example, in the AppServiceProvider
class:
InertiaTable::defaultGlobalSearch(); InertiaTable::defaultGlobalSearch('Default custom placeholder'); InertiaTable::defaultGlobalSearch(false); // disable
Example controller
<?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\Support\Collection; use Inertia\Inertia; use InertiaTable\InertiaTable; use Spatie\QueryBuilder\AllowedFilter; use Spatie\QueryBuilder\QueryBuilder; class UserIndexController { public function __invoke() { $globalSearch = AllowedFilter::callback('global', function ($query, $value) { $query->where(function ($query) use ($value) { Collection::wrap($value)->each(function ($value) use ($query) { $query ->orWhere('name', 'LIKE', "%{$value}%") ->orWhere('email', 'LIKE', "%{$value}%"); }); }); }); $users = QueryBuilder::for(User::class) ->defaultSort('name') ->allowedSorts(['name', 'email', 'language_code']) ->allowedFilters(['name', 'email', 'language_code', $globalSearch]) ->paginate() ->withQueryString(); return Inertia::render('Users/Index', [ 'users' => $users, ])->table(function (InertiaTable $table) { $table ->withGlobalSearch() ->defaultSort('name') ->column(key: 'name', searchable: true, sortable: true, canBeHidden: false) ->column(key: 'email', searchable: true, sortable: true) ->column(key: 'language_code', label: 'Language') ->column(label: 'Actions') ->selectFilter(key: 'language_code', label: 'Language', options: [ 'en' => 'English', 'nl' => 'Dutch', ]); } }
Client-side installation (Inertia)
You can install the package via either npm
or yarn
:
npm install @starfolksoftware/inertia-table --save yarn add @starfolksoftware/inertia-table
Add the repository path to the content
array of your Tailwind configuration file. This ensures that the styling also works on production builds.
module.exports = { content: [ './node_modules/@starfolksoftware/inertia-table/**/*.{js,vue}', ] }
Table component
To use the Table
component and all its related features, you must import the Table
component and pass the users
data to the component.
<script setup> import { Table } from "@starfolksoftware/inertia-table"; defineProps(["users"]) </script> <template> <Table :resource="users" /> </template>
The resource
property automatically detects the data and additional pagination meta data. You may also pass this manually to the component with the data
and meta
properties:
<template> <Table :data="users.data" :meta="users.meta" /> </template>
If you want to manually render the table, like in v1 of this package, you may use the head
and body
slot. Additionally, you can still use the meta
property to render the paginator.
<template> <Table :meta="users"> <template #head> <tr> <th>User</th> </tr> </template> <template #body> <tr v-for="(user, key) in users.data" :key="key" > <td>{{ user.name }}</td> </tr> </template> </Table> </template>
The Table
has some additional properties to tweak its front-end behaviour.
<template> <Table :striped="true" :prevent-overlapping-requests="false" :input-debounce-ms="1000" :preserve-scroll="true" /> </template>
Custom column cells
When using auto-fill, you may want to transform the presented data for a specific column while leaving the other columns untouched. For this, you may use a cell template. This example is taken from the Example Controller above.
<template> <Table :resource="users"> <template #cell(actions)="{ item: user }"> <a :href="`/users/${user.id}/edit`"> Edit </a> </template> </Table> </template>
Multiple tables per page
You may want to use more than one table component per page. Displaying the data is easy, but using features like filtering, sorting, and pagination requires a slightly different setup. For example, by default, the page
query key is used for paginating the data set, but now you want two different keys for each table. Luckily, this package takes care of that and even provides a helper method to support Spatie's query package. To get this to work, you need to name your tables.
Let's take a look at Spatie's QueryBuilder
. In this example, there's a table for the companies and a table for the users. We name the tables accordingly. So first, call the static updateQueryBuilderParameters
method to tell the package to use a different set of query parameters. Now, filter
becomes companies_filter
, column
becomes companies_column
, and so forth. Secondly, change the pageName
of the database paginator.
InertiaTable::updateQueryBuilderParameters('companies'); $companies = QueryBuilder::for(Company::query()) ->defaultSort('name') ->allowedSorts(['name', 'email']) ->allowedFilters(['name', 'email']) ->paginate(pageName: 'companiesPage') ->withQueryString(); InertiaTable::updateQueryBuilderParameters('users'); $users = QueryBuilder::for(User::query()) ->defaultSort('name') ->allowedSorts(['name', 'email']) ->allowedFilters(['name', 'email']) ->paginate(pageName: 'usersPage') ->withQueryString();
Then, we need to apply these two changes to the InertiaTable
class. There's a name
and pageName
method to do so.
return Inertia::render('TwoTables', [ 'companies' => $companies, 'users' => $users, ])->table(function (InertiaTable $inertiaTable) { $inertiaTable ->name('users') ->pageName('usersPage') ->defaultSort('name') ->column(key: 'name', searchable: true) ->column(key: 'email', searchable: true); })->table(function (InertiaTable $inertiaTable) { $inertiaTable ->name('companies') ->pageName('companiesPage') ->defaultSort('name') ->column(key: 'name', searchable: true) ->column(key: 'address', searchable: true); });
Lastly, pass the correct name
property to each table in the Vue template. Optionally, you may set the preserve-scroll
property to table-top
. This makes sure to scroll to the top of the table on new data. For example, when changing the page of the second table, you want to scroll to the top of the table, instead of the top of the page.
<script setup> import { Table } from "@starfolksoftware/inertia-table"; defineProps(["companies", "users"]) </script> <template> <Table :resource="companies" name="companies" preserve-scroll="table-top" /> <Table :resource="users" name="users" preserve-scroll="table-top" /> </template>
Pagination translations
You can override the default pagination translations with the setTranslations
method. You can do this in your main JavaScript file:
import { setTranslations } from "@starfolksoftware/inertia-table"; setTranslations({ next: "Next", no_results_found: "No results found", of: "of", per_page: "per page", previous: "Previous", results: "results", to: "to" });
Table.vue slots
The Table.vue
has several slots that you can use to inject your own implementations.
Each slot is provided with props to interact with the parent Table
component.
<template> <Table> <template v-slot:tableGlobalSearch="slotProps"> <input placeholder="Custom Global Search Component..." @input="slotProps.onChange($event.target.value)" /> </template> </Table> </template>
Testing
A huge Laravel Dusk E2E test-suite can be found in the app
directory. Here you'll find a Laravel + Inertia application.
cd app
cp .env.example .env
composer install
npm install
npm run production
touch database/database.sqlite
php artisan migrate:fresh --seed
php artisan dusk:chrome-driver
php artisan serve
php artisan dusk
Upgrading from v1
Server-side
- The
addColumn
method has been renamed tocolumn
. - The
addFilter
method has been renamed toselectFilter
. - The
addSearch
method has been renamed tosearchInput
. - For all renamed methods, check out the arguments as some have been changed.
- The
addColumns
andaddSearchRows
methods have been removed. - Global Search is not enabled by default anymore.
Client-side
- The
InteractsWithQueryBuilder
mixin has been removed and is no longer needed. - The
Table
component no longer needs thefilters
,search
,columns
, andon-update
properties. - When using a custom
thead
ortbody
slot, you need to provide the styling manually. - When using a custom
thead
, theshowColumn
method has been renamed toshow
. - The
setTranslations
method is no longer part of thePagination
component, but should be imported. - The templates and logic of the components are not separated anymore. Use slots to inject your own implementations.
v2.1 Roadmap
- Boolean filters
- Date filters
- Date range filters
- Switch to Vite for the demo app
Changelog
Please see CHANGELOG for more information what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security related issues, please email faruk@starfolksoftware.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.