I use Laravel to manage my projects and I wanted to integrate the drag and drop code I wrote using Alpine.js, so I built a new component. This Laravel component supports passing in variables which builds up the two sides of the drag and drop listing; the default and the selected. This means I can use the same component over and over, but with different lists.
You can create a new component using Laravel's artisan
on the command line. This will create the class and Blade view for you.
php artisan make:component DragNDrop
The Class
We need the component to accept some properties and expose them to the view. We need a type and collection of $form
items
to populate the “from” list. Optionally we can pass a $to
collection which will be used to pre-populated the “to” list and
exclude them from the $from
list.
The defined render()
method reviews the Blade view for the component. Note this is in a sub-folder
from the base components path. There is an itemView()
method which we will discuss later.
<?php
namespace App\View\Components;
use Illuminate\Support\Collection;
use Illuminate\View\Component;
use Illuminate\View\View;
class DragNDrop extends Component {
/** @var string */
public $id;
/** @var string */
public $type;
/** @var \Illuminate\Support\Collection */
public $from;
/** @var \Illuminate\Support\Collection */
public $to;
public function __construct(string $type, Collection $from, ?Collection $to = null)
{
$this->id = uniqid();
$this->type = $type;
$this->from = $from->diff($to);
$this->to = $to;
}
/**
* Look up the view to show for each item from the item model.
* This defaults to the main component item view.
* A model could define its own item view.
*
* @param mixed $item
* @return string
*/
public function itemView($item): string
{
return $item->dragItemView ?? 'components.drag-n-drop.item';
}
public function render(): View
{
return view('components.drag-n-drop.listing');
}
}
The Blade
The HTML for the component listing should match the version from the alpine.js example
with a few changes to loop through the $from
and $to
collections.
I have added the id
property with the $id
variable to the x-data
attribute. This allows us to have multiple instances of
this Blade component without them clashing.
@foreach()
as been used to loop through the $from
and $to
variables. These collections provide data to the item view.
The <li>
id attribute has been updated to use our Blade component scope $id
and unique $item
identifier; id="{{ $id }}--{{ $item->id }}"
Usually our component sits inside a form and we want to know which items are in the $to
list. Each of our items has a hidden input field,
which is submitted with the form. We need to make the inputs inside the from
list to be disabled so they're not submitted.
We have moved the item content to a separate file and we include it using the Blade method @include()
. Useful variables have been
passed to the separate view.
<div x-data="{ id: '{{ $id }}', adding: false, removing: false }" {{ $attributes->merge(['class' => 'drag-and-drop']) }}>
<div class="drag-and-drop__container drag-and-drop__container--from">
<h3 class="drag-and-drop__title">From</h3>
<ul
class="drag-and-drop__items"
:class="{ 'drag-and-drop__items--removing': removing }"
x-on:drop="removing = false"
x-on:drop.prevent="
const id event.dataTransfer.getData('text/plain');
const target = event.target.closest('ul');
const element = document.getElementById(id);
target.appendChild(element);
element.querySelector('input[type=hidden]').disabled = true;
"
x-on:dragover.prevent="removing = true"
x-on:dragleave.prevent="removing = false">
@foreach($from ?? [] as $item)
<li
id="{{ $id }}--{{ $item->id }}"
class="drag-and-drop__item"
:class="{ 'drag-and-drop__item--dragging': dragging }"
x-on:dragstart.self="
dragging = true;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', event.target.id);
"
x-on:dragend="dragging = false"
x-data="{ dragging: false }"
draggable="true">
@include($itemView($item), ['item' => $item, 'id' => $id, 'loop' => $loop, 'disabled' => true])
</li>
@endforeach
</ul>
</div>
<div class="drag-and-drop__divider">⇄</div>
<div class="drag-and-drop__container drag-and-drop__container--to">
<h3 class="drag-and-drop__title">To</h3>
<ul
class="drag-and-drop__items"
:class="{ 'drag-and-drop__items--adding': adding }"
x-on:drop="adding = false"
x-on:drop.prevent="
const id = event.dataTransfer.getData('text/plain');
const target = event.target.closest('ul');
const element = document.getElementById(id);
target.appendChild(element);
element.querySelector('input[type=hidden]').disabled = false;
"
x-on:dragover.prevent="adding = true"
x-on:dragleave.prevent="adding = false">
@foreach($to ?? [] as $item)
<li
id="{{ $id }}--{{ $item->id }}"
class="drag-and-drop__item"
:class="{ 'drag-and-drop__item--dragging': dragging }"
x-on:dragstart.self="
dragging = true;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', event.target.id);
"
x-on:dragend="dragging = false"
x-data="{ dragging: false }"
draggable="true">
@include($itemView($item), ['item' => $item, 'id' => $id, 'loop' => $loop, 'disabled' => false])
</li>
@endforeach
</ul>
</div>
</div>
The item view
I have split the HTML for each item list in to a seperate Blade file. This allows us to customise
the output of each list item based on the $item
variable passed in to the itemView()
method.
The default template item.blade.php
might look like this;
<input type="hidden" name="{{ $type }}[{{ $loop->index }}][id]" value="{{ $item->id }}" {{ $disabled ? 'disabled' : '' }}>
{{ $item->title }}
If the $item
model has an attribute for dragItemView
, then the custom template will be used. For example, I have setup a
mutator method on the $roles
model, which has the path to a custom view.
<?php
namespace App\Models;
class Role extends Model {
public function getDragItemViewAttribute(): ?string
{
return 'role.components.drag-n-drop';
}
}
And the custom role.components.drag-n-drop
Blade template;
<input type="hidden" name="{{ $type }}[{{ $loop->index }}][id]" value="{{ $item->id }}" {{ $disabled ? 'disabled' : '' }}>
<h3>{{ $item->name }}</h3>
<p>({{ $item->level }})</p>
Using the component
We can now use the component multiple times, for example associating roles and groups to a user. This will us two unique component instances, allowing drag and dropping of items within each component and a custom view for each item within the role list.
<form action="{{ route('users.update', $user) }}" method="POST">
<x-drag-drop type="roles" :from="$roles" :to="$user->roles" />
<x-drag-drop type="groups" :from="$groups" :to="$user->groups" />
</form>