Wiremodal is a framework-agnostic modal package for Laravel, which allows to handle modals, so you don’t have co configure them in all your projects.
It ships a few Livewire-side helpers that make exactly this pleasant. This post is the Livewire integration end to end: opening and closing from PHP, delivering a payload on open, the one trap to avoid, and the optional form panel for when a modal happens to be a form.
How to install
Pull the package in and get the assets onto the page.
composer require edulazaro/wiremodal
php artisan vendor:publish --tag=wiremodal-assets
Enter fullscreen mode Exit fullscreen mode
The service provider auto-registers and there is no config file. Point your layout at the published files:
<link rel="stylesheet" href="{{ asset('vendor/wiremodal/css/wiremodal.css') }}">
<script src="{{ asset('vendor/wiremodal/js/wiremodal.js') }}" defer></script>
Enter fullscreen mode Exit fullscreen mode
If you bundle with Vite, skip the publish and import straight from the vendor directory instead, so a package update flows through without re-publishing anything:
/* resources/css/app.css */
@import "../../vendor/edulazaro/wiremodal/resources/css/wiremodal.css";
Enter fullscreen mode Exit fullscreen mode
// resources/js/app.js
import '../../vendor/edulazaro/wiremodal/resources/js/wiremodal.js';
Enter fullscreen mode Exit fullscreen mode
Opening and closing from Livewire
Define the modal once with the <x-wiremodal> component, give it a name, and fill the body and footer slots. Here is a delete confirmation:
<x-wiremodal name="confirm-delete" title="Delete record?" size="sm">
<x-slot:body>
<p>This action cannot be undone.</p>
</x-slot:body>
<x-slot:footer>
<button type="button" data-wm-dismiss>Cancel</button>
<button type="button" wire:click="destroy">Delete</button>
</x-slot:footer>
</x-wiremodal>
Enter fullscreen mode Exit fullscreen mode
The Cancel button carries data-wm-dismiss, and any element with that attribute closes the modal it sits in, so you never write a cancel handler. To open and close from the component itself, use the macros the package registers on every Livewire component:
public function confirmDelete(): void
{
$this->openModal('confirm-delete');
}
public function destroy(): void
{
// delete the record...
$this->closeModal('confirm-delete');
}
Enter fullscreen mode Exit fullscreen mode
That is the whole open and close cycle from the server, and there is nothing to import: openModal and closeModal are macros wiremodal adds for you.
Handing the modal a payload
A confirmation needs no data, but most modals do: the row you clicked. openModal() takes a second argument, and Wiremodal delivers it to the modal through a wiremodal:opened event whose detail is { name, data }, fired on both the modal element and window. You read it with a one-line Alpine handler.
public function view(int $id): void
{
$task = Task::findOrFail($id);
$this->openModal('task-detail', [
'id' => $task->id,
'title' => $task->title,
]);
}
Enter fullscreen mode Exit fullscreen mode
<x-wiremodal name="task-detail" title="Task" size="md"
x-data="{ task: {} }"
@wiremodal:opened.window="if ($event.detail.name === 'task-detail') task = $event.detail.data || {}">
<x-slot:body>
<p x-text="task.title"></p>
</x-slot:body>
<x-slot:footer>
<button type="button" data-wm-dismiss>Close</button>
</x-slot:footer>
</x-wiremodal>
Enter fullscreen mode Exit fullscreen mode
The if ($event.detail.name === ...) guard matters because the event fires on window for every modal, so each one only reacts to its own payload. The panel fills with the right row the instant it opens.
The positional-dispatch trap
There is one mistake worth calling out, because it fails silently. Do not skip the macro and dispatch the browser event yourself with a positional string:
// Silently never opens the modal
$this->dispatch('open-wiremodal', 'task-detail');
Enter fullscreen mode Exit fullscreen mode
Livewire wraps that positional string into an array, so the browser receives e.detail = ['task-detail']. Wiremodal’s parser accepts a bare string or an object with a name key, never an array, so the modal just never opens and no error is thrown. The openModal() and closeModal() macros exist precisely to avoid this, since they dispatch with named arguments under the hood. If you ever dispatch by hand, use the named form:
$this->dispatch('open-wiremodal', name: 'task-detail', data: ['id' => 42]);
Enter fullscreen mode Exit fullscreen mode
When the modal is a form
Everything above works for any modal. When the modal happens to be a form and you want native submission, the Enter key and a real submit button, add as="form". It renders the panel as a <form> instead of a <div>, so wire:submit fires on submit and the autofocus attribute is honored.
<x-wiremodal name="edit-task" title="Edit task" size="md"
as="form" wire:submit="save"
x-data
@wiremodal:opened.window="
if ($event.detail.name === 'edit-task') {
$wire.set('editingId', $event.detail.data.id);
$wire.set('title', $event.detail.data.title);
}
">
<x-slot:body>
<label>
Title
<input type="text" wire:model="title" autofocus>
</label>
</x-slot:body>
<x-slot:footer>
<button type="button" data-wm-dismiss>Cancel</button>
<button type="submit">Save</button>
</x-slot:footer>
</x-wiremodal>
Enter fullscreen mode Exit fullscreen mode
public ?int $editingId = null;
public string $title = '';
public function save(): void
{
Task::findOrFail($this->editingId)->update(['title' => $this->title]);
$this->closeModal('edit-task');
}
Enter fullscreen mode Exit fullscreen mode
You open it exactly as before with openModal('edit-task', [...]); the only difference is as="form" turning the panel into a real form so Enter and the submit button drive wire:submit. Without it you would hang a wire:click on the Save button instead. That is the point: the form is one prop, not the price of entry.
Sizes and persistent modals
Two props round it out. size takes one of eleven named widths and defaults to 2xl (42rem), running from xs (20rem) to 7xl (80rem); a value outside the catalog throws an InvalidArgumentException at render, so a typo fails loudly instead of producing a wrong-sized box.
Use fullscreen to fill the viewport. And persistent makes a modal ignore overlay clicks and the ESC key, so a wizard step or a you must choose prompt only closes through an explicit data-wm-dismiss button or a programmatic closeModal().
<x-wiremodal name="preview" size="4xl">
<x-wiremodal name="finish-checkout" title="Confirm order" persistent>
Enter fullscreen mode Exit fullscreen mode
👉 Package on Packagist: https://packagist.org/packages/edulazaro/wiremodal
👉 Source on GitHub: https://github.com/edulazaro/wiremodal
답글 남기기