Table
Editing & Column-Level Filters
Beyond the dedicated Filter classes, any column can have an inline filter in its header.
Column-Level Filtering
Beyond the dedicated Filter classes, any column can have an inline filter in its header.
// Select filter in column header (pick one)TextColumn::make('status') ->filterable() ->filterAsSelect(['active' => 'Active', 'inactive' => 'Inactive']) // Multi-select filter (pick several → matches any, whereIn). Renders the same// searchable combobox as the wire-forms Select — search is on by default.BadgeColumn::make('role') ->filterAsMultiSelect([ 'admin' => 'Administrator', 'editor' => 'Editor', 'viewer' => 'Viewer', ], 'Any role') ->filterSearchable(false) // opt out of the search box for a short list // Boolean filterBooleanColumn::make('is_active') ->filterable() ->filterAsBoolean() // Date range filterTextColumn::make('created_at') ->filterable() ->filterAsDateRange() // Number range filterTextColumn::make('price') ->filterable() ->filterAsNumberRange(0, 10000) // min, max, optional step // Custom filter logicTextColumn::make('name') ->filterable() ->filterUsing(fn (Builder $query, mixed $value) => $query->where('name', 'like', "%{$value}%")) ->filterDebounce(500) // Filter with operatorTextColumn::make('age') ->filterable() ->filterOperator('>=')
Column-Level Filter API
A column header filter is a placement of a canonical Filter — the filterAs*()
helpers are thin factories over TextFilter / SelectFilter / DateFilter /
NumberRangeFilter / TernaryFilter, or pass a ready one with ->filter(). See
Column-Level Filters for the shared engine, chips
and query-string persistence.
->filterable(bool $filterable = true, string $type = 'text', array|string $options = [])->isFilterable(): bool->filter(Filter $filter) // attach a ready-made canonical filter->getFilter(): ?Filter->filterAsSelect(array|string $options, ?string $placeholder = null) // single value; searchable combobox->filterAsMultiSelect(array|string $options, ?string $placeholder = null) // several values (whereIn); searchable combobox->filterSearchable(bool $condition = true) // toggle the in-panel search (on by default)->filterAsDate(string|DateTimeInterface|null $minDate = null, string|DateTimeInterface|null $maxDate = null)->filterAsDateRange(string|DateTimeInterface|null $minDate = null, string|DateTimeInterface|null $maxDate = null)->filterAsNumberRange(?float $min = null, ?float $max = null, ?float $step = null)->filterAsBoolean(?string $trueLabel = null, ?string $falseLabel = null)->filterOperator(string $operator) // '=', '!=', '>', '<', '>=', '<=', 'like' (default, partial match), 'starts_with', 'ends_with'->filterDebounce(int $ms)->filterPlaceholder(?string $placeholder)->filterUsing(Closure $fn) // fn(Builder $query, mixed $value)
Inline Editing
Columns can also use the generic editable() API (in addition to dedicated TextInputColumn/SelectColumn/ToggleColumn):
TextColumn::make('name') ->editable() // type defaults to 'text' ->editableRules(fn ($record) => ['required', 'max:255']) ->editableUsing(function ($record, $column, $value) { $record->update([$column => $value]); }) TextColumn::make('category') // editable(enabled, type, options) — 'text' | 'select' | 'toggle' ->editable(true, 'select', ['a' => 'Category A', 'b' => 'Category B']) ->editableRules(fn ($record) => ['required', 'in:a,b'])
The options argument of both editable(type: 'select', …) and filterable() /
filterAsSelect() accepts a PHP enum class as well — it expands to value => label exactly
like the dedicated SelectColumn/SelectFilter. See Enum Options.
How inline saves work
Saving a cell (updateTableCell) deliberately does not re-render the table — a DOM morph
would reset the Alpine state of every editable cell. Instead each cell updates its own appearance
optimistically and reconciles with the server, via one shared Alpine component
(wireEditableCell): text inputs, selects and toggles all use it, so they behave consistently.
- Optimistic + rollback. The cell shows the new value immediately, then calls the server; if the save fails (validation, permission, error) it rolls back to the last server-confirmed value and surfaces the message.
- Optimistic locking. Each edit carries the row's version (
updated_at). If the row changed since the page loaded, the save is rejected as a conflict: the cell loads the current value and shows the conflict message inline on the cell itself (a red state on the text/select/toggle, no toast orNotificationManagersetup required) — so two people (or two quick edits that bump the row) can't silently clobber each other. Polling refreshes each cell's version on the next cycle. Opt in to also raise a (more prominent) toast for conflicts withTable::notifyEditConflicts()— this one needs the notification system wired up (a toast container); the inline message works without it. - Server-side authorization. The client
disabled()state is only cosmetic — a per-recorddisabled()cell (and any column permission) is enforced again on the server inupdateTableCell, so a forged request can't write to a locked cell.