Livewire breaking other javascript on page I'm using Liveiwre to handle search, filtering and sorting on a user table.
Each user has a 'manage' dropdown button that opens a sub menu where you can select action such as 'edit', 'delete' etc.
However, the dropdown no longer opens if the page has been sorted...
The javascript in the dropdown uses sequential numbers to distinguish between multiple instances on the same page.
It's almost as though these now become out of sync.
Are there any known ways to avoid Livewire from interfeering with existing JS?
I have tried wire:ignore on the dropdown component but that did not work.
That mostly because when you sort, the element gets deleted from the DOM. Thus, all event attached to it will be removed.
Show your code JS to show the dropdown, so we can able to help more.
Generally, in such a case you should listen to the click event on the document element or any parent that doesn't get removed when sorting.
@MohamedTammam Thanks Mohamed.
The JS looks like this:
<script>
let thisDropdownContainerManage12 = document.querySelector('.tui-dropdown-manage-12');
let thisDropdownManage12 = document.querySelector('#manage-12');
let thisDropdownButtonManage12 = thisDropdownContainerManage12.querySelector('button');
function closeDropdownManage12() {
thisDropdownManage12.classList.add('hidden');
thisDropdownManage12.classList.remove('absolute');
thisDropdownContainerManage12.classList.remove('opened');
}
thisDropdownButtonManage12.addEventListener('click', (e) => {
thisDropdownManage12.classList.toggle('hidden');
thisDropdownManage12.classList.toggle('absolute');
thisDropdownContainerManage12.classList.toggle('opened');
e.stopPropagation();
});
document.addEventListener('click', (e) => {
if (!thisDropdownContainerManage12.contains(e.target)) {
closeDropdownManage12();
}
});
</script>
Each dropdown number is different based on the user ID.
@brandymedia Update it to be
const dropdownSelector = '.tui-dropdown-manage';
const manageSelector = '.manage';
function openDropdown(targetSelector) {
const target = document.querySelector(targetSelector);
if (target) {
target.classList.remove('hidden');
target.classList.add('absolute');
target.classList.add('opened');
}
}
function closeDropdown(exceptSelector) {
const target = document.querySelector(exceptSelector);
document.querySelectorAll(manageSelector).forEach((el) => {
if (el !== target) {
el.classList.add('hidden');
el.classList.remove('absolute');
el.classList.remove('opened');
}
});
}
document.addEventListener('click', (e) => {
// If element matches the selector or is child of an element matches the selector.
if (e.target.matches(dropdownSelector) || e.target.closest(dropdownSelector)) {
openDropdown(e.target.dataset.target);
closeDropdown(e.target.dataset.target);
return;
}
closeDropdown();
});
Make sure to add the dataset-target property to your .tui-dropdown-manage element, Like:
<div dataset-target="#manage-12" class="tui-dropdown-manage"></div>
This code should be added to page only once.
PS: I would rather choose AlpineJS if possible.
I updated my JS to remove the need for a custom identifier...
const dropdowns = document.querySelectorAll('.tui-dropdown');
if (dropdowns) {
dropdowns.forEach(dropdown => {
const button = dropdown.querySelector('button');
const content = dropdown.querySelector('.tui-dropdown-content');
button.addEventListener('click', e => {
e.stopPropagation();
content.classList.toggle('hidden');
});
});
document.addEventListener('click', e => {
dropdowns.forEach(dropdown => {
const content = dropdown.querySelector('.tui-dropdown-content');
content.classList.add('hidden');
});
});
}
But it seems that when livewire sends a request to the server all event listeners are broken.
I would advice you to use alpine.js, which should be bundled in livewire by default which would simplify it a lot, this would then be the code for each dropdown
<div x-data="{ open: false }" @click.outside="open = false" class="tui-dropdown">
<div class="content" x-show="open"></div>
<button @click="open = true">button</button>
</div>
@salfel thanks. The JS is actually part of a UI librray called Turbine UI which I developed as an open source package.
I've considered using Apline JS but currently it does not support it.
Would you say that alpine won't suffer from the same issue as vanilla JS?
@brandymedia alpinejs is directly baked into livewire, because the creator is the same, hence the integration is much better
it depends how you implemented it, maybe you could share your livewire page, that seems to be the issue rather than the js itself
@brandymedia another alternative would be to dispatch a custom event on the server when the table gets ordered
$this->dispatch('ordered')
and then listen on the frontend for it:
Livewire.on('ordered', () => {
// reset eventlisteners
})
@salfel I did try this but still could not get the JS to reapply the event handlers. But this also still means editgin JS that is part of a third-party package which is not ideal.
I'll share the code with you...
The livewire component view:
@foreach ($prospects as $prospect)
<tr wire:key="{{ $prospect->id }}">
<td>
<x-t-link href="{{ route('prospects.show', $prospect->id) }}">
{{ $prospect->name }}
</x-t-link>
</td>
<td><x-t-link href="mailto:{{ $prospect->email }}">{{ $prospect->email }}</x-t-link></td>
<td class="font-mono text-sm">{{ $prospect->created_at->format(config('streamline.prospects.date_format')) }}</td>
<td>
<x-t-dropdown target="manage-{{ $prospect->id }}" border="false" shadow class="z-10">
<x-t-button
prefix="<i class='fa-solid fa-cog'></i>"
suffix="<i class='fa-solid fa-chevron-down fa-fw'></i>"
>
{{ __('Manage') }}
</x-t-button>
<x-slot:content>
<x-t-list-group border="false" class="p-0 gap-0">
<x-t-button
href="{{ route('prospects.edit', $prospect->id) }}"
border="false"
rounded="false"
prefix="<i class='fa-solid fa-pencil fa-fw'></i>"
class:content="text-left"
size="sm"
>
{{ __('Edit') }}
</x-t-button>
<form method="post" action="{{ route('prospects.destroy', $prospect->id) }}" class="delete-form" data-prospect="{{ $prospect->id }}">
@csrf
@method('DELETE')
<x-t-button
border="false"
rounded="false"
prefix="<i class='fas fa-trash fa-fw'></i>"
size="sm"
class="text-left delete-form-button"
full
>
{{ __('Delete') }}
</x-t-button>
</form>
</x-t-list-group>
</x-slot:content>
</x-t-dropdown>
</td>
</tr>
@endforeach
The JS script:
const dropdowns = document.querySelectorAll('.tui-dropdown');
if (dropdowns) {
dropdowns.forEach(dropdown => {
const button = dropdown.querySelector('button');
const content = dropdown.querySelector('.tui-dropdown-content');
button.addEventListener('click', e => {
e.stopPropagation();
content.classList.toggle('hidden');
});
});
document.addEventListener('click', e => {
dropdowns.forEach(dropdown => {
const content = dropdown.querySelector('.tui-dropdown-content');
content.classList.add('hidden');
});
});
}
The JS works on page load but then once sorted or paginated, the event listeners break.
have you tried using livewire's wire:click property, i think that would eliminate this issue
This did not work unfortunately. Still completely stumped on this one.
have you tried adding onclick to the button instead of attaching event listeners to buttons that are there at the start but have been swapped out by livewire on any dom update of the sequence
Please sign in or create an account to participate in this conversation.