Drag And Drop Sorting In Laravel
- Introduction
- Set Up
- Create Livewire Component
- Update the Blade View
- Trouble Shooting
- Additional Resources
Introduction
This guide walks through the process of adding drag-and-drop sorting functionality to a Laravel application using AlpineJS and its sorting plugin. The solution works seamlessly with both Laravel and Livewire components.
Before you begin, make sure you have a Laravel application set up with the Naykel/Gotime starter package and JTB, and that you have a basic understanding of Livewire.
In this tutorial, we will explore how to implement drag-and-drop in Laravel by creating a simple Livewire to-do list component with drag-and-drop functionality.
Set Up
Create Model, Migration, and Factory
First, we need to create a migration, model, and factory for the to_dos
table. To do this, run the
following command in your terminal:
php artisan make:model ToDo -mf
Navigate to the database/migrations
directory and open the migration file. Add the following code
to the up
method in the migration file to add the basic necessary fields.
Schema::create('to_dos', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->integer('position')->nullable();
$table->timestamps();
});
Open the generated factory file and add the following code to define the factory:
public function definition(): array
{
return [
'name' => $this->faker->sentence,
// this needs to be updated to be more dynamic and work with existing records
'position' => $this->faker->numberBetween(1, 10),
];
}
Seed and Migrate the Database
The id
field is being set to prevent the first record from being created with an id
of 1
. This
is to make it easier to visualise the sorting functionality and the values being updated.
Intentionally set the position
out of order to demonstrate the sorting functionality.
ToDo::create(['id' => 487, 'name' => 'First todo...', 'position' => 0]);
ToDo::create(['name' => 'Second todo...', 'position' => 1]);
ToDo::create(['name' => 'Third todo...', 'position' => 4]);
ToDo::create(['name' => 'Fourth todo...', 'position' => 3]);
ToDo::create(['name' => 'Fifth todo...', 'position' => 2]);
Run the migration to create the to_dos
table:
php artisan migrate --seed
Add AlpineJS and the Sort Plugin to Your Project
// Alpine Plugins
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.x.x/dist/cdn.min.js"></script>
// Alpine Core
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
Create Livewire Component
php artisan make:livewire SortableToDoList
The command will generate two files app/Livewire/SortableToDoList
and
resources/views/livewire/sortable-to-do-list.blade.php
.
Basic Component
Now, modify the SortableToDoList
component to fetch the to-dos from the database and display them
in the view.
<?php
namespace App\Http\Livewire;
use App\Models\ToDo;
use Livewire\Component;
use Illuminate\Support\Facades\DB;
class SortableToDoList extends Component
{
public string $view = 'livewire.sortable-to-do-list';
protected function query()
{
return ToDo::query();
}
public function render()
{
$query = ToDo::all()->sortBy('position');
return view($this->view, [
'items' => $query
]);
}
}
view
property is used to prevent an error when using the sort
method.
For reasons which are not clear, you get an error when the sort
method fires unless
there is at least one public property defined in the component.sort
Method
Add the Add the sort
method to the SortableToDoList
component to handle the drag-and-drop sorting
functionality.
public function sort($id, $position): void
{
$model = $this->query()->findOrFail($id);
DB::transaction(function () use ($model, $position) {
$current = $model->position;
// No position change, do nothing and exit early...
if ($current === $position) return;
// Temporarily move the item out of the position stack...
$model->update(['position' => -1]);
// Grab the shifted block and shift it up or down...
$block = $this->query()->whereBetween('position', [
min($current, $position),
max($current, $position),
]);
// Determine the direction of the shift...
$isDraggingDownwards = $current < $position;
// Adjust the positions: decrement for moving down, increment for moving up
$isDraggingDownwards
? $block->decrement('position')
: $block->increment('position');
// Reinsert the item back at its new position
$model->update(['position' => $position]);
});
// Re-arrange the list in case there are any gaps...
$this->arrange();
}
Here's a breakdown of how it works:
- Find the Target Model
The sort method starts by locating the ToDo
model with the given $id
passed to the method. This
is done using the query()
method, which retrieves the ToDo
model query builder. It ensures the
model exists with findOrFail($id)
. If the item with the provided ID is not found, an exception
will be thrown.
- Transaction Block
The sorting logic is wrapped inside a DB::transaction()
to ensure that all changes are made
atomically. If any part of the process fails, all changes are rolled back, maintaining data
integrity.
- Position Check
Inside the transaction, it checks if the item's current database position ($current
) is the same
as the desired new $position
. If there is no change in position, the method returns early without
doing anything.
- Move Item Temporarily Out of Stack
The target ToDo
item is temporarily moved out of the position stack by setting its position
to
-1
. This is a placeholder value that ensures no conflicts while adjusting the positions of other
items in the list.
- Shift the Block of Affected Items
It identifies a range of items whose position values need to be shifted.
This is done using whereBetween('position', [min($current, $position), max($current, $position)])
to
get the range of items that will be affected by the reordering.
- If the item is being dragged down ($current < $position), the positions of the affected items are decremented.
- If the item is being dragged up ($current > $position), the positions of the affected items are incremented.
- Reinsert the Item
After the block of affected items is shifted, the target ToDo
item is placed back into the correct
$position using update(['position' => $position]).
arrange
Method
Add the The arrange
method is used to reassign the positions of all items in the list after a drag-and-drop
operation. This ensures that the positions are sequential and there are no gaps between the items.
public function arrange(): void
{
DB::transaction(function () {
$position = 0;
foreach ($this->query()->get() as $model) {
$model->update(['position' => $position++]);
}
});
}
Update the Blade View
Open resources/views/livewire/sortable-to-do-list.blade.php
and add the following to build out our
front end and sortable list.
<div x-sort="$wire.sort($item, $position)" class="space-y-05">
@foreach ($items as $todo)
<div wire:key="{{ $todo->id }}" x-sort:item="{{ $todo->id }}" class="bx flex va-c hover:bg-gray-100 cursor-move">
<div class="flex space-between w-full ">
<div>{{ $todo->name }}</div>
<div>Position: {{ $todo->position }}</div>
</div>
</div>
@endforeach
</div>
Add Sort Handle
By default, when using the AlpineJS sort plugin, the entire element is draggable. To make the dragging more user-friendly, you can add a handle to the element that users can click and drag to reorder the list.
<div x-sort="$wire.sort($item, $position)" class="space-y-05">
@foreach ($items as $todo)
<!-- Drag and drop item -->
<div wire:key="{{ $todo->id }}" x-sort:item="{{ $todo->id }}" class="bx flex va-c">
<!-- Drag handle -->
<div x-sort:handle class="cursor-move pxy-025 mr-05 opacity-05">
<x-gt-icon name="drag-vertical" class="wh-1" />
</div>
<!-- Item content -->
<div class="flex space-between w-full ">
<div>{{ $todo->name }}</div>
<div>Position: {{ $todo->position }}</div>
</div>
</div>
@endforeach
</div>
Trouble Shooting
Drag and drop sorting is not holding the position.
- Make sure you set
orderBy('position')
in the query method. (set global scope)