diff options
author | Sam Light <samlight1994@gmail.com> | 2025-03-30 14:20:57 +0100 |
---|---|---|
committer | Sam Light <samlight1994@gmail.com> | 2025-03-30 14:20:57 +0100 |
commit | e744b8d67ef1d18050158dd523ba7d804c1c8528 (patch) | |
tree | 56c90a83912bd63bdd507e76e5ef00c82a904b50 | |
parent | a883170dd1c723bcd02916f9bc8a96ed85a61761 (diff) |
Filters, sorting, escaping and more....
-rw-r--r-- | resources/views/table.blade.php | 10 | ||||
-rw-r--r-- | resources/views/toolbar/select-filter.blade.php | 23 | ||||
-rw-r--r-- | src/Columns/Column.php | 34 | ||||
-rw-r--r-- | src/Concerns/Makable.php | 11 | ||||
-rw-r--r-- | src/TableComponent.php | 65 | ||||
-rw-r--r-- | src/Toolbar.php | 9 | ||||
-rw-r--r-- | src/Toolbar/ColumnSelect.php | 4 | ||||
-rw-r--r-- | src/Toolbar/Filter.php | 26 | ||||
-rw-r--r-- | src/Toolbar/Item.php | 3 | ||||
-rw-r--r-- | src/Toolbar/SelectFilter.php | 35 | ||||
-rw-r--r-- | workbench/app/Livewire/ProductsTable.php | 36 |
11 files changed, 236 insertions, 20 deletions
diff --git a/resources/views/table.blade.php b/resources/views/table.blade.php index bcefe57..448a290 100644 --- a/resources/views/table.blade.php +++ b/resources/views/table.blade.php @@ -9,7 +9,15 @@ <thead> <tr> @foreach($columns as $column) - <th scope="col">{{ $column->getTitle() }}</th> + <th + scope="col" + @class(['sortable' => $column->isSortable()]) + @if($column->isSortable()) + wire:click="orderBy('{{ $column->name }}')" + @endif + > + {{ $column->getTitle() }} + </th> @endforeach </tr> </thead> diff --git a/resources/views/toolbar/select-filter.blade.php b/resources/views/toolbar/select-filter.blade.php new file mode 100644 index 0000000..13cdbc1 --- /dev/null +++ b/resources/views/toolbar/select-filter.blade.php @@ -0,0 +1,23 @@ +<div class="filter d-flex align-items-center"> + @if ($label) + <label for="{{ $id }}" class="mx-3"> + {{ $label }} + </label> + @endif + <select + id="{{ $id }}" + class="form-control" + wire:model.live="filters.{{ $key }}" + > + @if (!empty($placeholder)) + <option value=""> + {{ $placeholder }} + </option> + @endif + @foreach ($options as $value => $option) + <option value="{{ $value }}"> + {{ $option }} + </option> + @endforeach + </select> +</div> diff --git a/src/Columns/Column.php b/src/Columns/Column.php index 895708f..a8dc2e6 100644 --- a/src/Columns/Column.php +++ b/src/Columns/Column.php @@ -5,6 +5,7 @@ namespace Lightscale\LaralightTables\Columns; use Lightscale\LaralightTables\TableComponent; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; use Illuminate\View\ComponentAttributeBag; use Illuminate\Support\HtmlString; @@ -14,6 +15,7 @@ class Column { private TableComponent $table; private bool $showInSelect; + private bool $shouldEscape = true; private ?Closure $slotFn = null; private ?Closure $sortFn = null; @@ -54,12 +56,24 @@ class Column { return $this; } - public function sortable(callable $fn) : static + public function sortable(?callable $fn): static { - $this->sortFn = Closure::fromCallable($fn); + $this->sortFn = $fn; return $this; } + public function isSortable(): bool + { + return $this->sortFn !== null; + } + + public function applySort(Builder $query, string $dir): void + { + if ($this->sortFn !== null) { + ($this->sortFn)($query, $dir); + } + } + public function colClass(string $v) : static { $this->colClass = $v; @@ -88,9 +102,23 @@ class Column { return $this->showInSelect; } + public function shouldEscape(bool $v): static + { + $this->shouldEscape = $v; + return $this; + } + + public function escape(string $content): string + { + return $this->shouldEscape ? e($content) : $content; + } + protected function getContent(Model $row): string { - return $this->slotFn?->call($this, $row, $this) ?? $this->defaultSlot($row); + return $this->escape( + $this->slotFn?->call($this, $row, $this) ?? + $this->defaultSlot($row) + ); } public function view(Model $row): HtmlString diff --git a/src/Concerns/Makable.php b/src/Concerns/Makable.php new file mode 100644 index 0000000..7562970 --- /dev/null +++ b/src/Concerns/Makable.php @@ -0,0 +1,11 @@ +<?php + +namespace Lightscale\LaralightTables\Concerns; + +trait Makable +{ + public static function make(mixed ...$args): static + { + return new static(...$args); + } +} diff --git a/src/TableComponent.php b/src/TableComponent.php index 0a55d5e..266cdc3 100644 --- a/src/TableComponent.php +++ b/src/TableComponent.php @@ -2,6 +2,8 @@ namespace Lightscale\LaralightTables; +use Lightscale\LaralightTables\Columns\Column; + use Livewire\Component; use Livewire\WithPagination; use Livewire\Attributes\Url; @@ -28,9 +30,19 @@ abstract class TableComponent extends Component #[Url] public int $pageSize; + public array $activeColumns = []; - public function mount() + #[Url] + public array $filters = []; + + #[Url] + public ?string $order = null; + + #[Url] + public ?string $orderDirection = null; + + public function mount(): void { $this->setDefaultActiveColumns(); $this->setDefaultPageSize(); @@ -63,6 +75,28 @@ abstract class TableComponent extends Component } } + public function updatedFilters(): void + { + $this->resetPage(); + } + + public function orderBy(string $column): void + { + if ($column === $this->order) { + if ($this->orderDirection === 'desc') { + $this->order = null; + $this->orderDirection = null; + } + else { + $this->orderDirection = 'desc'; + } + } + else { + $this->order = $column; + $this->orderDirection = 'asc'; + } + } + protected function toolbar(): ?Toolbar { return null; @@ -91,6 +125,25 @@ abstract class TableComponent extends Component protected function search(Builder $builder, string $search) : void {} + protected function getFilters(): Collection + { + return $this->getToolbar()?->getFilters() ?? collect(); + } + + + protected function getOrderColumn(): ?Column + { + return ($name = $this->order) === null ? null : $this->getColumns()[$name] ?? null; + } + + protected function applyOrder(Builder $query): void + { + $column = $this->getOrderColumn(); + if ($column) { + $column->applySort($query, $this->orderDirection); + } + } + protected function buildQuery() : Builder { $query = $this->query(); @@ -99,16 +152,22 @@ abstract class TableComponent extends Component $this->search($query, $this->search); } + foreach ($this->getFilters() as $filter) { + $filter->applyFilter($query); + } + + $this->applyOrder($query); + return $query; } private ?Collection $columnsCache = null; - public function getColumns() + public function getColumns(): Collection { if($this->columnsCache === null) { $this->columnsCache = collect($this->columns())->each( fn($c) => $c->setTable($this) - ); + )->keyBy('name'); } return $this->columnsCache; diff --git a/src/Toolbar.php b/src/Toolbar.php index 2db39f5..8539e0e 100644 --- a/src/Toolbar.php +++ b/src/Toolbar.php @@ -5,7 +5,7 @@ namespace Lightscale\LaralightTables; use Lightscale\LaralightTables\Toolbar\Item as ToolbarItem; use Lightscale\LaralightTables\Toolbar\Search as SearchItem; use Lightscale\LaralightTables\Toolbar\PageSize as PageSizeItem; -use Lightscale\LaralightTables\Toolbar\Filter as FitlerItem; +use Lightscale\LaralightTables\Toolbar\Filter as FilterItem; use Illuminate\View\View; use Illuminate\Support\Collection; @@ -18,6 +18,7 @@ class Toolbar protected Collection $startItems; protected Collection $midItems; protected Collection $endItems; + protected Collection $filterItems; public function __construct( private TableComponent $table @@ -26,6 +27,7 @@ class Toolbar $this->startItems = collect(); $this->midItems = collect(); $this->endItems = collect(); + $this->filterItems = collect(); } private function addItem(Collection $list, ToolbarItem $item): static @@ -76,6 +78,11 @@ class Toolbar return $this->pageSizeItem; } + public function getFilters(): Collection + { + return $this->filterItems; + } + public function render(): View { return view('laralight-tables::toolbar', [ diff --git a/src/Toolbar/ColumnSelect.php b/src/Toolbar/ColumnSelect.php index a70c49a..9380650 100644 --- a/src/Toolbar/ColumnSelect.php +++ b/src/Toolbar/ColumnSelect.php @@ -11,7 +11,9 @@ class ColumnSelect extends Item public function render(): View { return view('laralight-tables::toolbar.column-select', [ - 'allColumns' => $this->getTable()->getColumns() + 'allColumns' => $this->getTable() + ->getColumns() + ->filter(fn($c) => $c->getShowInSelect()) ]); } } diff --git a/src/Toolbar/Filter.php b/src/Toolbar/Filter.php index 19f29cc..51e0d9a 100644 --- a/src/Toolbar/Filter.php +++ b/src/Toolbar/Filter.php @@ -2,12 +2,30 @@ namespace Lightscale\LaralightTables\Toolbar; +use Illuminate\Database\Eloquent\Builder; + use Closure; abstract class Filter extends Item { protected ?Closure $filterCallback = null; + public function __construct( + protected string $key, + protected ?string $label = null, + ) {} + + public function label(string $v): static + { + $this->label = $v; + return $this; + } + + public function makeId(): string + { + return "filter_{$this->key}"; + } + public function filter(callable $filterCB): static { $this->filterCallback = $filterCB; @@ -16,7 +34,13 @@ abstract class Filter extends Item public function applyFilter(Builder $query): void { - ($this->filterCallback)($query, $value); + if ($this->filterCallback !== null) { + $value = $this->getTable()->filters[$this->key] ?? null; + + if (!empty($value)) { + ($this->filterCallback)($query, $value); + } + } } } diff --git a/src/Toolbar/Item.php b/src/Toolbar/Item.php index 168f877..e91698f 100644 --- a/src/Toolbar/Item.php +++ b/src/Toolbar/Item.php @@ -4,12 +4,15 @@ namespace Lightscale\LaralightTables\Toolbar; use Lightscale\LaralightTables\TableComponent; use Lightscale\LaralightTables\Toolbar; +use Lightscale\LaralightTables\Concerns\Makable; use Illuminate\View\View; use Illuminate\Support\HtmlString; abstract class Item { + use Makable; + private Toolbar $toolbar; public function setToolbar(Toolbar $toolbar): void diff --git a/src/Toolbar/SelectFilter.php b/src/Toolbar/SelectFilter.php new file mode 100644 index 0000000..0549885 --- /dev/null +++ b/src/Toolbar/SelectFilter.php @@ -0,0 +1,35 @@ +<?php + +namespace Lightscale\LaralightTables\Toolbar; + +use Illuminate\View\View; + +class SelectFilter extends Filter +{ + protected iterable $options; + protected ?string $placeholder = null; + + public function placeholder(?string $v): static + { + $this->placeholder = $v; + return $this; + } + + public function options(iterable $options): static + { + $this->options = $options; + return $this; + } + + public function render(): View + { + return view('laralight-tables::toolbar.select-filter', [ + 'id' => $this->makeId(), + 'key' => $this->key, + 'label' => $this->label, + 'options' => $this->options, + 'placeholder' => $this->placeholder, + ]); + } + +} diff --git a/workbench/app/Livewire/ProductsTable.php b/workbench/app/Livewire/ProductsTable.php index c5a0f61..9717b5d 100644 --- a/workbench/app/Livewire/ProductsTable.php +++ b/workbench/app/Livewire/ProductsTable.php @@ -3,12 +3,14 @@ namespace Workbench\App\Livewire; use Workbench\App\Models\Product; +use Workbench\App\Models\Category; use Lightscale\LaralightTables\Columns\Column; use Lightscale\LaralightTables\Toolbar; use Lightscale\LaralightTables\Toolbar\Search; use Lightscale\LaralightTables\Toolbar\PageSize; use Lightscale\LaralightTables\Toolbar\ColumnSelect; +use Lightscale\LaralightTables\Toolbar\SelectFilter; use Illuminate\Database\Eloquent\Builder; @@ -18,12 +20,21 @@ class ProductsTable extends Table public function toolbar(): Toolbar { - return parent::toolbar() - ->addStartItem(new Search()) - ->addEndItem(new PageSize()) - ->addEndItem(new ColumnSelect()); + $categoryFilter = SelectFilter::make('category') + ->placeholder(__('Filter category')) + ->options(Category::pluck('name', 'id')) + ->filter( + fn(Builder $q, string $value) => $q->whereHas( + 'category', + fn(Builder $q) => $q->where('id', $value) + ) + ); - return $toolbar; + return parent::toolbar() + ->addStartItem(Search::make()) + ->addStartItem($categoryFilter) + ->addEndItem(PageSize::make()) + ->addEndItem(ColumnSelect::make()); } public function query(): Builder @@ -40,12 +51,17 @@ class ProductsTable extends Table public function columns(): array { return [ - Column::make('id', 'ID'), - Column::make('name', 'Name'), - Column::make('category.name', 'Category') + Column::make('id', 'ID') + ->sortable(fn(Builder $q, string $dir) => $q->orderBy('id', $dir)), + Column::make('name', 'Name') + ->sortable(fn(Builder $q, string $dir) => $q->orderBy('name', $dir)), + Column::make('category_name', 'Category') ->slot(fn($r) => $r->category->name), - Column::make('price', 'Price'), - Column::make('stock', 'Stock'), + Column::make('price', 'Price') + ->sortable(fn(Builder $q, string $dir) => $q->orderBy('price', $dir)) + ->slot(fn($r, $c) => "£{$r->{$c->name}}"), + Column::make('stock', 'Stock') + ->sortable(fn(Builder $q, string $dir) => $q->orderBy('stock', $dir)), ]; } } |