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 </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!
hi dos the 'position' in categories table migration had to be nullable by default.it is showing error when i migrate:fresh and insert seed data
I think by default its value is 0, but can't guarantee: what is the actual error test and what seed data are you using?
I made the position default to 9999, so new Categories will be at the end. Or if you default to 0 it will be very first category
While you follow along and encountred some errors as I did, I suggest you to make some amendments. To seed position rows. Amend in
CategoryFactory.php
:Import namespace in
CategoriesList.php
For better UX and Web accessibility aspect, change buton cursor class to
cursor-move
incategories-list.blade.php
:I've added the code to Category model. Position has same number as id on seeding or new creation of Category.
Where and how can we dump
$list
array to see what's inside?If we paginate 10 the $categories per page, how can we drag and drop 11 category to the first position?
You can dump where we update the order. So it would be
updateOrder
method. To other page you cannot drag and drop.If an item will be positioned in another page than page 1, order number begins from 1. This causes that there are 2 categories with the same position number. The refactoring in CategoriesList helped me to achieve the problem.
Now how can you move a category that is on the second page to the first page? This seems to not be working.
Good question, I don't think it's possible with drag-drop. I guess it's better to build a separate button like "move one page up" or something.
Again so many issues following alone had to check the comments for fixes, and this comments were posted a month(s) ago not sure why the tutorial text was not updated. Not trying to be mean just giving honest feedback.
CategoryFactory position will increase in order.
Or you can increase the position value this way.
'position' => Category::max('position') + 1
@Emre Dikem
Yep, that's the approach i took too.