diff options
| author | Sam Light <samlight1994@gmail.com> | 2026-07-27 22:28:18 +0100 |
|---|---|---|
| committer | Sam Light <samlight1994@gmail.com> | 2026-07-27 22:28:18 +0100 |
| commit | 1dc1088933c906d4676056d53395d34ce0a68d01 (patch) | |
| tree | b3c39b1e58305e2b0985678b720991883372667f | |
| parent | 827a53363e7a03329d6e5972d9d9be99d17c889e (diff) | |
| -rw-r--r-- | resources/views/table.blade.php | 4 | ||||
| -rw-r--r-- | src/Columns/Column.php | 6 | ||||
| -rw-r--r-- | src/Support/SortDirections.php | 44 | ||||
| -rw-r--r-- | src/TableComponent.php | 32 | ||||
| -rw-r--r-- | tests/Feature/TableTest.php | 88 | ||||
| -rw-r--r-- | tests/Unit/SortDirectionsTest.php | 30 | ||||
| -rw-r--r-- | workbench/app/Livewire/ProductsTable.php | 10 |
7 files changed, 198 insertions, 16 deletions
diff --git a/resources/views/table.blade.php b/resources/views/table.blade.php index d4290a9..65687a1 100644 --- a/resources/views/table.blade.php +++ b/resources/views/table.blade.php @@ -22,8 +22,8 @@ <div @class([ 'ordered' => $column->isSortable(), - 'ordered-asc' => $column->name === $order && $orderDirection === 'asc', - 'ordered-desc' => $column->name === $order && $orderDirection === 'desc', + 'ordered-asc' => $column->name === $order && $sortDirection === \SortDirection::Ascending, + 'ordered-desc' => $column->name === $order && $sortDirection === \SortDirection::Descending, ]) @if($column->isSortable()) wire:click="orderBy('{{ $column->name }}')" diff --git a/src/Columns/Column.php b/src/Columns/Column.php index ede94f7..1943a42 100644 --- a/src/Columns/Column.php +++ b/src/Columns/Column.php @@ -31,6 +31,9 @@ class Column */ private ?Closure $slotFn = null; + /** + * @var ?Closure(Builder<Model>, SortDirection): void + */ private ?Closure $sortFn = null; /** @@ -92,9 +95,8 @@ class Column * @template TModel of Model * * @param Builder<TModel> $query - * @param SortDirection|'asc'|'desc'|null $dir */ - public function applySort(Builder $query, SortDirection|string|null $dir): void + public function applySort(Builder $query, SortDirection $dir): void { if ($this->sortFn !== null) { ($this->sortFn)($query, $dir); diff --git a/src/Support/SortDirections.php b/src/Support/SortDirections.php new file mode 100644 index 0000000..def0f53 --- /dev/null +++ b/src/Support/SortDirections.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +namespace Lightscale\LaralightTables\Support; + +use SortDirection; + +/** + * Converts between SortDirection and the 'asc'/'desc' strings used by the + * query builder and the Livewire wire/URL boundary. + * + * SortDirection is a pure enum, so it carries no value of its own. + */ +final class SortDirections +{ + public const ASC = 'asc'; + + public const DESC = 'desc'; + + /** + * @return 'asc'|'desc' + */ + public static function value(SortDirection $dir): string + { + return match ($dir) { + SortDirection::Ascending => self::ASC, + SortDirection::Descending => self::DESC, + }; + } + + public static function tryFrom(SortDirection|string|null $value): ?SortDirection + { + if ($value instanceof SortDirection) { + return $value; + } + + return match ($value === null ? null : strtolower($value)) { + self::ASC => SortDirection::Ascending, + self::DESC => SortDirection::Descending, + default => null, + }; + } +} diff --git a/src/TableComponent.php b/src/TableComponent.php index e36029e..196d5f8 100644 --- a/src/TableComponent.php +++ b/src/TableComponent.php @@ -13,6 +13,7 @@ use Illuminate\Support\Str; use Illuminate\View\View; use Lightscale\LaralightAssets\Facades\Assets; use Lightscale\LaralightTables\Columns\Column; +use Lightscale\LaralightTables\Support\SortDirections; use Lightscale\LaralightTables\Toolbar\Filter; use Livewire\Attributes\Url; use Livewire\Component; @@ -66,10 +67,12 @@ abstract class TableComponent extends Component public ?string $order = null; /** - * @var SortDirection|'asc'|'desc'|null + * Wire/URL transport only — read through getOrderDirection(). + * + * @var 'asc'|'desc'|null */ #[Url] - public SortDirection|string|null $orderDirection = null; + public ?string $orderDirection = null; public function mount(): void { @@ -115,18 +118,28 @@ abstract class TableComponent extends Component $this->resetPage(); } + public function getOrderDirection(): ?SortDirection + { + return SortDirections::tryFrom($this->orderDirection); + } + + protected function setOrderDirection(?SortDirection $dir): void + { + $this->orderDirection = $dir === null ? null : SortDirections::value($dir); + } + public function orderBy(string $column): void { if ($column === $this->order) { - if ($this->orderDirection === 'desc') { + if ($this->getOrderDirection() === SortDirection::Descending) { $this->order = null; - $this->orderDirection = null; + $this->setOrderDirection(null); } else { - $this->orderDirection = 'desc'; + $this->setOrderDirection(SortDirection::Descending); } } else { $this->order = $column; - $this->orderDirection = 'asc'; + $this->setOrderDirection(SortDirection::Ascending); } } @@ -203,8 +216,10 @@ abstract class TableComponent extends Component protected function applyOrder(Builder $query): void { $column = $this->getOrderColumn(); - if ($column) { - $column->applySort($query, $this->orderDirection); + $dir = $this->getOrderDirection(); + + if ($column !== null && $dir !== null) { + $column->applySort($query, $dir); } } @@ -267,6 +282,7 @@ abstract class TableComponent extends Component return view('laralight-tables::table', [ 'rootClass' => $this->rootClass, 'tableClass' => $this->tableClass, + 'sortDirection' => $this->getOrderDirection(), ] + compact( 'data', 'allColumns', 'columns', 'toolbars', )); diff --git a/tests/Feature/TableTest.php b/tests/Feature/TableTest.php index 8dc327c..8c34095 100644 --- a/tests/Feature/TableTest.php +++ b/tests/Feature/TableTest.php @@ -2,6 +2,7 @@ use Livewire\Livewire; use Workbench\App\Livewire\ProductsTable; +use Workbench\App\Models\Product; describe('products table', function () { it('does render', function () { @@ -13,3 +14,90 @@ describe('products table', function () { ->assertSeeHtml('<td'); }); }); + +describe('products table ordering', function () { + it('cycles a column through ascending, descending and unordered', function () { + Livewire::test(ProductsTable::class) + ->call('orderBy', 'name') + ->assertSet('order', 'name') + ->assertSet('orderDirection', 'asc') + ->call('orderBy', 'name') + ->assertSet('order', 'name') + ->assertSet('orderDirection', 'desc') + ->call('orderBy', 'name') + ->assertSet('order', null) + ->assertSet('orderDirection', null); + }); + + it('resets to ascending when a different column is clicked', function () { + Livewire::test(ProductsTable::class) + ->call('orderBy', 'name') + ->call('orderBy', 'name') + ->assertSet('orderDirection', 'desc') + ->call('orderBy', 'price') + ->assertSet('order', 'price') + ->assertSet('orderDirection', 'asc'); + }); + + it('exposes the direction as a SortDirection', function () { + $component = Livewire::test(ProductsTable::class); + + expect($component->instance()->getOrderDirection())->toBeNull(); + + $component->call('orderBy', 'name'); + expect($component->instance()->getOrderDirection())->toBe(SortDirection::Ascending); + + $component->call('orderBy', 'name'); + expect($component->instance()->getOrderDirection())->toBe(SortDirection::Descending); + }); + + it('marks the ordered column in the header', function () { + Livewire::test(ProductsTable::class) + ->assertDontSeeHtml('ordered-asc') + ->assertDontSeeHtml('ordered-desc') + ->call('orderBy', 'name') + ->assertSeeHtml('ordered-asc') + ->assertDontSeeHtml('ordered-desc') + ->call('orderBy', 'name') + ->assertSeeHtml('ordered-desc') + ->assertDontSeeHtml('ordered-asc'); + }); + + it('applies the direction to the query', function () { + $lowest = Product::orderBy('price')->orderBy('id')->value('id'); + $highest = Product::orderByDesc('price')->orderByDesc('id')->value('id'); + + expect($lowest)->not->toBe($highest); + + Livewire::test(ProductsTable::class) + // A single row of a single column, so the only <td> is the ordered id. + ->set('activeColumns', ['id']) + ->set('pageSize', 1) + ->call('orderBy', 'price') + ->assertSeeHtml(">{$lowest}</td>") + ->assertDontSeeHtml(">{$highest}</td>") + ->call('orderBy', 'price') + ->assertSeeHtml(">{$highest}</td>") + ->assertDontSeeHtml(">{$lowest}</td>"); + }); + + it('restores the order from the query string', function () { + $highest = Product::orderByDesc('price')->orderByDesc('id')->value('id'); + + Livewire::withQueryParams(['order' => 'price', 'orderDirection' => 'desc']) + ->test(ProductsTable::class) + ->assertSet('orderDirection', 'desc') + ->assertSeeHtml('ordered-desc') + ->set('activeColumns', ['id']) + ->set('pageSize', 1) + ->assertSeeHtml(">{$highest}</td>"); + }); + + it('ignores an unrecognised direction in the query string', function () { + Livewire::withQueryParams(['order' => 'price', 'orderDirection' => 'nonsense']) + ->test(ProductsTable::class) + ->assertOk() + ->assertDontSeeHtml('ordered-asc') + ->assertDontSeeHtml('ordered-desc'); + }); +}); diff --git a/tests/Unit/SortDirectionsTest.php b/tests/Unit/SortDirectionsTest.php new file mode 100644 index 0000000..c63d5bb --- /dev/null +++ b/tests/Unit/SortDirectionsTest.php @@ -0,0 +1,30 @@ +<?php + +use Lightscale\LaralightTables\Support\SortDirections; + +describe('SortDirections', function () { + it('converts the enum to a query builder direction', function () { + expect(SortDirections::value(SortDirection::Ascending))->toBe('asc'); + expect(SortDirections::value(SortDirection::Descending))->toBe('desc'); + }); + + it('resolves strings to the enum', function () { + expect(SortDirections::tryFrom('asc'))->toBe(SortDirection::Ascending); + expect(SortDirections::tryFrom('desc'))->toBe(SortDirection::Descending); + }); + + it('resolves strings case insensitively', function () { + expect(SortDirections::tryFrom('ASC'))->toBe(SortDirection::Ascending); + expect(SortDirections::tryFrom('Desc'))->toBe(SortDirection::Descending); + }); + + it('passes an enum straight through', function () { + expect(SortDirections::tryFrom(SortDirection::Descending))->toBe(SortDirection::Descending); + }); + + it('resolves null and unrecognised values to null', function () { + expect(SortDirections::tryFrom(null))->toBeNull(); + expect(SortDirections::tryFrom(''))->toBeNull(); + expect(SortDirections::tryFrom('nonsense'))->toBeNull(); + }); +}); diff --git a/workbench/app/Livewire/ProductsTable.php b/workbench/app/Livewire/ProductsTable.php index f1afce7..92a8cee 100644 --- a/workbench/app/Livewire/ProductsTable.php +++ b/workbench/app/Livewire/ProductsTable.php @@ -4,12 +4,14 @@ namespace Workbench\App\Livewire; use Illuminate\Database\Eloquent\Builder; use Lightscale\LaralightTables\Columns\Column; +use Lightscale\LaralightTables\Support\SortDirections; use Lightscale\LaralightTables\Toolbar; use Lightscale\LaralightTables\Toolbar\Button; use Lightscale\LaralightTables\Toolbar\ColumnSelect; use Lightscale\LaralightTables\Toolbar\PageSize; use Lightscale\LaralightTables\Toolbar\Search; use Lightscale\LaralightTables\Toolbar\SelectFilter; +use SortDirection; use Workbench\App\Models\Category; use Workbench\App\Models\Product; @@ -70,16 +72,16 @@ class ProductsTable extends Table { return [ Column::make('id', 'ID') - ->sortable(fn (Builder $q, string $dir) => $q->orderBy('id', $dir)), + ->sortable(fn (Builder $q, SortDirection $dir) => $q->orderBy('id', SortDirections::value($dir))), Column::make('name', 'Name') - ->sortable(fn (Builder $q, string $dir) => $q->orderBy('name', $dir)), + ->sortable(fn (Builder $q, SortDirection $dir) => $q->orderBy('name', SortDirections::value($dir))), Column::make('category_name', 'Category') ->slot(fn ($r) => $r->category->name), Column::make('price', 'Price') - ->sortable(fn (Builder $q, string $dir) => $q->orderBy('price', $dir)) + ->sortable(fn (Builder $q, SortDirection $dir) => $q->orderBy('price', SortDirections::value($dir))) ->slot(fn ($r, $c) => "£{$r->{$c->name}}"), Column::make('stock', 'Stock') - ->sortable(fn (Builder $q, string $dir) => $q->orderBy('stock', $dir)), + ->sortable(fn (Builder $q, SortDirection $dir) => $q->orderBy('stock', SortDirections::value($dir))), ]; } |
