alp-develop/laravel-livewire-tables
Reactive Livewire data tables for Laravel—search, sort, filter, paginate, export, and bulk actions with zero JavaScript. Supports Laravel 10–13, Livewire 3–4, PHP 8.1+, Tailwind or Bootstrap 4/5, plus dark mode and configurable themes.
getSelectedIds() now intersects client selectedIds with the live server query, preventing forged IDs from targeting unauthorized records.escapeCsvValue() before fputcsv().<script>, <iframe>, <object>, <embed>, <form>, <meta>, <link>, <base>, and <style> tags are stripped; on*= event handlers and javascript:/vbscript:/data: URIs in href/src/action attributes are also removed before rendering.!== null to instanceof Carbon — Carbon::createFromFormat() can return false, which would cause a fatal TypeError with the previous check.$search and TextFilter values are now capped at 200 characters to prevent excessive LIKE wildcard queries.perPage bypass: perPage is validated against $perPageOptions at State build time in render(), not only in the Livewire hook.flatpickr@4.6.13 with SHA-384 integrity and crossorigin="anonymous" attributes.apply() now skips queries where the submitted value is not a key in selectOptions.config('livewire-tables.colors.*') and dark mode overrides are validated against a safe CSS character allowlist before output inside the <style> tag.getSelectedIds() uses array_intersect against the live filtered query — bulk IDs injected from the client that are not present in the current result set are silently dropped.columns(), preventing injected sort fields from reaching SortStep.selectedIds, excludedIds, selectAllPages protected with #[Locked]: These properties are now marked with Livewire's #[Locked] attribute, preventing direct client-side mutation via wire protocol.[a-zA-Z0-9_.] are now rejected entirely instead of being silently stripped.resolveFilters() with an in-request $cachedFilters cache. The filters() method was previously called up to 5 times per Livewire request. All internal calls now go through resolveFilters().FilterStep builds a filterMap keyed by filter key in its constructor — filter lookups are now O(1) instead of O(n) on every apply() call.SortStep builds a columnMap keyed by field in its constructor — column lookups are now O(1) instead of O(n) on every apply() call.array_filter on each apply.buildAliasMap() result is cached per instance via $cachedAliasMap ??= — the regex-based map is built once instead of on every search.cachedColumns, cachedVisibleColumns, and cachedSearchableColumns ensure columns() is called once per request.getFilterByKey() O(1) via hash map: DataTableComponent builds a $cachedFilterMap on first call — all subsequent getFilterByKey() and resolveParentValue() lookups are O(1).buildExportQuery() now calls $this->getEngine()->applySteps() instead of constructing new SearchStep, FilterStep, and SortStep instances.buildSortChips() O(n) instead of O(n×m): Now builds a $columnByField map once before iterating sort fields, eliminating the inner loop.resolveOptions() caches dependent filter callback results per value within the request lifecycle.dehydrate() compares new state against the existing session before writing, avoiding unnecessary session writes on unchanged state.setEagerLoad(): Added setEagerLoad(array $relations) to HasConfiguration, allowing components to declare Eloquent relations to eager-load in configure(). Relations are applied in render() via $query->with().Engine is now non-final: Removed final modifier from Engine to allow subclassing for custom pipeline orchestration.getEngine() is now protected: getEngine() and $cachedEngine changed from private to protected, enabling subclasses to override the Engine instance.setEagerLoad() / getEagerLoad(): New protected/public pair on HasConfiguration for declaring eager-loaded relations per component.x-cloak added to the bulk actions toggle container and [x-cloak]{display:none!important} rule added to styles.blade.php, preventing the button from briefly appearing with wrong CSS classes before Alpine initializes.SortStepTest: field whitelist, regex sanitization, direction normalization.LikeEscapeTest extensions: 200-char truncation, field sanitization, custom search callback truncation passthrough.FilterStepTest extension: filter key whitelist validation.SelectFilterNormalizeTest extensions: integer key normalization, null value, type edge cases.ActionColumnTest: XSS injection rendered as escaped text, valid render.BladeColumnSecurityTest: raw HTML output by design, e() escape pattern, unescaped developer responsibility.ValidationTest: perPage bypass (2 tests), TOCTOU bulk ID protection (2 tests), sort field session injection.ExportSecurityTest extension: CSV tab/CR injection.EngineApplyStepsTest extension: Engine subclass extensibility.IntegrationTest: search+filter combined, bulk delete with active filter, bulk delete TOCTOU with filter, sort order, selectAllPages exclusions, getFilterByKey lookup.HasConfigurationTest (22 tests): setters/getters for CSS classes, debounce clamp, eagerLoad, perPageOptions, theme detection.HasFiltersTest (19 tests): clearFilters, removeFilter for all filter types, removeFilter cascade on dependent SelectFilters, applyFilter validation, filterHasActiveValue, getFilterValue.UpdatedTableFiltersTest (9 feature tests): updatedTableFilters via Livewire, selection reset, value normalization, dependent child filter clearing, clearFilters/removeFilter/applyFilter integration.HasSearchAndSortingTest (18 tests): hasSearch, clearSearch, updatedSearch truncation, sortBy toggle cycle, clearSort, clearSortField, isSortedBy, getSortDirection, getSortOrder.initialValue() no longer re-apply their default value when the page is reloaded after clicking "Clear All". Changed the condition in mount() from filterHasActiveValue() (which treated empty strings/arrays as "no value") to array_key_exists() (which respects cached empty state from a previous clear).dark_mode.selector from CSS selector (.lt-dark) to session key (lt-dark) in config/livewire-tables.php and demo/config/livewire-tables.php. The selector is now used as a Laravel session key, not a CSS class.DataTableComponent::boot() now reads dark mode state from the session (session($selector)) instead of relying on a #[Reactive] property passed from parent components. Removed #[Reactive] attribute and changed $darkMode type from ?bool to bool.dark-mode-changed event with browser-native lt-dark-toggled event in demo.js. Removed onDarkModeChanged() listener and $darkMode property from DemoPage.php. Removed :dark-mode bindings from Livewire component tags in demo-page.blade.php.x-data/x-on:lt-dark-toggled wrapper in table.blade.php for instant client-side dark mode toggle without server round trip.styles.blade.php.demo/composer.json package name from alvitres01/laravel-livewire-tabless to alp-develop/laravel-livewire-tables.--tag=livewire-tables-lang to --tag=livewire-tables-translations in docs to match LivewireTablesServiceProvider.docs/theming.md to list bootstrap-5/bootstrap-4 as primary config values with aliases, consistent with docs/configuration.md.tableKey is now a public #[Locked] property: Changed from protected to public with Livewire's #[Locked] attribute. You can now pass table-key directly from Blade tags to isolate state when rendering multiple instances of the same table component: <livewire:users-table table-key="users-active" />.config/livewire-tables.php is required, not optional.table-key usage examples.docs/dark-mode.md guide covering configuration, toggling, session detection, $this->darkMode, color presets, and cross-theme support.dark_mode section, available themes table, and dark mode link to docs/configuration.md.=, +, -, @, tab, or carriage return are now prefixed with a single quote to prevent formula execution when opening exported CSV files in Excel or Google Sheets.src attributes are now validated against an allowlist of safe URI schemes (http://, https://, /). Values using javascript:, data:, or other dangerous schemes are rejected and the image is not rendered.wire:click action and class attributes in action buttons are now escaped with htmlspecialchars() to prevent attribute breakout and XSS.Js::from() for safe JavaScript value encoding instead of raw string interpolation. Also uses dynamic getKeyName() instead of hardcoded 'id'.applyFieldSearch() no longer falls back to the unsanitized field name on preg_replace failure. If sanitization fails or produces an empty string, the field is skipped entirely.[^a-zA-Z0-9_.] regex as search fields before being passed to orderBy(), preventing SQL injection through crafted column identifiers.loadStateFromCache() now validates sortFields, tableFilters, and hiddenColumns against defined columns and filters when restoring from session, matching the validation already applied during dehydrate().QueryException when sorting with BladeColumn auto-generated field: BladeColumn::sortable() is now a no-op — marking a BladeColumn as sortable is silently ignored since it has no real DB column. Additionally, resolveColumns() now resets BladeColumn::$bladeCounter before each cache build, ensuring consistent _blade_N field IDs across multiple component instances and Octane environments where the static counter could accumulate between requests.Filter::applyFilter() when closure returns null: The applyFilter() method now guards against user-defined filter closures that omit an explicit return statement. If the callback returns a non-Builder value (including null), the original $query is returned instead, preventing a fatal TypeError: Return value must be of type Builder, null returned.form-check-input (Bootstrap-specific) to lt-bulk-checkbox (livewire-tables-specific). This prevents style collisions with frameworks like AdminLTE, Argon, or any other theme that defines .form-check-input with conflicting styles. All themes (Tailwind, Bootstrap 5, Bootstrap 4) now use the dedicated lt-bulk-checkbox class with theme colors (primary color when checked) via CSS variables for consistent dark mode support.BladeColumn cells that use position-relative internally (e.g. action buttons, toggles). Fixed by ensuring the dropdown has an explicit z-index that takes precedence over positioned cell content.$darkMode during Livewire update: Changed $darkMode property type from bool to ?bool so Livewire can safely hydrate null (e.g. when a row is deleted and the component re-syncs state through middleware), preventing a fatal TypeError: Cannot assign null to property of type bool.fr), German (de), Italian (it), Dutch (nl), Polish (pl), Russian (ru), Chinese Simplified (zh), Japanese (ja), Korean (ko), Turkish (tr), and Indonesian (id). Total bundled locales: 14. Publishable via php artisan vendor:publish --tag=livewire-tables-translations.languages.php per locale.__('demo.key') translations for all 14 locales.<select> dropdown with 35 countries, replacing the free-text input.TextColumn::make('users.email')) now correctly display values when the SELECT query uses a custom alias (e.g. users.email as user_email). Previously, display resolution only worked when the alias matched the auto-derived table_column pattern. The engine now falls back to the bare column name when the derived alias key is absent from the result row.disableMobile: true) instead of falling back to native date inputs on mobile devices.overflow-y-auto from Tailwind filter-dropdown).[@click](https://github.com/click).outside and closes the filter panel.--lt-bg-card, --lt-border, --lt-text, --lt-opt-*) for proper dark mode support across all themes..lt-select, .form-select, .custom-select) now use a light SVG fill in dark mode instead of the default dark fill. Fixed background shorthand resetting arrow positioning.lt-page-hide-mobile class).Livewire.hook('commit') to persist Alpine state across component re-renders triggered by parent morphs.removeFilter() now properly clears child filters that depend on the removed parent filter. Fixed comparison bug in updatedTableFilters() where field name was incorrectly compared to filter key.wire:model.live.debounce.500ms instead of instant updates, with a clear (X) button.initialValue() filters now dispatch table-filters-applied on mount so parent components receive the initial filter state.lt-dropdown-opened event.wire:key to all filter blade components.min-width:22rem for a more comfortable layout.card-header class, now uses rounded-top with border-bottom for cleaner styling.Core
SearchStep, FilterStep, SortStep applied in sequence per requestState value object carrying search, filters, sort fields, per-page, and pageEngine processes the pipeline and returns paginated resultsCompat utility for runtime PHP, Laravel, and Livewire version detectionColumn Types
TextColumn — plain text with optional format closureBooleanColumn — yes/no with visual badge renderingDateColumn — date display with configurable format stringImageColumn — image display with optional lightbox previewActionColumn — row action buttons with label, CSS class, and JS action callbackBladeColumn — custom Blade view or render closure per cellTextColumn::make() (no field required) with render(Closure) supportsortable(), searchable(), label(), format(), headerClass(), cellClass(), hidden() on all columnsFilter Types
TextFilter — free-text LIKE searchSelectFilter — single or multiple selection; supports searchable() and parent-child dependency with dependsOn() / dependsOnValues()BooleanFilter — true/false toggleNumberFilter — numeric input with min, max, and step constraintsNumberRangeFilter — min/max range with two inputsDateFilter — single date picker (Flatpickr)DateRangeFilter — from/to date range (Flatpickr)MultiDateFilter — multiple individual dates (Flatpickr)filter(Closure) on any filter typedefaultValue()Pagination & Per-Page
setDefaultPerPage(), setPerPageOptions(), setPerPageVisibility()Sorting
setDefaultSort(), setDefaultSortDirection()Search
searchable() columnssetSearchDebounce()clearSearch() resets search and dispatches filter eventBulk Actions
getSelectedIds() resolves the correct ID list regardless of selection modebulkActions() arrayexportCsvAuto built-in bulk action for instant CSV downloadCSV Export
exportCsvAuto action generates CSV from all visible text/date/boolean columnsexportCsv(Closure $headings, Closure $row) for full controlEvents & Lifecycle Hooks
table-filters-applied dispatched on every filter change, filter removal, clear-all, and search change; payload: tableKey, filters, search{tableKey}-refresh or global livewire-tables:refresh events$refreshEvent propertylisteners() methodonQuerying(Builder), onQueried(Collection), onRendering(), onRendered()Toolbar Slots
beforeToolbar, afterToolbar, beforeSearch, afterSearch, beforeFilters, afterFilterssetSlot(string $slot, string $view, array $data)Themes
TailwindTheme — default theme using self-contained lt-* CSS classes (no Tailwind scanning required)Bootstrap5Theme — Bootstrap 5 compatible themeBootstrap4Theme — Bootstrap 4 compatible themeThemeContract--lt-primary-50 through --lt-primary-700) for primary color customization$darkMode prop passed from parent componentThemeManager driver registry; switch theme via config or per table with setTheme()headerClass(), cellClass(), setTableClass(), etc.setTrClass(Closure) or stringState Persistence
Artisan Generator
php artisan make:livewiretable UsersTable User generates ready-to-edit scaffoldmake:livewiretable Admin/UsersTable UserInternationalization
en), Spanish (es), Portuguese (pt)php artisan vendor:publish --tag=livewire-tables-langDeveloper Experience
docker/test.shCompatibility
How can I help you explore Laravel packages today?