In this part, we will make categories "reorderable" with drag-drop behavior.
First, we need a new column in the categories
table.
database/migrations/xxxx_create_categories_table.php:
return new class extends Migration{ public function up() { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('slug'); $table->boolean('is_active')->default(true); $table->integer('position'); $table->timestamps(); }); }};
app/Models/Category.php:
class Category extends Model{ use HasFactory; protected $fillable = ['name', 'slug', 'is_active', 'position']; }
To make this work, we will use a package nextapps-be/livewire-sortablejs. In the resources/views/layouts/app.blade.php
add the CDN link.
@livewireScripts <script src="https://unpkg.com/@nextapps-be/livewire-sortablejs@0.4.1/dist/livewire-sortable.js"></script> </body></html>
Next, according to the package documentation, we need to add the Livewire action wire:sortable
which will call the updateOrder
method in the Livewire component. And, because we will drag the table row, we need to add wire:sortable.item
and wire:key
to the <tr>
tag.
Also, it will be possible to drag-drop only from the arrow button. For this, we need to add wire:sortable.handle
to a button.
Here's the Blade code:
resources/livewire/categories-list.blade.php:
<tbody class="bg-white divide-y divide-gray-200 divide-solid"> <tbody wire:sortable="updateOrder" class="bg-white divide-y divide-gray-200 divide-solid"> @foreach($categories as $category) <tr class="bg-white"> <tr class="bg-white" wire:sortable.item="{{ $category->id }}" wire:key="{{ $loop->index }}"> <td class="px-6"> <button> <button wire:sortable.handle class="cursor-move"> <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"> <path fill="none" d="M0 0h256v256H0z" /> <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M156.3 203.7 128 232l-28.3-28.3M128 160v72M99.7 52.3 128 24l28.3 28.3M128 96V24M52.3 156.3 24 128l28.3-28.3M96 128H24M203.7 99.7 232 128l-28.3 28.3M160 128h72" /> </svg> </button> </td>
Now, let's move to the Livewire Component and implement the updateOrder()
method. First, we will need the list of categories. For that, we will add public property $categories
and in render()
will assign all categories to it.
app/Livewire/CategoriesList.php:
use Illuminate\Support\Collection; class CategoriesList extends Component{ use WithPagination; public Category $category; public Collection $categories; public bool $showModal = false; public array $active; // ... public function render(): View { $categories = Category::paginate(10); $this->categories = Category::paginate(10); $this->active = $categories->mapWithKeys( $this->active = $this->categories->mapWithKeys( fn (Category $item) => [$item['id'] => (bool) $item['is_active']] )->toArray(); return view('livewire.categories-list', [ 'categories' => $categories, ]); } //}
But after doing this, the whole categories page will be broken.
Cannot assign Illuminate\Pagination\LengthAwarePaginator to property App\Http\Livewire\CategoriesList::$categories of type Illuminate\Support\Collection
To make this work, we need to make a workaround. Also, we will order by position.
app/Livewire/CategoriesList.php:
class CategoriesList extends Component{ use WithPagination; public Category $category; public Collection $categories; public bool $showModal = false; public array $active; // ... public function render(): View { $this->categories = Category::paginate(10); $cats = Category::orderBy('position')->paginate(10); $links = $cats->links(); $this->categories = collect($cats->items()); $this->active = $this->categories->mapWithKeys( fn (Category $item) => [$item['id'] => (bool) $item['is_active']] )->toArray(); return view('livewire.categories-list', [ 'links' => $links, ]); } //}
resources/livewire/categories-list.blade.php:
{!! $categories->links() !!} {!! $links !!}
Perfect, now we have a working page again and now we can make the updateOrder()
method.
app/Livewire/CategoriesList.php:
class CategoriesList extends Component{ // ... public function updateOrder($list) { foreach ($list as $item) { $cat = $this->categories->firstWhere('id', $item['value']); if ($cat['position'] != $item['order']) { Category::where('id', $item['value'])->update(['position' => $item['order']]); } } } // ...
In this method, we receive an array list with the order and the value.
array:10 [▼ // app/Livewire/CategoriesList.php:54 0 => array:2 [▼ "order" => 1 "value" => "38" ] 1 => array:2 [▼ "order" => 2 "value" => "25" ] 2 => array:2 [▶] 3 => array:2 [▶] 4 => array:2 [▶] 5 => array:2 [▶] 6 => array:2 [▶] 7 => array:2 [▶] 8 => array:2 [▶] 9 => array:2 [▶]]
But we don't need to update every category. That's why we first check if the category position in the DB isn't the same as after reorder, and only then do we do the update.
The last thing, when saving a new category, we need to set its position
to max number + 1.
app/Http/Livewire/CategoriesList.php:
class CategoriesList extends Component{ // ... public function save() { $this->validate(); $position = Category::max('position') + 1; Category::create(array_merge($this->only('name', 'slug'), ['position' => $position])); Category::create($this->only('name', 'slug')); $this->reset('showModal'); } // ...
But now we have a problem when we want to reorder on other pages. When we go to the second page the order again starts from the one. Let's fix that.
app/Livewire/CategoriesList.php:
class CategoriesList extends Component{ use WithPagination; public ?Category $category = null; public string $name = ''; public string $slug = ''; public Collection $categories; public bool $showModal = false; public array $active; public int $editedCategoryId = 0; public int $currentPage = 1; public int $perPage = 10; // ... public function updateOrder($list) { foreach ($list as $item) { $cat = $this->categories->firstWhere('id', $item['value']); $order = $item['order'] + (($this->currentPage - 1) * $this->perPage); if ($cat['position'] != $item['order']) { Category::where('id', $item['value'])->update(['position' => $item['order']]); if ($cat['position'] != $order) { Category::where('id', $item['value'])->update(['position' => $order]); } } } // ... public function render(): View { $cats = Category::orderBy('position')->paginate(10); $cats = Category::orderBy('position')->paginate($this->perPage); $links = $cats->links(); $this->currentPage = $cats->currentPage(); $this->categories = collect($cats->items()); $this->active = $this->categories->mapWithKeys( fn (Category $item) => [$item['id'] => (bool) $item['is_active']] )->toArray(); return view('livewire.categories-list', [ 'links' => $links, ]); } // ...}
Good, now the categories can be reordered with drag-drop!