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

Livewire Datatables Laravel Package

arm092/livewire-datatables

Livewire Datatables builds searchable, sortable, filterable, and editable admin tables with Laravel, Livewire, and Tailwind CSS. Query with models or the query builder, format columns, add bulk actions, and use the built-in Apricode theme.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the runtime matrix first. The package requires Laravel 10–13, Livewire 4.3.5+, Tailwind and Alpine. If your app is still on Livewire 3, stop here — this is not a drop-in. Check composer show livewire/livewire before you commit to anything.

composer require arm092/livewire-datatables

Add the Alpine cloak rule to your app CSS, or every filter dropdown and column-visibility menu will flash on load:

[x-cloak] { display: none; }

Decide on publishing up front. You do not need to publish. The default Apricode theme is self-contained, needs no tailwind.config.js entries, and is scoped to the package's own controls so it won't bleed into your markup. Publish only if you have a concrete reason:

php artisan vendor:publish --provider="Arm092\LivewireDatatables\LivewireDatatablesServiceProvider"

Publishing gets you resources/livewire/datatables/* (for restyling) and a config file with the default date/time formats. Reasons to publish: the design must match an existing admin shell, you want to override the tooltip/link partials, or you need different global date formats. Otherwise leave it alone — published views become a merge burden on every upgrade.

Where to look first. Three places, in this order:

  1. config/livewire-datatables.php in the package (or published) — date/time formats.
  2. resources/views/livewire/datatables/ in the package — the actual partials behind linkTo, truncate, view, and column rendering. When output looks wrong, read these.
  3. The Column class source — the README's method table is good but the source is authoritative on defaults and chaining.

First use case — a throwaway read-only list. The fastest thing to do is inline the component in Blade. Use this to prove the install works before writing any class:

<livewire:datatable
    model="App\Models\User"
    name="all-users"
    include="id, name, email, created_at"
    dates="created_at"
/>

Load the page. If you get a working table with global search and sortable headers, the install is sound and you can move on to a real component.

Then generate the component you'll actually keep. The inline form is a demo tool; anything with filters, callbacks, or editing belongs in a class, because props-in-Blade doesn't scale past a handful of columns.

php artisan make:livewire-datatable users-table --model=user
# -> app/Livewire/Datatables/UsersTable.php with public $model = User::class

php artisan make:livewire-datatable reports.activity
# -> app/Livewire/Datatables/Reports/Activity.php

The class extends Arm092\LivewireDatatables\Livewire\LivewireDatatable (note the Livewire\ sub-namespace in the import — the README's import line is the one to copy verbatim, it's easy to get wrong) and returns columns from getColumns().


Implementation Patterns

Prefer builder() over $model

public $model is the convenience path. The moment you have a tenancy scope, a soft-delete policy, an active flag, or a join you always want, switch to builder():

public function builder(): Builder
{
    return User::query()
        ->where('users.tenant_id', auth()->user()->tenant_id)
        ->with('planet'); // eager-load whatever callbacks touch
}

Two reasons this matters more than it looks. First, Column::delete() resolves records through builder(), so a scoped builder is also your authorisation boundary for the delete button — a model policy is applied automatically, but only to rows your builder can reach. Second, the package adds its own joins for dot-notation relation columns; starting from a pre-constrained builder keeps that join logic anchored to the right base query. If you start from $model and then need a scope, you'll end up fighting the component instead of writing one line of where.

Also call ->with() for any relation a callback column dereferences. Without it you get per-row lazy loads and an N+1 that only shows up on the second page of results.

Choose the column type before the column methods

This is the single biggest productivity lever in the package. The type decides default formatting and what the filter UI becomes; the fluent methods only decorate on top.

Type Filter becomes Use when
Column text input free-text fields
NumberColumn numeric range IDs, money, counts
BooleanColumn yes/no flags, email_verified_at-style null checks
DateColumn / TimeColumn date / time range any date-ish column; also handles formatting
LabelColumn none fixed header + fixed content, no SQL executed
Column::index($this) none row numbering independent of data
Column::checkbox() none bulk selection
Column::delete() none per-row delete with policy check

If you pick the wrong base type you end up writing a callback to fake formatting that DateColumn::format() would have given you for free. A date stored as a string and rendered with Column::name('dob') will show raw ISO timestamps and filter as free text.

The callback + filterOn pair

Column::callback() is where this package earns its keep — you get full PHP for display without hand-writing table markup. But a derived display value has no SQL column to filter on, so you must tell it:

Column::callback(['name', 'allegiance'], function ($name, $allegiance) {
        return "{$name} is allied to {$allegiance}";
    })
    ->label('Allegiance')
    ->filterable(['Rebellion', 'Empire'])
    ->filterOn('users.allegiance'),

The rule: filterOn() takes raw SQL, not Eloquent dot notation. users.allegiance, never user.allegiance. Passing the Eloquent form silently produces wrong results or a SQL error. filterOn() also accepts an array, which the package combines with OR — useful when a display string is derived from two underlying columns.

For custom rendering that callbacks can't express cleanly, ->view('your.view') hands the column value, the whole row, and extra parameters to a Blade template. That's the escape hatch for badges, avatars, status pills — far better than concatenating HTML in a closure.

Relation columns: dot notation, with a performance asterisk

Column::name('planet.name')->label('Planet')->searchable(),

The package walks the relationship and adds the joins for you. For HasOne/BelongsTo this is clean. For HasMany/BelongsToMany/HasManyThrough it builds an aggregated subquery — correct, but it's a correlated subquery per row on a joined table. On a large table, sort or search on one of these and watch the query time. Mitigation: keep many-relation columns out of the default sort, mark them ->unsortable() if the sort is genuinely expensive, and test the generated SQL against production-shaped data. Grab the query with DB::listen() in a service provider during development rather than guessing.

Editing

Column::name('email')
    ->editable()
    ->rules(['required', 'email']),

rules() accepts an array, a string, or a closure — the closure form receives the current model, the field name, and the submitted value, which is the right place for "unique except this row" and "only editable by an admin" checks:

->rules(fn ($model, $field, $value) =>
    Rule::unique('users', 'email')->ignore($model->id))

Because editing writes straight to the model, anything the model doesn't guard against (mass-assignment, observers that assume web-form input) is now reachable from the table. Verify $fillable and any model events before shipping an editable column on a sensitive field.

Bulk actions

Column::checkbox() puts IDs into the public $selected array; Column::checkbox('some_uuid') if your key isn't id. From there you write the action yourself against $this->selected (validate the selection, re-query through builder() rather than trusting the IDs, then $this->dispatch('refreshLivewireDatatable') or reset the selection). The README advertises "Mass Action (Bulk) Support" without giving the API surface, so read the component source for whatever built-in helper exists before hand-rolling one — but do not trust submitted IDs. Re-resolve them through builder(), exactly as Column::delete() does.

Cross-component refresh

The component listens for refreshLivewireDatatable. Any sibling component that mutates the underlying data should fire it:

$this->dispatch('refreshLivewireDatatable')->to(UsersTable::class);

This is the glue that makes a drawer/modal edit form and the table coexist. Wire it up on day one; retrofitting it after three components depend on stale state is annoying.

Wide tables: groups and hideable

For anything over ~8 columns, lean on ->group('identity') plus ->hideable() and ->hide(). Groups toggle as a unit, so a "personal details" group can be collapsed for a compact default view without you building a settings panel. Set ->hide() on the low-value columns (long text, timestamps, internal IDs) and let power users turn them on.

Export

->exportCallback(fn ($value) => ...) reformats a cell for export; ->excludeFromExport() drops a column entirely. Set both at column-definition time, not at export time — the export path reuses the column array, so a column with a display callback but no exportCallback will export the mutated display string (with HTML, or the linkTo markup) rather than the raw value. This is the most common "why is my CSV full of HTML" bug.


Gotchas and Tips

The name prop is not optional in practice. Multiple tables on one page — or the same table class on two different pages — will collide without a unique name. Treat it like a route name: one per table, never reused. Symptom is cross-talk in filters, sorting, and column visibility that looks like a caching bug but isn't.

Read the source for the column method name. The README shows both "declare a public method columns" and code using getColumns(). The prose and the example disagree; the class in vendor/ is the tiebreaker. Same for hide vs hidden in the method table (->hide() in the prose, ->hidden() in one example) — grep the Column class before you spend twenty minutes on a method-not-found.

raw() requires an explicit alias. Column::raw("CONCAT(...) AS \Native Age`")— theAS aliasis mandatory, and the alias becomes what you reference infilterOn()` and sorting. Omit it and you get a confusingly-timed SQL error far from the column definition.

Livewire 4 root-element rule breaks published views. If you published views under an older version, datatable.blade.php and hide-column-multiselect.blade.php may have multiple root elements. After upgrading from 3.2.1 or earlier, refresh exactly those two files. Do not run vendor:publish --force — it will overwrite your customised views along with them. Diff and copy.

Tailwind purge will eat package classes if you have an aggressive content config. The default theme is designed to avoid this, but if you've narrowed content to resources/views/**, published views live in a path you may not be scanning. Add the package views to your content globs, or accept that you'll be publishing and inlining.

Column::delete() needs a policy or it's a footgun. It applies an existing model policy automatically — but "automatically" means "if one exists." Verify with a user who shouldn't be able to delete, not just an admin who can.

Date/time formats are global, not per column. They come from config. If one table needs Y-m-d and another needs d/m/Y H:i, pass the format explicitly to ->format('...') rather than publishing config and changing the default for the whole app.

link('/users/{{slug}}/edit', 'edit {{first_name}}') does string interpolation of row attributes. That's convenient and it is also an injection surface if any interpolated attribute is user-controlled and lands in an href. Keep interpolation to IDs and slugs; if you need arbitrary user content in a URL, drop to ->view() and use url() / proper escaping.

Debugging checklist, in the order that resolves issues fastest:

  1. Column not appearing? dump($this->getColumns()) in the component — is it in the array, and is it ->hide()n?
  2. Filter returns nothing? Check filterOn() for Eloquent dot notation, and check the filterable option values match the stored values exactly (case, whitespace).
  3. Sort does nothing on a callback column? It needs sortBy() pointing at a real column or expression.
  4. Slow page? DB::listen() and look for correlated subqueries from many-relation columns.
  5. Rendering glitches / flicker? Confirm [x-cloak] is in your compiled CSS, and confirm Alpine is loaded once.

Extension points, ranked by cost of maintenance: ->view() on a single column (cheapest, zero upgrade risk) → ->exportCallback() / custom Column subclass → published config → published views (most expensive; every upstream template change becomes a manual merge).

Assess the risk honestly. 30 stars, zero dependents, and a maintained-fork lineage means you are, functionally, adopting a dependency solo. Pin the exact version in composer.json, read the changelog on every bump, and keep your table components thin — column arrays and builder() only, no business logic buried in closures. If the package stalls, thin components can be rewritten against raw Livewire in an afternoon; components with logic smeared through twenty callbacks cannot.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo