Using Laravel 11, Livewire 3, livewire/sortable plugin ...
I have a list of items which can be dragged and dropped to sort them. Within each item is a button which opens a dropdown. The dropdown is positioned using the Livewire teleport feature, attaching the dropdown content to the page body.
When the page loads all the dropdowns work as expected (showing when the button is clicked). After reordering the items by dragging and dropping some or all of the dropdowns stop showing on the button click. Inspection of the html shows that the teleported elements have been deleted from the page body.
Behaviour is different depending on which item positions are swapped. I have a test list of 5 items. If I swap the positions of items 1 and 2 then all the dropdowns stop working. If I swap them back the dropdowns for items 1 and 2 work again, but the rest don't. If I swap the positions of items 4 and 5, the dropdown for item 4 stops working, but the rest are ok. If I swap them back, the dropdown for item 4 is still not working and the rest still work.
It looks like something is broken in the re-rendering of the page after a data update, but I can't work out exactly what.
I've created some sample code to recreate the problem:
Live wire component (Test.php):
<?php
namespace App\Livewire;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Lazy;
use Livewire\Component;
use Illuminate\View\View;
#[Lazy]
class Test extends Component
{
public $data = array(
array('id' => 1, 'sequence' => 1, 'title' => 'First'),
array('id' => 2, 'sequence' => 2, 'title' => 'Second'),
array('id' => 3, 'sequence' => 3, 'title' => 'Third'),
array('id' => 4, 'sequence' => 4, 'title' => 'Fourth'),
array('id' => 5, 'sequence' => 5, 'title' => 'Fifth'),
);
public function mount()
{
}
#[Computed]
protected function items()
{
$items = $this->data;
usort($items, function ($a, $b) {
return $a['sequence'] > $b['sequence'];
});
return $items;
}
public function render(): View
{
$items = $this->items;
return view('livewire.test', compact('items'));
}
public function updateOrder($orderedIds)
{
if (is_array($orderedIds)) {
foreach ($orderedIds as $orderedId) {
foreach ($this->data as &$dataItem) {
if ($dataItem['id'] == $orderedId['value']) {
$dataItem['sequence'] = $orderedId['order'];
}
}
}
}
}
}
Blade template (livewire-sort.blade.php):
<!DOCTYPE html>
<html lang="en">
<head>
<title>Livewire Sort</title>
<style>
.test-sortable-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
border: solid 1px #ccc;
border-radius: 5px;
background-color: #fff;
width: 400px;
padding: 5px;
margin: 5px;
}
.test-sortable-handle {
cursor: grab;
}
.dropdown {
border: solid 1px #ccc;
border-radius: 5px;
background-color: #fff;
padding: 5px;
}
</style>
</head>
<body>
<h1>Livewire Sort</h1>
@livewire('test')
@livewireScripts
<script src="https://cdn.jsdelivr.net/gh/livewire/[email protected]/dist/livewire-sortable.js"></script>
</body>
</html>
Livewire blade template (test.blade.php):
<div wire:sortable="updateOrder">
@foreach ($items as $item)
<div class="test-sortable-item" wire:key="test-item-{{ $item['id'] }}" wire:sortable.item="{{ $item['id'] }}">
<span class="test-sortable-handle" wire:sortable.handle>{{ $item['title'] }}</span>
<div x-data="{ open: false }">
<div class="test-dropdown" wire:key="test-dropdown-{{ $item['id'] }}">
<button type="button" x-on:click="open = ! open" x-on:click.outside="open = false" x-ref="{{ 'btn' . $item['id'] }}">
Open
</button>
<template x-teleport="body">
<div class="dropdown" x-show="open" x-anchor.bottom-start="$refs.{{ 'btn' . $item['id'] }}" x-transition:enter.origin.top.right x-cloak>
<span>Dropdown option</span>
</div>
</template>
</div>
</div>
</div>
@endforeach
</div>
This is part of a much bigger project, so this is a very cut-down version of the real page, but it does demonstrate the issue. Apologies if I've missed anything that is project specific!
Any suggestions on what's going on?