├── LICENSE.md ├── README.md ├── UPGRADE.md ├── composer.json ├── resources └── views │ ├── bar │ ├── bar.blade.php │ ├── buttons │ │ ├── clear-search.blade.php │ │ └── reordering.blade.php │ ├── dropdowns │ │ ├── actions.blade.php │ │ ├── columns.blade.php │ │ ├── filters.blade.php │ │ ├── polling.blade.php │ │ └── trashed.blade.php │ ├── search.blade.php │ └── selection.blade.php │ ├── columns │ ├── content │ │ ├── boolean.blade.php │ │ ├── default.blade.php │ │ └── image.blade.php │ ├── footer │ │ └── default.blade.php │ ├── header │ │ └── default.blade.php │ └── search │ │ ├── boolean.blade.php │ │ ├── date.blade.php │ │ ├── default.blade.php │ │ └── select.blade.php │ ├── components │ ├── button.blade.php │ └── dropdown.blade.php │ ├── filters │ ├── boolean.blade.php │ ├── date.blade.php │ ├── filter.blade.php │ └── select.blade.php │ ├── livewire │ └── livewire-table.blade.php │ ├── pagination │ └── pagination.blade.php │ └── table │ └── table.blade.php └── src ├── Actions ├── Action.php ├── BaseAction.php └── Concerns │ └── CanBeStandalone.php ├── Columns ├── BaseColumn.php ├── BooleanColumn.php ├── Column.php ├── Concerns │ ├── CanBeClickable.php │ ├── CanBeRaw.php │ ├── HasData.php │ ├── HasFooter.php │ ├── HasFormat.php │ ├── HasHeader.php │ ├── HasSearch.php │ ├── HasSize.php │ ├── HasSorting.php │ ├── HasValue.php │ └── HasVisibility.php ├── DateColumn.php ├── ImageColumn.php ├── SelectColumn.php └── ViewColumn.php ├── Concerns ├── CanBeComputed.php ├── CanBeMultiple.php ├── CanBeQualified.php ├── HasActions.php ├── HasColumns.php ├── HasDeferredLoading.php ├── HasFilters.php ├── HasIdentifier.php ├── HasInitialization.php ├── HasMetadata.php ├── HasOptions.php ├── HasPagination.php ├── HasPolling.php ├── HasQueryString.php ├── HasRelations.php ├── HasReordering.php ├── HasSearch.php ├── HasSelect.php ├── HasSelection.php ├── HasSession.php ├── HasSoftDeletes.php ├── HasSorting.php └── Makeable.php ├── Enums ├── Direction.php ├── SearchScope.php └── TrashedMode.php ├── Exceptions ├── BaseException.php └── ColumnException.php ├── Filters ├── BaseFilter.php ├── BooleanFilter.php ├── Concerns │ └── HasFilter.php ├── DateFilter.php └── SelectFilter.php ├── Livewire └── LivewireTable.php ├── ServiceProvider.php └── Support └── Column.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present Ramon Rietdijk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livewire Tables 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ramonrietdijk/livewire-tables/tests.yml?label=tests)](https://github.com/ramonrietdijk/livewire-tables/actions/workflows/tests.yml) 4 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ramonrietdijk/livewire-tables.svg)](https://packagist.org/packages/ramonrietdijk/livewire-tables) 6 | 7 | Easily create dynamic tables for models with Laravel Livewire. 8 | 9 | ![Livewire Table](./art/table.webp) 10 | 11 | If you enjoy using this package, please consider leaving a star. 12 | 13 | ## Demo 14 | 15 | See the [Livewire Table](https://livewire-tables.ramonrietdijk.nl) in action. 16 | 17 | ## Documentation 18 | 19 | Open the [documentation](https://ramonrietdijk.github.io/livewire-tables) to see how this package can be used with 20 | examples. 21 | 22 | ## Testing 23 | 24 | To make sure everything works, run the following command: 25 | 26 | ```sh 27 | composer quality 28 | ``` 29 | 30 | ## Credits 31 | 32 | - [Ramon Rietdijk](https://github.com/ramonrietdijk) 33 | - [All Contributors](../../contributors) 34 | 35 | ## License 36 | 37 | This package is released under the [MIT](LICENSE.md) license. 38 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | Guide to upgrade the package to the next major versions. 4 | 5 | ## Upgrading to 5.x from 4.x 6 | 7 | Laravel 12 is supported since version 5.x. Support for Laravel 11 and PHP version 8.2 have both been dropped. 8 | 9 | The property `$useSelection` has been removed from the `HasSelection` trait as the selection will automatically be disabled when no actions have been added. 10 | 11 | The signature of methods `search` and `applySearch` in the `HasSearch` trait of columns has been changed. It now has a `SearchScope` as the second parameter. The callback for `searchable` on columns has not been changed. Access to the scope is given via the third parameter of your callback. 12 | 13 | The default implementation of searching explicitly checks if the input is a string now. 14 | 15 | All views have been updated to support Tailwind 4. 16 | 17 | ## Upgrading to 4.x from 3.x 18 | 19 | Laravel 11 is supported since version 4.x. Support for Laravel 10 and PHP version 8.1 have both been dropped. 20 | 21 | The `mount` method has been renamed to `mountHasInitialization` and is moved to the `HasInitialization` concern. If you are overriding the `mount` method, make sure to remove `parent::mount()` as it's no longer available. 22 | 23 | The `$key` parameter of methods `updatedFilters` and `updatedSearch` have been made nullable. If you are overriding these methods, make sure to update the method signature. 24 | 25 | ## Upgrading to 3.x from 2.x 26 | 27 | Support for Livewire 3 has been introduced in version 3.x. 28 | 29 | To be consistent with the default configuration of Livewire 3, the Livewire Table has been moved outside the `Http` namespace. 30 | 31 | Please, update all references to this class with the new namespace. 32 | 33 | ```php 34 | use RamonRietdijk\LivewireTables\Http\Livewire\LivewireTable; 35 | 36 | // Should be replaced with 37 | 38 | use RamonRietdijk\LivewireTables\Livewire\LivewireTable; 39 | ``` 40 | 41 | ## Upgrading to 2.x from 1.x 42 | 43 | In version 2.x of this package the dependency of `blade-ui-kit/blade-heroicons` has been dropped. All icons are now displayed using the SVG directly. If you have published the views of this package you should either update all icons to SVG instead of the blade component or install `blade-ui-kit/blade-heroicons` again manually. 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ramonrietdijk/livewire-tables", 3 | "description": "Dynamic tables for models with Laravel Livewire", 4 | "type": "package", 5 | "license": "MIT", 6 | "keywords": [ 7 | "Laravel", 8 | "Livewire", 9 | "Table" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Ramon Rietdijk", 14 | "email": "info@ramonrietdijk.nl" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.3", 19 | "laravel/framework": "^12.0", 20 | "livewire/livewire": "^3.0" 21 | }, 22 | "require-dev": { 23 | "larastan/larastan": "^3.1", 24 | "laravel/pint": "^1.21", 25 | "orchestra/testbench": "^10.0", 26 | "phpstan/phpstan-strict-rules": "^2.0", 27 | "phpunit/phpunit": "^11.5" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "RamonRietdijk\\LivewireTables\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "RamonRietdijk\\LivewireTables\\Tests\\": "tests", 37 | "RamonRietdijk\\LivewireTables\\Tests\\Database\\Factories\\": "tests/database/factories" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "phpunit", 42 | "analyse": "phpstan --memory-limit=256M", 43 | "style": "pint --test", 44 | "quality": [ 45 | "@test", 46 | "@analyse", 47 | "@style" 48 | ] 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "RamonRietdijk\\LivewireTables\\ServiceProvider" 54 | ] 55 | } 56 | }, 57 | "config": { 58 | "sort-packages": true 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true 62 | } 63 | -------------------------------------------------------------------------------- /resources/views/bar/bar.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @includeWhen($this->canSearch(), 'livewire-table::bar.search') 4 |
5 | 6 |
7 | @include('livewire-table::bar.selection') 8 |
9 | @includeWhen($this->useReordering, 'livewire-table::bar.buttons.reordering') 10 | @include('livewire-table::bar.dropdowns.polling') 11 | @include('livewire-table::bar.dropdowns.columns') 12 | @include('livewire-table::bar.dropdowns.filters') 13 | @include('livewire-table::bar.dropdowns.actions') 14 | @include('livewire-table::bar.dropdowns.trashed') 15 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /resources/views/bar/buttons/clear-search.blade.php: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /resources/views/bar/buttons/reordering.blade.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/bar/dropdowns/actions.blade.php: -------------------------------------------------------------------------------- 1 | @if($table['actions']->isNotEmpty()) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | @php($standaloneActions = $table['actions']->filter(fn($action): bool => $action->isStandalone())) 11 | @if($standaloneActions->isNotEmpty()) 12 |
13 | 15 | @lang('Standalone Actions') 16 | 17 | @foreach($standaloneActions as $standaloneAction) 18 | @if($standaloneAction->callback() === null) 19 | 27 | @else 28 | 34 | @endif 35 | @endforeach 36 |
37 | @endif 38 | 39 | @php($actions = $table['actions']->filter(fn($action): bool => ! $action->isStandalone())) 40 | @if($actions->isNotEmpty()) 41 |
42 | 44 | @lang('Actions') 45 | 46 | @foreach($actions as $action) 47 | @if($action->callback() === null) 48 | 58 | @else 59 | 67 | @endif 68 | @endforeach 69 |
70 | @endif 71 |
72 |
73 | @endif 74 | -------------------------------------------------------------------------------- /resources/views/bar/dropdowns/columns.blade.php: -------------------------------------------------------------------------------- 1 | @if($table['columns']->isNotEmpty()) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | @lang('Columns') 12 | 13 | @lang('All') 14 | 15 | 16 | @lang('None') 17 | 18 | 19 | @foreach($table['columns'] as $column) 20 | 38 | @endforeach 39 | 40 | @endif 41 | -------------------------------------------------------------------------------- /resources/views/bar/dropdowns/filters.blade.php: -------------------------------------------------------------------------------- 1 | @if($table['filters']->isNotEmpty()) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | @lang('Filters') 12 | @if ($this->canClearFilters()) 13 | 14 | @lang('Clear') 15 | 16 | @endif 17 | 18 |
19 | @foreach($table['filters'] as $filter) 20 |
21 | {{ $filter->render() }} 22 |
23 | @endforeach 24 |
25 |
26 | @endif 27 | -------------------------------------------------------------------------------- /resources/views/bar/dropdowns/polling.blade.php: -------------------------------------------------------------------------------- 1 | @if(count($pollingOptions) > 0) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | @lang('Polling') 12 | 13 |
14 |
15 | 16 | @lang('Refresh records') 17 | 18 | 25 |
26 |
27 |
28 | @endif 29 | -------------------------------------------------------------------------------- /resources/views/bar/dropdowns/trashed.blade.php: -------------------------------------------------------------------------------- 1 | @if($this->hasSoftDeletes()) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | @lang('Trashed') 12 | 13 |
14 |
15 | 16 | @lang('Show') 17 | 18 | 25 |
26 |
27 |
28 | @endif 29 | -------------------------------------------------------------------------------- /resources/views/bar/search.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 6 | @includeWhen($this->canClearSearch(), 'livewire-table::bar.buttons.clear-search') 7 |
8 | -------------------------------------------------------------------------------- /resources/views/bar/selection.blade.php: -------------------------------------------------------------------------------- 1 | @if(count($this->selected) > 0) 2 |
3 |
5 | 6 | 7 | 8 | 9 | 10 | @choice('Selected 1 record|Selected :value records', count($this->selected), ['value' => count($this->selected)]) 11 | 12 | 23 |
24 |
25 | @endif 26 | -------------------------------------------------------------------------------- /resources/views/columns/content/boolean.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($value === null) 3 |
4 | @elseif($value) 5 |
6 | @else 7 |
8 | @endif 9 |
10 | -------------------------------------------------------------------------------- /resources/views/columns/content/default.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($value === null) 3 | 4 | @elseif($column->isRaw()) 5 | {!! $value !!} 6 | @else 7 | {{ $value }} 8 | @endif 9 |
10 | -------------------------------------------------------------------------------- /resources/views/columns/content/image.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($value) 3 | {{ $column->label() }} 10 | @endif 11 |
12 | -------------------------------------------------------------------------------- /resources/views/columns/footer/default.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if(($content = $column->getFooterContent()) !== null) 3 | {!! $content !!} 4 | @else 5 |   6 | @endif 7 |
8 | -------------------------------------------------------------------------------- /resources/views/columns/header/default.blade.php: -------------------------------------------------------------------------------- 1 | @if($column->isSortable()) 2 | 5 | {{ $column->label() }} 6 | @if(! $this->isReordering()) 7 | @if($this->sortColumn === $column->code()) 8 | @if($this->sortDirection === 'asc') 9 | 10 | 11 | 12 | 13 | @else 14 | 15 | 16 | 17 | 18 | @endif 19 | @else 20 | 21 | 22 | 23 | 24 | @endif 25 | @endif 26 | 27 | @else 28 | {{ $column->label() }} 29 | @endif 30 | -------------------------------------------------------------------------------- /resources/views/columns/search/boolean.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | -------------------------------------------------------------------------------- /resources/views/columns/search/date.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | -------------------------------------------------------------------------------- /resources/views/columns/search/default.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | -------------------------------------------------------------------------------- /resources/views/columns/search/select.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 18 |
19 | -------------------------------------------------------------------------------- /resources/views/components/button.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active' => false]) 2 | 3 | 18 | -------------------------------------------------------------------------------- /resources/views/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['icon', 'label', 'active' => false]) 2 | 3 |
4 | 10 | {{ $icon ?? '' }} 11 | 12 | 19 |
20 | -------------------------------------------------------------------------------- /resources/views/filters/boolean.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ $filter->label() }} 4 | 5 | 11 |
12 | -------------------------------------------------------------------------------- /resources/views/filters/date.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ $filter->label() }} 4 | 5 |
6 |
7 | @lang('From') 8 | 11 |
12 |
13 | @lang('To') 14 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /resources/views/filters/filter.blade.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramonrietdijk/livewire-tables/684a7e866cde98a7e383fb0c72a1118fb55a31c5/resources/views/filters/filter.blade.php -------------------------------------------------------------------------------- /resources/views/filters/select.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ $filter->label() }} 4 | 5 | 27 |
28 | -------------------------------------------------------------------------------- /resources/views/livewire/livewire-table.blade.php: -------------------------------------------------------------------------------- 1 |
polling()) > 0) wire:poll.{{ $polling }} @endif 4 | > 5 | @include('livewire-table::bar.bar') 6 |
8 | @include('livewire-table::table.table') 9 |
10 | {{ $paginator->links('livewire-table::pagination.pagination') }} 11 |
12 | -------------------------------------------------------------------------------- /resources/views/pagination/pagination.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if ($paginator->hasPages()) 3 | 4 | 150 | @endif 151 |
152 | -------------------------------------------------------------------------------- /resources/views/table/table.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @if($this->canSelect()) 5 | 8 | @endif 9 | @foreach($table['columns'] as $column) 10 | @continue(! in_array($column->code(), $this->columns)) 11 | 14 | @endforeach 15 | 16 | 17 | @if($this->canSelect()) 18 | 19 | @endif 20 | @foreach($table['columns'] as $column) 21 | @continue(! in_array($column->code(), $this->columns)) 22 | 27 | @endforeach 28 | 29 | 30 | 31 | @if($this->deferLoading && ! $this->initialized) 32 | 33 | 38 | 39 | @else 40 | @forelse($paginator->items() as $item) 41 | isReordering()) 45 | draggable="true" 46 | x-on:dragstart="e => e.dataTransfer.setData('key', '{{ $item->getKey() }}')" 47 | x-on:dragover.prevent="" 48 | x-on:drop="e => { 49 | $wire.call( 50 | 'reorderItem', 51 | e.dataTransfer.getData('key'), 52 | '{{ $item->getKey() }}', 53 | e.target.offsetHeight / 2 > e.offsetY 54 | ) 55 | }" 56 | @endif 57 | > 58 | @if($this->canSelect()) 59 | 67 | @endif 68 | @foreach($table['columns'] as $column) 69 | @continue(! in_array($column->code(), $this->columns)) 70 | 87 | @endforeach 88 | 89 | @empty 90 | 91 | 96 | 97 | @endforelse 98 | @endif 99 | 100 | 101 | 102 | @if($this->canSelect()) 103 | 104 | @endif 105 | @foreach($table['columns'] as $column) 106 | @continue(! in_array($column->code(), $this->columns)) 107 | 110 | @endforeach 111 | 112 | 113 |
6 | 7 | 12 | {{ $column->renderHeader() }} 13 |
23 | @if($column->isSearchable()) 24 | {{ $column->renderSearch() }} 25 | @endif 26 |
34 | 35 | @lang('Fetching records...') 36 | 37 |
63 |
64 | 65 |
66 |
true, 73 | 'select-none cursor-pointer' => $column->isClickable() || $this->isReordering(), 74 | ]) 75 | @if($column->isClickable() && ! $this->isReordering()) 76 | @if(($link = $this->link($item)) !== null) 77 | x-on:click.prevent="window.location.href = '{{ $link }}'" 78 | @elseif($this->canSelect()) 79 | x-on:click="$wire.selectItem('{{ $item->getKey() }}')" 80 | @endif 81 | @endif 82 | x-bind:class="~selected.indexOf('{{ $item->getKey() }}') 83 | ? 'bg-blue-100 group-odd:bg-blue-100 group-hover:bg-blue-200 dark:bg-blue-900 dark:group-odd:bg-blue-900 dark:group-hover:bg-blue-800' 84 | : 'bg-neutral-100 group-odd:bg-white group-hover:bg-neutral-200 dark:bg-neutral-800 dark:group-odd:bg-neutral-900 dark:group-hover:bg-neutral-700'"> 85 | {{ $column->render($item) }} 86 |
92 | 93 | @lang('No results') 94 | 95 |
108 | {{ $column->renderFooter() }} 109 |
114 | -------------------------------------------------------------------------------- /src/Actions/Action.php: -------------------------------------------------------------------------------- 1 | label; 29 | } 30 | 31 | public function code(): string 32 | { 33 | return $this->code; 34 | } 35 | 36 | public function callback(): ?Closure 37 | { 38 | return $this->callback; 39 | } 40 | 41 | /** @param Enumerable $models */ 42 | public function execute(Enumerable $models): mixed 43 | { 44 | $callback = $this->callback(); 45 | 46 | if ($callback === null) { 47 | return null; 48 | } 49 | 50 | return call_user_func($callback, $models); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Actions/Concerns/CanBeStandalone.php: -------------------------------------------------------------------------------- 1 | standalone = $standalone; 14 | 15 | return $this; 16 | } 17 | 18 | public function isStandalone(): bool 19 | { 20 | return $this->standalone; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Columns/BaseColumn.php: -------------------------------------------------------------------------------- 1 | column = $column; 54 | $this->code = $code ?? Str::of($column)->replace('.', '_')->toString(); 55 | } else { 56 | $this->column = null; 57 | $this->code = $code ?? md5($label); 58 | $this->displayUsing = $column; 59 | $this->computed = true; 60 | } 61 | } 62 | 63 | public function label(): string 64 | { 65 | return $this->label; 66 | } 67 | 68 | public function column(): ?string 69 | { 70 | return $this->column; 71 | } 72 | 73 | public function code(): string 74 | { 75 | return $this->code; 76 | } 77 | 78 | public function render(Model $model): mixed 79 | { 80 | $value = $this->resolveValue($model); 81 | 82 | return view($this->view, [ 83 | 'column' => $this, 84 | 'value' => $value, 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Columns/BooleanColumn.php: -------------------------------------------------------------------------------- 1 | clickable = $clickable; 14 | 15 | return $this; 16 | } 17 | 18 | public function isClickable(): bool 19 | { 20 | return $this->clickable; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Columns/Concerns/CanBeRaw.php: -------------------------------------------------------------------------------- 1 | raw = $raw; 14 | 15 | return $this; 16 | } 17 | 18 | public function isRaw(): bool 19 | { 20 | return $this->raw; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasData.php: -------------------------------------------------------------------------------- 1 | */ 10 | protected array $data = []; 11 | 12 | /** @param string|array $key */ 13 | public function with(string|array $key, mixed $value = null): static 14 | { 15 | if (is_array($key)) { 16 | $this->data = $key; 17 | } else { 18 | $this->data[$key] = $value; 19 | } 20 | 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasFooter.php: -------------------------------------------------------------------------------- 1 | footerCallback = $footerCallback; 21 | 22 | return $this; 23 | } 24 | 25 | public function getFooterContent(): mixed 26 | { 27 | if ($this->footerCallback === null) { 28 | return null; 29 | } 30 | 31 | return call_user_func($this->footerCallback); 32 | } 33 | 34 | public function hasFooter(): bool 35 | { 36 | return $this->footerCallback() !== null; 37 | } 38 | 39 | public function footerCallback(): ?Closure 40 | { 41 | return $this->footerCallback; 42 | } 43 | 44 | public function renderFooter(): mixed 45 | { 46 | return view($this->footerView, ['column' => $this]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasFormat.php: -------------------------------------------------------------------------------- 1 | format = $format; 14 | 15 | return $this; 16 | } 17 | 18 | public function getFormat(): ?string 19 | { 20 | return $this->format; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasHeader.php: -------------------------------------------------------------------------------- 1 | header = $header; 19 | 20 | return $this; 21 | } 22 | 23 | public function hasHeader(): bool 24 | { 25 | return $this->header; 26 | } 27 | 28 | public function renderHeader(): mixed 29 | { 30 | if (! $this->hasHeader()) { 31 | return ''; 32 | } 33 | 34 | return view($this->headerView, ['column' => $this]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasSearch.php: -------------------------------------------------------------------------------- 1 | searchable = $searchable; 27 | $this->searchCallback = null; 28 | } else { 29 | $this->searchable = true; 30 | $this->searchCallback = $searchable; 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | public function isSearchable(): bool 37 | { 38 | return $this->searchable; 39 | } 40 | 41 | public function searchCallback(): ?Closure 42 | { 43 | return $this->searchCallback; 44 | } 45 | 46 | /** @param Builder $builder */ 47 | public function search(Builder $builder, SearchScope $scope, mixed $search): void 48 | { 49 | $this->qualifyQuery($builder, function (Builder $builder, string $column) use ($search): void { 50 | if (is_string($search)) { 51 | $builder->where($column, 'LIKE', '%'.$search.'%'); 52 | } 53 | }); 54 | } 55 | 56 | /** @param Builder $builder */ 57 | public function applySearch(Builder $builder, SearchScope $scope, mixed $search): void 58 | { 59 | if ($this->searchCallback !== null) { 60 | call_user_func($this->searchCallback, $builder, $search, $scope); 61 | 62 | return; 63 | } 64 | 65 | if (! $this->isComputed()) { 66 | $this->search($builder, $scope, $search); 67 | } 68 | } 69 | 70 | public function renderSearch(): mixed 71 | { 72 | return view($this->searchView, ['column' => $this]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasSize.php: -------------------------------------------------------------------------------- 1 | width = $width; 16 | $this->height = $height; 17 | 18 | return $this; 19 | } 20 | 21 | public function width(int $width): static 22 | { 23 | $this->width = $width; 24 | 25 | return $this; 26 | } 27 | 28 | public function height(int $height): static 29 | { 30 | $this->height = $height; 31 | 32 | return $this; 33 | } 34 | 35 | public function getWidth(): int 36 | { 37 | return $this->width; 38 | } 39 | 40 | public function getHeight(): int 41 | { 42 | return $this->height; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasSorting.php: -------------------------------------------------------------------------------- 1 | sortable = $sortable; 22 | $this->sortCallback = null; 23 | } else { 24 | $this->sortable = true; 25 | $this->sortCallback = $sortable; 26 | } 27 | 28 | return $this; 29 | } 30 | 31 | public function isSortable(): bool 32 | { 33 | return $this->sortable; 34 | } 35 | 36 | public function sortCallback(): ?Closure 37 | { 38 | return $this->sortCallback; 39 | } 40 | 41 | /** @param Builder $builder */ 42 | public function sort(Builder $builder, Direction $direction): void 43 | { 44 | $builder->orderBy($this->qualify($builder), $direction->value); 45 | } 46 | 47 | /** @param Builder $builder */ 48 | public function applySorting(Builder $builder, Direction $direction): void 49 | { 50 | if ($this->sortCallback !== null) { 51 | call_user_func($this->sortCallback, $builder, $direction); 52 | 53 | return; 54 | } 55 | 56 | if (! $this->isComputed()) { 57 | $this->sort($builder, $direction); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasValue.php: -------------------------------------------------------------------------------- 1 | displayUsing = $displayUsing; 19 | 20 | return $this; 21 | } 22 | 23 | public function displayUsingCallback(): ?Closure 24 | { 25 | return $this->displayUsing; 26 | } 27 | 28 | public function getValue(Model $model): mixed 29 | { 30 | $column = $this->column(); 31 | 32 | if ($column === null) { 33 | return $model; 34 | } 35 | 36 | $segments = Column::make($column)->segments(); 37 | 38 | $value = $model; 39 | 40 | foreach ($segments as $segment) { 41 | if ($value instanceof Collection) { 42 | $value = $value->pluck($segment); 43 | 44 | continue; 45 | } 46 | 47 | $value = data_get($value, str_replace('->', '.', $segment)); 48 | } 49 | 50 | return $value; 51 | } 52 | 53 | public function resolveValue(Model $model): mixed 54 | { 55 | $value = $this->getValue($model); 56 | 57 | if ($this->displayUsing !== null) { 58 | return call_user_func($this->displayUsing, $value, $model); 59 | } 60 | 61 | if ($value instanceof Collection) { 62 | $value = $value->toArray(); 63 | } 64 | 65 | if (is_array($value)) { 66 | $value = implode(', ', $value); 67 | } 68 | 69 | return $value; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Columns/Concerns/HasVisibility.php: -------------------------------------------------------------------------------- 1 | visible = $visible; 14 | 15 | return $this; 16 | } 17 | 18 | public function hide(): static 19 | { 20 | return $this->visible(false); 21 | } 22 | 23 | public function isVisible(): bool 24 | { 25 | return $this->visible; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Columns/DateColumn.php: -------------------------------------------------------------------------------- 1 | getValue($model); 21 | 22 | if ($this->displayUsing !== null) { 23 | return call_user_func($this->displayUsing, $value, $model); 24 | } 25 | 26 | if ($value === null) { 27 | return null; 28 | } 29 | 30 | /** @var Carbon $date */ 31 | $date = Carbon::parse($value); 32 | 33 | return $this->format === null 34 | ? $date->toDateTimeString() 35 | : $date->format($this->format); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Columns/ImageColumn.php: -------------------------------------------------------------------------------- 1 | $builder */ 19 | public function search(Builder $builder, SearchScope $scope, mixed $search): void 20 | { 21 | if ($scope === SearchScope::Global) { 22 | parent::search($builder, $scope, $search); 23 | 24 | return; 25 | } 26 | 27 | $this->qualifyQuery($builder, function (Builder $builder, string $column) use ($search): void { 28 | $builder->where($column, '=', $search); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Columns/ViewColumn.php: -------------------------------------------------------------------------------- 1 | column(); 21 | 22 | if ($this->displayUsing !== null) { 23 | $view = call_user_func($this->displayUsing, $model, $model); 24 | } 25 | 26 | if ($view === null) { 27 | return null; 28 | } 29 | 30 | return view($view) 31 | ->with('model', $model) 32 | ->with($this->data); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Concerns/CanBeComputed.php: -------------------------------------------------------------------------------- 1 | computed = $computed; 14 | 15 | return $this; 16 | } 17 | 18 | public function isComputed(): bool 19 | { 20 | return $this->computed; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Concerns/CanBeMultiple.php: -------------------------------------------------------------------------------- 1 | multiple = $multiple; 14 | 15 | return $this; 16 | } 17 | 18 | public function isMultiple(): bool 19 | { 20 | return $this->multiple; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Concerns/CanBeQualified.php: -------------------------------------------------------------------------------- 1 | qualifyUsingAlias = $qualifyUsingAlias; 20 | 21 | return $this; 22 | } 23 | 24 | public function shouldQualifyUsingAlias(): bool 25 | { 26 | return $this->qualifyUsingAlias; 27 | } 28 | 29 | /** @param Builder $builder */ 30 | public function qualify(Builder $builder): string 31 | { 32 | $column = $this->column(); 33 | 34 | if ($column === null) { 35 | throw new ColumnException('No column has been set'); 36 | } 37 | 38 | return Column::make($column)->qualify($builder); 39 | } 40 | 41 | /** @param Builder $builder */ 42 | public function qualifyQuery(Builder $builder, Closure $callback): void 43 | { 44 | $column = $this->column(); 45 | 46 | if ($column === null) { 47 | throw new ColumnException('No column has been set'); 48 | } 49 | 50 | $column = Column::make($column); 51 | 52 | if ($column->hasRelation() && ! $this->shouldQualifyUsingAlias()) { 53 | $builder->whereHas($column->relation(), function (Builder $builder) use ($callback, $column): void { 54 | call_user_func($callback, $builder, $builder->qualifyColumn($column->name())); 55 | }); 56 | } else { 57 | call_user_func($callback, $builder, $column->qualify($builder)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Concerns/HasActions.php: -------------------------------------------------------------------------------- 1 | */ 13 | protected function actions(): array 14 | { 15 | return [ 16 | // 17 | ]; 18 | } 19 | 20 | /** @return Enumerable */ 21 | protected function resolveActions(): Enumerable 22 | { 23 | return once(fn (): Enumerable => collect($this->actions())); 24 | } 25 | 26 | public function executeAction(string $code): mixed 27 | { 28 | /** @var BaseAction $action */ 29 | $action = $this->resolveActions()->firstOrFail(fn (BaseAction $action): bool => $code === $action->code()); 30 | 31 | $models = collect(); 32 | 33 | if (! $action->isStandalone() && count($this->selected) > 0) { 34 | $models = $this->query()->whereIn($this->model()->getQualifiedKeyName(), $this->selected)->get(); 35 | } 36 | 37 | $response = $action->execute($models); 38 | 39 | if ($response !== false) { 40 | if (! $action->isStandalone()) { 41 | $this->clearSelection(); 42 | } 43 | 44 | $this->dispatch('refreshLivewireTable'); 45 | } 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Concerns/HasColumns.php: -------------------------------------------------------------------------------- 1 | */ 13 | public array $columns = []; 14 | 15 | /** @var array */ 16 | public array $columnOrder = []; 17 | 18 | public function selectAllColumns(bool $select): void 19 | { 20 | /** @var array $columns */ 21 | $columns = $select 22 | ? $this->resolveColumns()->map(fn (BaseColumn $column): string => $column->code())->toArray() 23 | : []; 24 | 25 | $this->columns = $columns; 26 | 27 | $this->updateSession(); 28 | } 29 | 30 | public function reorderColumn(string $from, string $to, bool $above): void 31 | { 32 | if ($from === $to) { 33 | return; 34 | } 35 | 36 | $currentOrder = (int) array_search($from, $this->columnOrder, true); 37 | 38 | $toOrder = (int) array_search($to, $this->columnOrder, true); 39 | 40 | $up = $toOrder > $currentOrder; 41 | 42 | if ($above && $up) { 43 | $newOrder = $toOrder - 1; 44 | } elseif (! $above && ! $up) { 45 | $newOrder = $toOrder + 1; 46 | } else { 47 | $newOrder = $toOrder; 48 | } 49 | 50 | if ($newOrder === $currentOrder) { 51 | return; 52 | } 53 | 54 | $columnOrder = $this->columnOrder; 55 | 56 | $removedColumn = array_splice($columnOrder, $currentOrder, 1); 57 | 58 | array_splice($columnOrder, $newOrder, 0, $removedColumn); 59 | 60 | $this->columnOrder = array_values($columnOrder); 61 | 62 | $this->updateSession(); 63 | } 64 | 65 | protected function initializeColumns(): static 66 | { 67 | $columns = $this->resolveColumns(); 68 | 69 | if (count($this->columns) === 0) { 70 | /** @var array $visibleColumns */ 71 | $visibleColumns = $columns 72 | ->filter(fn (BaseColumn $column): bool => $column->isVisible()) 73 | ->map(fn (BaseColumn $column): string => $column->code()) 74 | ->values() 75 | ->toArray(); 76 | 77 | $this->columns = $visibleColumns; 78 | } 79 | 80 | /** @var array $codes */ 81 | $codes = $columns->map(fn (BaseColumn $column): string => $column->code())->toArray(); 82 | 83 | $this->columnOrder = array_unique(array_merge($this->columnOrder, $codes)); 84 | 85 | return $this; 86 | } 87 | 88 | /** @return array */ 89 | protected function columns(): array 90 | { 91 | return [ 92 | // 93 | ]; 94 | } 95 | 96 | /** @return Enumerable */ 97 | protected function resolveColumns(): Enumerable 98 | { 99 | return once(function (): Enumerable { 100 | return collect($this->columns()) 101 | ->sortBy(function (BaseColumn $column): int { 102 | return (int) array_search($column->code(), $this->columnOrder, true); 103 | })->values(); 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Concerns/HasDeferredLoading.php: -------------------------------------------------------------------------------- 1 | */ 15 | public array $filters = []; 16 | 17 | /** @return array */ 18 | protected function queryStringHasFilters(): array 19 | { 20 | if (! $this->useQueryString) { 21 | return []; 22 | } 23 | 24 | return [ 25 | 'filters' => [ 26 | 'as' => $this->getQueryStringName('filters'), 27 | ], 28 | ]; 29 | } 30 | 31 | public function updatedFilters(mixed $value, ?string $key): void 32 | { 33 | if (blank($value) && $key !== null) { 34 | unset($this->filters[$key]); 35 | } 36 | 37 | $this->resetPage(); 38 | } 39 | 40 | public function clearFilters(): void 41 | { 42 | $this->filters = []; 43 | 44 | $this->updateSession(); 45 | } 46 | 47 | protected function canClearFilters(): bool 48 | { 49 | return count($this->filters) > 0; 50 | } 51 | 52 | /** @return array */ 53 | protected function filters(): array 54 | { 55 | return [ 56 | // 57 | ]; 58 | } 59 | 60 | /** @return Enumerable */ 61 | protected function resolveFilters(): Enumerable 62 | { 63 | return once(fn (): Enumerable => collect($this->filters())); 64 | } 65 | 66 | /** @param Builder $builder */ 67 | protected function applyFilters(Builder $builder): static 68 | { 69 | $builder->where(function (Builder $builder): void { 70 | $this->resolveFilters()->each(function (BaseFilter $filter) use ($builder): void { 71 | $value = $this->filters[$filter->code()] ?? null; 72 | 73 | $builder->where(fn (Builder $builder) => $filter->applyFilter($builder, $value)); 74 | }); 75 | }); 76 | 77 | return $this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Concerns/HasIdentifier.php: -------------------------------------------------------------------------------- 1 | initialized = true; 14 | } 15 | 16 | public function mountHasInitialization(): void 17 | { 18 | $this->initialize(); 19 | } 20 | 21 | protected function initialize(): void 22 | { 23 | $this->restoreSession(); 24 | 25 | $this->initializeColumns(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Concerns/HasMetadata.php: -------------------------------------------------------------------------------- 1 | */ 10 | protected array $metadata = []; 11 | 12 | public function setMeta(string $key, mixed $value): static 13 | { 14 | $this->metadata[$key] = $value; 15 | 16 | return $this; 17 | } 18 | 19 | public function getMeta(string $key, mixed $default = null): mixed 20 | { 21 | return $this->metadata[$key] ?? $default; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Concerns/HasOptions.php: -------------------------------------------------------------------------------- 1 | */ 10 | protected array $options = []; 11 | 12 | /** @param array $options */ 13 | public function options(array $options = []): static 14 | { 15 | $this->options = $options; 16 | 17 | return $this; 18 | } 19 | 20 | /** @return array */ 21 | public function getOptions(): array 22 | { 23 | return $this->options; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Concerns/HasPagination.php: -------------------------------------------------------------------------------- 1 | */ 12 | protected array $perPageOptions = [ 13 | 15, 14 | 25, 15 | 50, 16 | 75, 17 | 100, 18 | ]; 19 | 20 | /** @return array */ 21 | protected function queryStringHasPagination(): array 22 | { 23 | if (! $this->useQueryString) { 24 | return []; 25 | } 26 | 27 | return [ 28 | 'perPage' => [ 29 | 'as' => $this->getQueryStringName('perPage'), 30 | ], 31 | ]; 32 | } 33 | 34 | public function updatingPaginators(): void 35 | { 36 | $this->selectedPage = false; 37 | } 38 | 39 | public function updatingPerPage(): void 40 | { 41 | $this->resetPage(); 42 | } 43 | 44 | protected function perPage(): int 45 | { 46 | $options = $this->perPageOptions(); 47 | 48 | if (! in_array($this->perPage, $options, true)) { 49 | return $options[0]; 50 | } 51 | 52 | return $this->perPage; 53 | } 54 | 55 | /** @return array */ 56 | protected function perPageOptions(): array 57 | { 58 | return $this->perPageOptions; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Concerns/HasPolling.php: -------------------------------------------------------------------------------- 1 | */ 12 | protected array $pollingOptions = []; 13 | 14 | /** @return array */ 15 | protected function queryStringHasPolling(): array 16 | { 17 | if (! $this->useQueryString) { 18 | return []; 19 | } 20 | 21 | return [ 22 | 'polling' => [ 23 | 'as' => $this->getQueryStringName('polling'), 24 | ], 25 | ]; 26 | } 27 | 28 | protected function polling(): string 29 | { 30 | if (! array_key_exists($this->polling, $this->pollingOptions())) { 31 | return ''; 32 | } 33 | 34 | return $this->polling; 35 | } 36 | 37 | /** @return array */ 38 | protected function pollingOptions(): array 39 | { 40 | return $this->pollingOptions; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Concerns/HasQueryString.php: -------------------------------------------------------------------------------- 1 | queryStringPrefix) > 0) { 16 | return $this->queryStringPrefix.'_'.$name; 17 | } 18 | 19 | return $name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Concerns/HasRelations.php: -------------------------------------------------------------------------------- 1 | $builder */ 21 | protected function applyRelations(Builder $builder): static 22 | { 23 | $columns = $this->resolveColumns() 24 | ->filter(fn (BaseColumn $column): bool => ! $column->isComputed()) 25 | ->map(fn (BaseColumn $column): ?string => $column->column()); 26 | 27 | $filters = $this->resolveFilters() 28 | ->filter(fn (BaseFilter $filter): bool => ! $filter->isComputed()) 29 | ->map(fn (BaseFilter $filter): ?string => $filter->column()); 30 | 31 | $allColumns = $columns 32 | ->merge($filters) 33 | ->filter(fn (?string $column): bool => $column !== null) 34 | ->unique() 35 | ->values(); 36 | 37 | $with = []; 38 | $join = []; 39 | $lookup = []; 40 | 41 | foreach ($allColumns as $column) { 42 | $segments = Column::make($column)->segments(); 43 | 44 | /** @var Collection $previous */ 45 | $previous = collect(); 46 | 47 | /** @var Model $model */ 48 | $model = $builder->getModel(); 49 | 50 | $alias = null; 51 | $shouldJoin = true; 52 | 53 | foreach ($segments as $segment) { 54 | $relation = $this->getEloquentRelation($model, $segment); 55 | 56 | if ($relation === null) { 57 | break; 58 | } 59 | 60 | $fullRelation = $previous->push($segment)->implode('.'); 61 | 62 | if (! in_array($fullRelation, $with, true)) { 63 | $with[] = $fullRelation; 64 | } 65 | 66 | if (! in_array($fullRelation, $join, true) && $shouldJoin) { 67 | $join[] = $fullRelation; 68 | 69 | $lookup[$fullRelation] = $this->joinEloquentRelation($builder, $relation, $model, $segment, $alias); 70 | } 71 | 72 | if (! isset($lookup[$fullRelation])) { 73 | $shouldJoin = false; 74 | } 75 | 76 | if ($shouldJoin) { 77 | /** @var string $alias */ 78 | $alias = $lookup[$fullRelation]; 79 | } 80 | 81 | $model = $relation->getRelated(); 82 | } 83 | } 84 | 85 | $builder->with($with); 86 | 87 | return $this; 88 | } 89 | 90 | /** @return ?Relation */ 91 | protected function getEloquentRelation(Model $model, string $relation): ?Relation 92 | { 93 | try { 94 | $reflectionMethod = new ReflectionMethod($model, $relation); 95 | 96 | if (! $reflectionMethod->isPublic()) { 97 | return null; 98 | } 99 | 100 | $relation = $model->$relation(); 101 | 102 | if (! ($relation instanceof Relation)) { 103 | return null; 104 | } 105 | 106 | return $relation; 107 | } catch (ReflectionException) { 108 | return null; 109 | } 110 | } 111 | 112 | /** 113 | * @param Builder $builder 114 | * @param Relation $relation 115 | */ 116 | protected function joinEloquentRelation(Builder $builder, Relation $relation, Model $model, string $name, ?string $parent = null): ?string 117 | { 118 | $method = 'join'.class_basename($relation); 119 | 120 | if (! method_exists($this, $method)) { 121 | return null; 122 | } 123 | 124 | return $this->$method($builder, $relation, $model, $name, $parent); 125 | } 126 | 127 | /** 128 | * @param Builder $builder 129 | * @param BelongsTo $relation 130 | */ 131 | protected function joinBelongsTo(Builder $builder, BelongsTo $relation, Model $model, string $name, ?string $parent = null): string 132 | { 133 | /** @var Model $subModel */ 134 | $subModel = $relation->getModel(); 135 | 136 | $alias = $parent !== null 137 | ? $parent.'_'.$name 138 | : $name; 139 | 140 | $parentTable = $parent ?? $model->getTable(); 141 | 142 | $builder->leftJoin( 143 | $subModel->getTable().' AS '.$alias, 144 | $alias.'.'.$relation->getOwnerKeyName(), 145 | '=', 146 | $parentTable.'.'.$relation->getForeignKeyName() 147 | ); 148 | 149 | return $alias; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Concerns/HasReordering.php: -------------------------------------------------------------------------------- 1 | */ 18 | protected function queryStringHasReordering(): array 19 | { 20 | if (! $this->useQueryString) { 21 | return []; 22 | } 23 | 24 | return [ 25 | 'reordering' => [ 26 | 'as' => $this->getQueryStringName('reordering'), 27 | ], 28 | ]; 29 | } 30 | 31 | public function updatedReordering(): void 32 | { 33 | $this->selected = []; 34 | $this->selectedPage = false; 35 | } 36 | 37 | public function reorderItem(string $from, string $to, bool $above): void 38 | { 39 | if ($from === $to) { 40 | return; 41 | } 42 | 43 | /** @var Model $from */ 44 | $from = $this->query()->findOrFail($from); 45 | 46 | /** @var Model $to */ 47 | $to = $this->query()->findOrFail($to); 48 | 49 | $column = $this->reorderingColumn; 50 | 51 | /** @var int $currentOrder */ 52 | $currentOrder = $from->getAttribute($column) ?? 0; 53 | 54 | /** @var int $toOrder */ 55 | $toOrder = $to->getAttribute($column) ?? 0; 56 | 57 | $up = $toOrder > $currentOrder; 58 | 59 | if ($above && $up) { 60 | $newOrder = $toOrder - 1; 61 | } elseif (! $above && ! $up) { 62 | $newOrder = $toOrder + 1; 63 | } else { 64 | $newOrder = $toOrder; 65 | } 66 | 67 | if ($newOrder === $currentOrder) { 68 | return; 69 | } 70 | 71 | if ($up) { 72 | // The new order is higher, meaning that everything between has to go down by one. 73 | $this 74 | ->query() 75 | ->where($column, '>', $currentOrder) 76 | ->where($column, '<=', $newOrder) 77 | ->decrement($column); 78 | } else { 79 | // The new order is lower, meaning that everything between has to go up by one. 80 | $this 81 | ->query() 82 | ->where($column, '<', $currentOrder) 83 | ->where($column, '>=', $newOrder) 84 | ->increment($column); 85 | } 86 | 87 | $from->setAttribute($column, $newOrder); 88 | $from->save(); 89 | } 90 | 91 | protected function isReordering(): bool 92 | { 93 | return $this->useReordering && $this->reordering; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Concerns/HasSearch.php: -------------------------------------------------------------------------------- 1 | */ 17 | public array $search = []; 18 | 19 | /** @return array */ 20 | protected function queryStringHasSearch(): array 21 | { 22 | if (! $this->useQueryString) { 23 | return []; 24 | } 25 | 26 | return [ 27 | 'globalSearch' => [ 28 | 'as' => $this->getQueryStringName('globalSearch'), 29 | ], 30 | 'search' => [ 31 | 'as' => $this->getQueryStringName('search'), 32 | ], 33 | ]; 34 | } 35 | 36 | public function updatedGlobalSearch(): void 37 | { 38 | $this->resetPage(); 39 | } 40 | 41 | public function updatedSearch(mixed $value, ?string $key): void 42 | { 43 | if (blank($value) && $key !== null) { 44 | unset($this->search[$key]); 45 | } 46 | 47 | $this->resetPage(); 48 | } 49 | 50 | protected function canSearch(): bool 51 | { 52 | return $this->resolveColumns()->filter(fn (BaseColumn $column): bool => $column->isSearchable())->isNotEmpty(); 53 | } 54 | 55 | public function clearSearch(): void 56 | { 57 | $this->globalSearch = ''; 58 | $this->search = []; 59 | 60 | $this->resetPage(); 61 | 62 | $this->updateSession(); 63 | } 64 | 65 | protected function canClearSearch(): bool 66 | { 67 | return strlen($this->globalSearch) > 0 || count($this->search) > 0; 68 | } 69 | 70 | /** @param Builder $builder */ 71 | protected function applyGlobalSearch(Builder $builder): static 72 | { 73 | if (strlen($this->globalSearch) === 0 || count($this->columns) === 0) { 74 | return $this; 75 | } 76 | 77 | $columns = $this->resolveColumns()->filter(function (BaseColumn $column): bool { 78 | return $column->isSearchable() && in_array($column->code(), $this->columns, true); 79 | }); 80 | 81 | $builder->where(function (Builder $builder) use ($columns): void { 82 | $columns->each(function (BaseColumn $column) use ($builder): void { 83 | $builder->orWhere(function (Builder $builder) use ($column) { 84 | $column->applySearch($builder, SearchScope::Global, $this->globalSearch); 85 | }); 86 | }); 87 | }); 88 | 89 | return $this; 90 | } 91 | 92 | /** @param Builder $builder */ 93 | protected function applyColumnSearch(Builder $builder): static 94 | { 95 | if (count($this->columns) === 0) { 96 | return $this; 97 | } 98 | 99 | $columns = $this->resolveColumns()->filter(function (BaseColumn $column): bool { 100 | return $column->isSearchable() && in_array($column->code(), $this->columns, true); 101 | }); 102 | 103 | $builder->where(function (Builder $builder) use ($columns): void { 104 | $columns->each(function (BaseColumn $column) use ($builder): void { 105 | $builder->where(function (Builder $builder) use ($column) { 106 | $search = $this->search[$column->code()] ?? null; 107 | 108 | if (! blank($search)) { 109 | $column->applySearch($builder, SearchScope::Column, $search); 110 | } 111 | }); 112 | }); 113 | }); 114 | 115 | return $this; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Concerns/HasSelect.php: -------------------------------------------------------------------------------- 1 | $builder */ 13 | protected function applySelect(Builder $builder): static 14 | { 15 | $builder->addSelect($builder->qualifyColumn('*')); 16 | 17 | return $this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Concerns/HasSelection.php: -------------------------------------------------------------------------------- 1 | */ 13 | public array $selected = []; 14 | 15 | public bool $selectedPage = false; 16 | 17 | public function updatingSelectedPage(bool $selectedPage): void 18 | { 19 | $this->selectPage($selectedPage); 20 | } 21 | 22 | public function clearSelection(): void 23 | { 24 | $this->selected = []; 25 | $this->selectedPage = false; 26 | 27 | $this->updateSession(); 28 | } 29 | 30 | public function selectItem(string $key): void 31 | { 32 | if (! $this->canSelect()) { 33 | return; 34 | } 35 | 36 | $selected = collect($this->selected); 37 | 38 | /** @var array $newSelection */ 39 | $newSelection = $selected->contains($key) 40 | ? $selected->diff([$key])->values()->toArray() 41 | : $selected->add($key)->toArray(); 42 | 43 | $this->selected = $newSelection; 44 | 45 | $this->updateSession(); 46 | } 47 | 48 | public function selectPage(bool $select): void 49 | { 50 | if (! $this->canSelect()) { 51 | return; 52 | } 53 | 54 | /** @var array $items */ 55 | $items = $this->paginate()->items(); 56 | 57 | $page = collect($items)->map(function (Model $model): string { 58 | /** @var int|string $key */ 59 | $key = $model->getKey(); 60 | 61 | return (string) $key; 62 | }); 63 | 64 | $selected = collect($this->selected); 65 | 66 | /** @var array $newSelection */ 67 | $newSelection = $select 68 | ? $selected->merge($page)->unique()->values()->toArray() 69 | : $selected->diff($page)->values()->toArray(); 70 | 71 | $this->selected = $newSelection; 72 | 73 | $this->updateSession(); 74 | } 75 | 76 | public function selectTable(bool $select): void 77 | { 78 | if (! $this->canSelect()) { 79 | return; 80 | } 81 | 82 | $table = $this->appliedQuery()->get()->map(function (Model $model): string { 83 | /** @var int|string $key */ 84 | $key = $model->getKey(); 85 | 86 | return (string) $key; 87 | }); 88 | 89 | $selected = collect($this->selected); 90 | 91 | /** @var array $newSelection */ 92 | $newSelection = $select 93 | ? $selected->merge($table)->unique()->values()->toArray() 94 | : $selected->diff($table)->values()->toArray(); 95 | 96 | $this->selected = $newSelection; 97 | $this->selectedPage = false; 98 | 99 | $this->updateSession(); 100 | } 101 | 102 | protected function canSelect(): bool 103 | { 104 | $hasActions = $this->resolveActions() 105 | ->filter(fn (BaseAction $action): bool => ! $action->isStandalone()) 106 | ->isNotEmpty(); 107 | 108 | return $hasActions && ! $this->isReordering(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Concerns/HasSession.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected array $sessionProperties = [ 15 | 'columns', 16 | 'columnOrder', 17 | ]; 18 | 19 | public function updatedHasSession(string $property): void 20 | { 21 | if (! $this->useSession) { 22 | return; 23 | } 24 | 25 | $property = Str::of($property)->before('.')->toString(); 26 | 27 | if (! in_array($property, $this->sessionProperties, true)) { 28 | return; 29 | } 30 | 31 | $this->updateSession(); 32 | } 33 | 34 | protected function sessionKey(): string 35 | { 36 | return $this->identifier().':configuration'; 37 | } 38 | 39 | protected function updateSession(): void 40 | { 41 | if (! $this->useSession) { 42 | return; 43 | } 44 | 45 | $data = []; 46 | 47 | foreach ($this->sessionProperties as $property) { 48 | if (property_exists($this, $property)) { 49 | $data[$property] = $this->{$property}; 50 | } 51 | } 52 | 53 | session()->put($this->sessionKey(), $data); 54 | } 55 | 56 | protected function restoreSession(): void 57 | { 58 | if (! $this->useSession) { 59 | return; 60 | } 61 | 62 | $data = session()->get($this->sessionKey()); 63 | 64 | if (is_array($data)) { 65 | $this->fill($data); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Concerns/HasSoftDeletes.php: -------------------------------------------------------------------------------- 1 | */ 16 | protected function queryStringHasSoftDeletes(): array 17 | { 18 | if (! $this->useQueryString) { 19 | return []; 20 | } 21 | 22 | return [ 23 | 'trashed' => [ 24 | 'as' => $this->getQueryStringName('trashed'), 25 | ], 26 | ]; 27 | } 28 | 29 | protected function hasSoftDeletes(): bool 30 | { 31 | return method_exists($this->model(), 'trashed'); 32 | } 33 | 34 | /** @param Builder $builder */ 35 | protected function applySoftDeletes(Builder $builder): static 36 | { 37 | if (! $this->hasSoftDeletes()) { 38 | return $this; 39 | } 40 | 41 | $trashedMode = TrashedMode::from($this->trashed); 42 | 43 | if ($trashedMode === TrashedMode::WithoutTrashed) { 44 | /** @phpstan-ignore-next-line */ 45 | $builder->withoutTrashed(); 46 | } 47 | 48 | if ($trashedMode === TrashedMode::WithTrashed) { 49 | /** @phpstan-ignore-next-line */ 50 | $builder->withTrashed(); 51 | } 52 | 53 | if ($trashedMode === TrashedMode::OnlyTrashed) { 54 | /** @phpstan-ignore-next-line */ 55 | $builder->onlyTrashed(); 56 | } 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Concerns/HasSorting.php: -------------------------------------------------------------------------------- 1 | */ 19 | protected function queryStringHasSorting(): array 20 | { 21 | if (! $this->useQueryString) { 22 | return []; 23 | } 24 | 25 | return [ 26 | 'sortColumn' => [ 27 | 'as' => $this->getQueryStringName('sortColumn'), 28 | ], 29 | 'sortDirection' => [ 30 | 'as' => $this->getQueryStringName('sortDirection'), 31 | ], 32 | ]; 33 | } 34 | 35 | public function sort(string $column): void 36 | { 37 | $isEqual = $this->sortColumn === $column; 38 | $isAscending = $this->sortDirection === Direction::Ascending->value; 39 | 40 | if (! $isEqual) { 41 | $this->sortColumn = $column; 42 | $this->sortDirection = Direction::Ascending->value; 43 | } else { 44 | if ($isAscending) { 45 | $this->sortDirection = Direction::Descending->value; 46 | } else { 47 | $this->sortColumn = ''; 48 | $this->sortDirection = ''; 49 | } 50 | } 51 | 52 | $this->updateSession(); 53 | } 54 | 55 | /** @param Builder $builder */ 56 | protected function applySorting(Builder $builder): static 57 | { 58 | $hasSorting = ! blank($this->sortColumn) && ! blank($this->sortDirection); 59 | 60 | if ($this->isReordering() || ($this->useReordering && ! $hasSorting)) { 61 | $builder->orderBy($builder->qualifyColumn($this->reorderingColumn)); 62 | 63 | return $this; 64 | } 65 | 66 | if (! $hasSorting) { 67 | $builder->orderBy($builder->qualifyColumn($this->model()->getKeyName())); 68 | 69 | return $this; 70 | } 71 | 72 | $direction = Direction::tryFrom($this->sortDirection); 73 | 74 | /** @var ?BaseColumn $column */ 75 | $column = $this->resolveColumns()->first(function (BaseColumn $column): bool { 76 | return $column->isSortable() && $this->sortColumn === $column->code(); 77 | }); 78 | 79 | if ($column !== null && $direction !== null) { 80 | $column->applySorting($builder, $direction); 81 | } 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Concerns/Makeable.php: -------------------------------------------------------------------------------- 1 | column = $column; 39 | $this->code = $code ?? Str::of($column)->replace('.', '_')->toString(); 40 | } else { 41 | $this->column = null; 42 | $this->code = $code ?? md5($label); 43 | $this->filterUsing = $column; 44 | $this->computed = true; 45 | } 46 | } 47 | 48 | public function label(): string 49 | { 50 | return $this->label; 51 | } 52 | 53 | public function column(): ?string 54 | { 55 | return $this->column; 56 | } 57 | 58 | public function code(): string 59 | { 60 | return $this->code; 61 | } 62 | 63 | public function render(): mixed 64 | { 65 | return view($this->view, [ 66 | 'filter' => $this, 67 | ]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Filters/BooleanFilter.php: -------------------------------------------------------------------------------- 1 | filterUsing = $filterUsing; 18 | 19 | return $this; 20 | } 21 | 22 | public function filterUsingCallback(): ?Closure 23 | { 24 | return $this->filterUsing; 25 | } 26 | 27 | /** @param Builder $builder */ 28 | public function filter(Builder $builder, mixed $value): void 29 | { 30 | $builder->when(! blank($value), function (Builder $builder) use ($value): void { 31 | $this->qualifyQuery($builder, function (Builder $builder, string $column) use ($value): void { 32 | if (is_array($value)) { 33 | $builder->whereIn($column, $value); 34 | } else { 35 | $builder->where($column, '=', $value); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | /** @param Builder $builder */ 42 | public function applyFilter(Builder $builder, mixed $value): void 43 | { 44 | if ($this->filterUsing !== null) { 45 | call_user_func($this->filterUsing, $builder, $value); 46 | 47 | return; 48 | } 49 | 50 | if (! $this->isComputed()) { 51 | $this->filter($builder, $value); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Filters/DateFilter.php: -------------------------------------------------------------------------------- 1 | $value */ 16 | $from = $value['from'] ?? null; 17 | $to = $value['to'] ?? null; 18 | 19 | $builder->when($from, function (Builder $builder, ?string $from): void { 20 | $this->qualifyQuery($builder, function (Builder $builder, string $column) use ($from): void { 21 | $builder->whereDate($column, '>=', $from); 22 | }); 23 | }); 24 | 25 | $builder->when($to, function (Builder $builder, ?string $to): void { 26 | $this->qualifyQuery($builder, function (Builder $builder, string $column) use ($to): void { 27 | $builder->whereDate($column, '<=', $to); 28 | }); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Filters/SelectFilter.php: -------------------------------------------------------------------------------- 1 | '$refresh', 62 | ]; 63 | 64 | protected function link(Model $model): ?string 65 | { 66 | return null; 67 | } 68 | 69 | protected function model(): Model 70 | { 71 | return app($this->model); 72 | } 73 | 74 | /** @return Builder */ 75 | protected function query(): Builder 76 | { 77 | return $this->model()->query(); 78 | } 79 | 80 | /** @return Builder */ 81 | protected function appliedQuery(): Builder 82 | { 83 | $query = $this->query(); 84 | 85 | $this 86 | ->applySelect($query) 87 | ->applySoftDeletes($query) 88 | ->applyRelations($query) 89 | ->applyGlobalSearch($query) 90 | ->applyColumnSearch($query) 91 | ->applyFilters($query) 92 | ->applySorting($query); 93 | 94 | return $query; 95 | } 96 | 97 | /** @return LengthAwarePaginator */ 98 | protected function paginate(): LengthAwarePaginator 99 | { 100 | if ($this->deferLoading && ! $this->initialized) { 101 | return new ConcreteLengthAwarePaginator([], 0, $this->perPage()); 102 | } 103 | 104 | return $this->appliedQuery()->paginate($this->perPage()); 105 | } 106 | 107 | public function render(): mixed 108 | { 109 | return view($this->view, [ 110 | 'paginator' => $this->paginate(), 111 | 'table' => [ 112 | 'columns' => $this->resolveColumns(), 113 | 'filters' => $this->resolveFilters(), 114 | 'actions' => $this->resolveActions(), 115 | ], 116 | 'perPageOptions' => $this->perPageOptions(), 117 | 'pollingOptions' => $this->pollingOptions(), 118 | ]); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/../resources/views', 'livewire-table'); 14 | 15 | $this->publishes([ 16 | __DIR__.'/../resources/views' => resource_path('views/vendor/livewire-table'), 17 | ], 'views'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Support/Column.php: -------------------------------------------------------------------------------- 1 | column); 24 | } 25 | 26 | public function name(): string 27 | { 28 | return $this->column()->explode('.')->last() ?? ''; 29 | } 30 | 31 | public function hasRelation(): bool 32 | { 33 | return $this->column()->explode('.')->count() > 1; 34 | } 35 | 36 | public function relation(string $glue = '.'): string 37 | { 38 | return $this->column()->explode('.')->slice(0, -1)->implode($glue); 39 | } 40 | 41 | public function alias(): string 42 | { 43 | return $this->relation('_'); 44 | } 45 | 46 | /** @param Builder $builder */ 47 | public function qualify(Builder $builder): string 48 | { 49 | $name = $this->name(); 50 | $alias = $this->alias(); 51 | 52 | return strlen($alias) > 0 53 | ? $alias.'.'.$name 54 | : $builder->qualifyColumn($name); 55 | } 56 | 57 | /** @return array */ 58 | public function segments(): array 59 | { 60 | /** @var array $segments */ 61 | $segments = $this->column()->explode('.')->toArray(); 62 | 63 | return $segments; 64 | } 65 | } 66 | --------------------------------------------------------------------------------