mrcatz/datatable
A Laravel Livewire DataTable + Form Builder that bundles pagination, sorting, filters, smart search, inline editing, bulk actions, exports, and CRUD scaffolding into one composable API, so you can ship admin pages in minutes while keeping your own Tailwind/DaisyUI stack.
Install and wire the shell first. The package is a Livewire 3+ / Tailwind / DaisyUI framework, so the three layout directives are non-negotiable. Missing any of them causes silent failures (no table, no form modal, no toasts) rather than a visible error — budget an hour for this step alone the first time.
composer require mrcatz/datatable
In resources/views/layouts/app.blade.php, before </body>:
@include('mrcatz::components.ui.notification')
@livewireScripts
@stack('scripts')
If you're on Breeze/Jetstream/Filament, @livewireScripts and @stack('scripts') are probably already there — you only need the notification include. Then add the vendor blade path to your Tailwind content scan, or every class the package emits gets purged:
/* resources/css/app.css — Tailwind v4 */
@source '../../vendor/mrcatz/**/*.blade.php';
Where to look first. The hosted docs at datatable.catzoid.tech are the real reference — the README is a tour, not a manual. Read the Quick Start, then the Columns page, then Inline Editing. The repo itself is thin (readme, changelog, docs), so the docs site and the generated scaffolding are your two sources of truth.
First use case: scaffold one read-only listing before you build anything else.
php artisan mrcatz:make Product --path=Admin
This generates a Page component, a Table component, and the supporting classes under App\Livewire\Admin\Product\. Register it:
Route::get('/admin/products', \App\Livewire\Admin\Product\ProductPage::class);
Do this against a real model with real data on day one. The value proposition — "Filament's CRUD productivity without giving up your stack" — only becomes legible once you've seen a working table with pagination, sorting, and search on your own schema. Resist the urge to hand-write the page component before seeing what the generator produces; the generated structure encodes the intended separation between the Page (business logic, hooks) and the Table (presentation, columns).
Optional dependencies are genuinely optional — add maatwebsite/excel and barryvdh/laravel-dompdf only when the first export request arrives. Keep laravel/scout + Meilisearch out of scope until the built-in search demonstrably fails on your data volume; the Meilisearch driver is explicitly beta.
Treat the generated Page component as your controller and the Table component as your view. The split is the load-bearing architectural decision in this package. Page owns baseQuery(), saveData(), dropData(), prepareEditData(); Table owns column definitions and presentation. When a requirement doesn't fit, the first question should be "which of the two owns this?" — not "how do I override the template?"
Columns: define once, get everything. withColumn() in the Table carries an editable flag, formatting, image handling, and search/sort participation. The fluent builder is the whole API surface for a listing:
MrCatzDataTables::make()
->withColumn('name', __('Name'), sortable: true, searchable: true)
->withColumn('price', __('Price'), sortable: true)
->withColumn('status', __('Status'), filterable: true, editable: true)
->withColumnImage('image_url', __('Photo'), fallback: 'name');
Inline editing: use the hook, don't bypass it. Marking a column editable makes cells double-clickable and dispatches inlineUpdateData (constant: MrCatzEvent::INLINE_UPDATE). You implement onInlineUpdate($rowData, $columnKey, $newValue) on the Page. Put authorization and validation inside that hook — it's the only seam where a client-supplied write lands. Per-row gating is supported, so gate editability on policy checks rather than hiding columns.
Bulk actions follow the same shape. Selected-row state and UI come free (via the HasBulkActions trait); your job is the handler. Conditional per-row selection is supported, which is the correct place to express "you may only bulk-archive things you own" — not a post-hoc filter in the handler.
Filters: prefer the typed factories over callbacks. Select, callback, dependent, and date/range filters all exist with operator support. Dependent filters (country → city) are the one pattern worth learning properly because hand-rolling cascading filter state is exactly the boilerplate this package is selling. Use callbacks only for genuinely bespoke where clauses.
Form Builder for add/edit, mirroring the table's shape. Define forms in PHP with chainable modifiers, sections, and conditional fields. If your form and your table define the same domain twice with different field lists, you've reintroduced the drift the package exists to remove — keep them adjacent in the same component directory so divergence is visible in code review.
Search is a feature, not a LIKE. Multi-keyword highlighting, per-column relevance scoring, and typo tolerance are on by default and materially change what users expect from every other table in your app. If you ship one page with fuzzy multi-keyword search and the rest with naive LIKE, the inconsistency reads as a bug.
URL state is the integration point with the rest of your app. Search, sort, filter, pagination, multi-sort, and hidden columns (col_hidden) persist in the URL. This means deep links into filtered views "just work" — lean on it for support tickets, dashboards, and notification links instead of building a separate saved-view feature.
Exports inherit the current view. Export respects the active filters and search. Communicate this to users, because the alternative assumption ("export = whole table") is common and generates support noise.
Localization: publish the lang files if you ship in more than English/Indonesian.
php artisan vendor:publish --tag=mrcatz-lang
Use the mrcatz_lang() helper in any custom blade you write so replacement keys behave consistently.
The silent-failure triangle. Table renders but is unstyled → Tailwind isn't scanning vendor/mrcatz. Table doesn't render at all → missing @livewireScripts / @stack('scripts'). Form modal opens but nothing happens, or toasts never appear → missing notification include. These three account for most "it doesn't work" reports and produce no console errors. Check them before debugging anything else.
Use MrCatzEvent constants, never string literals. 'refresh-data', 'inlineUpdateData', and friends are stable strings today, but the constants class was added precisely because magic strings rot. Same for method signatures: the 1.0.2 changelog removed strict types from override-able methods (saveData(), dropData(), prepareEditData(), baseQuery(), setTable(), setFilter(), onFilterChanged()) to avoid Declaration compatibility fatal errors. If you override these, do not add return types or parameter types — you will break on upgrade, and the package intentionally won't enforce them.
Table-prefixed column keys are a recurring bug class. In 1.29.5, withColumnImage('demo_products.image_url') rendered the fallback initial instead of the image, because the callback looked up the prefixed key on a row object where the property is unprefixed. Fixed now, and getExpandView() image/text readers got the same treatment — but the pattern generalizes: if you're mixing qualified keys (table.column) and unqualified ones, test image rendering and expand views explicitly rather than assuming they behave like text columns.
Card-grouped sections hide fields. 1.29.9 fixed dateRange() being silently dropped when a section used ->asCard(). The lesson: any field you place inside ->asCard() sections deserves a visual check in the rendered form. Silent field omission is worse than a broken field, and the multi-card vs. single-grid render paths have historically diverged.
Native min/max on pickers does not clamp — until it does. The changelogs for 1.29.7/1.29.8/1.29.9 tell a story: min/max attributes are only validated on submit for time(), datetime-local(), month, and year, so users could scroll or type past the range and still submit. Each required a JS clamp that re-dispatches input/change to sync Livewire. If you're on an older version, assume range constraints are advisory. If you're on current, know that these clamps are JS-side and therefore not your server-side validation — always revalidate ranges in saveData().
Validate on the server regardless of what the form enforces. Every clamp above is client-side. The Form Builder's conditional fields and native attributes are UX, not security. saveData() is where rules belong.
Filter presets use localStorage. Persistence is per-browser, not per-user. The package degrades gracefully in private browsing (1.1.0), which means presets silently don't persist there — don't promise "saved filters follow you across devices."
Extension points, ranked. In rough order of how often you'll reach for them: baseQuery() (scoping, eager loading, tenant isolation — put with() here or you'll N+1 on every page), onInlineUpdate() (auth + validation), bulk action handlers, onRowClick() (drawer/detail navigation), onFilterChanged() (cascade side effects), and finally column/field formatting callbacks. Overriding package blades should be a last resort — you inherit maintenance on every upgrade, and this package ships frequently (1.29.x is a fast patch cadence).
On maturity: 12 stars, zero dependents, largely AI-assisted. That's not disqualifying — the changelog is detailed, versioned, and honest about fixes, and the feature surface is genuinely broad. But you are an early adopter on a single-maintainer package. Concretely: pin your version in composer.json rather than floating, read the changelog before every bump (the 1.29.x series contains several silent-failure fixes you want), and avoid building your only admin surface on it until you've lived with it for a sprint. The override hooks and the Page/Table split mean the exit cost is moderate, not catastrophic — keep your baseQuery() and business logic in Page methods that don't import package namespaces, and you can port to a hand-rolled table or Filament later without touching your domain code.
Watch for the Livewire 4 and Laravel 13 claims. The README advertises Laravel 11–13 and Livewire 3–4, but CI badges and changelog coverage suggest the tested matrix is narrower. Verify against your actual combination in a branch before committing a project to it.
How can I help you explore Laravel packages today?