chrishenrique / livewire-modal-crud
Livewire Modals Crud easy
Package info
github.com/chrishenrique/livewire-modal-crud
pkg:composer/chrishenrique/livewire-modal-crud
Requires
- php: ^8.2
- illuminate/config: ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/database: ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^11.0 || ^12.0 || ^13.0
- illuminate/routing: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- illuminate/view: ^11.0 || ^12.0 || ^13.0
- livewire/livewire: ^4.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Build CRUD modals with Livewire without wiring the plumbing yourself: dispatch one browser event, and the package mounts your Livewire component inside a Bootstrap modal, resolves the record, picks the title and the button labels from the mode, and closes the modal when you are done.
Requirements
| Package | Version |
|---|---|
| PHP | 8.2+ |
| Laravel | 11, 12, 13 |
| Livewire | 4.x |
| Bootstrap | 4 (with jQuery) or 5 — loaded by your application |
The package renders Bootstrap markup and drives Bootstrap's modal JavaScript. It does not bundle Bootstrap; your layout must already load it.
| Package version | Livewire | Laravel |
|---|---|---|
| 2.x | 4.x | 11–13 |
| 1.x | 3.x | 10–12 |
Installation
composer require chrishenrique/livewire-modal-crud
That is all that is required — the package serves its own JavaScript. Publish only what you want to customise:
php artisan vendor:publish --tag=modal-config # config/modal-crud.php php artisan vendor:publish --tag=modal-views # resources/views/vendor/modal-crud php artisan vendor:publish --tag=modal-js # public/vendor/modal-crud/js
Published JavaScript takes precedence over the built-in files.
Layout
Livewire's own assets plus the modal host, once:
<head> @livewireStyles </head> <body> {{-- ... --}} @livewire('modal') @livewireScripts </body>
Configuration
// config/modal-crud.php return [ // bootstrap4 (requires jQuery) or bootstrap5 'lib' => 'bootstrap5', // URI the package serves its JavaScript from when not published 'asset_route_prefix' => 'modal-crud', ];
Writing a modal
A modal is a Livewire component extending ModalCrud. Point $modelClass at
the record it manages, declare rules(), and give it a form view.
namespace App\Livewire\Modals; use App\Models\User; use ChrisHenrique\ModalCrud\Livewire\ModalCrud; class UsersForm extends ModalCrud { protected $modelClass = User::class; public string $viewForm = 'livewire.modals.users-form'; public string $name = ''; public ?string $email = null; public function rules(): array { return [ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email,'.$this->id, ]; } protected function authorizeModal(): void { $this->authorize($this->getMode(), $this->model); } }
The view extends the package layout and fills the content section. Everything
around it — header, title, footer, submit and cancel buttons — is supplied for
you:
{{-- resources/views/livewire/modals/users-form.blade.php --}} @extends('modal-crud::components.layouts.modal') @section('content') <div class="modal-body"> <div class="mb-3"> <label class="form-label">Nome</label> <input type="text" class="form-control" wire:model="name"> @error('name') <span class="text-danger">{{ $message }}</span> @enderror </div> <div class="mb-3"> <label class="form-label">E-mail</label> <input type="email" class="form-control" wire:model="email"> @error('email') <span class="text-danger">{{ $message }}</span> @enderror </div> </div> @endsection
Opening it
<button class="btn btn-primary" onclick="Livewire.dispatchTo('modal', 'openModal', { component: 'modals.users-form', arguments: { mode: 'create' } })" >Novo</button> <button class="btn btn-secondary" onclick="Livewire.dispatchTo('modal', 'openModal', { component: 'modals.users-form', arguments: { mode: 'edit', id: {{ $user->id }} } })" >Editar</button>
openModal takes a third argument to override the modal's appearance for a
single opening:
Livewire.dispatchTo('modal', 'openModal', { component: 'modals.users-form', arguments: { mode: 'edit', id: 1 }, modalAttributes: { size: 'modal-xl', closeOnClickAway: false } })
Security
Important
component, mode and id all come from the browser. Anyone who can guess a
component name and a record id can dispatch openModal for it.
The ModalContract check that openModal() performs is routing, not
authorization — every ModalCrud satisfies it by inheritance. Each modal is
responsible for its own access control. Override authorizeModal():
protected function authorizeModal(): void { // Runs on every request, after the model is resolved. $this->authorize($this->getMode(), $this->model); }
It runs on the initial mount and on every subsequent round-trip, so access
revoked mid-session takes effect immediately. $mode and $id are #[Locked],
so the client cannot flip a show modal into a delete one.
Modals with no $modelClass and no sensitive data can leave the hook alone.
Modes
| Mode | Title | submit() calls |
View |
|---|---|---|---|
create |
$createTitle |
store() |
viewCreate() |
edit |
$editTitle |
update() |
viewEdit() |
delete |
$deleteTitle |
destroy() |
viewDelete() |
show |
$showTitle |
— (no submit button) | viewShow() |
| anything else | $modalTitle |
action() |
viewDefault() |
Any other string is a valid mode; it falls through to action(), which you
override for custom workflows:
class ArchiveModal extends ModalCrud { protected function action(): void { $this->model->archive(); parent::action(); // closes the modal and fires the hooks } }
The built-in modes are also available as
ChrisHenrique\ModalCrud\Enums\ModalMode if you prefer not to use strings.
Lifecycle hooks
Define any of these on your modal; they are called if they exist.
| Hook | When |
|---|---|
authorizeModal() |
Every request, right after the model is resolved |
beforeValidate() |
Before validate(), on create and edit |
afterValidate() |
After validate() succeeds, before persisting |
stored() |
After a successful create |
changed() |
After a successful edit |
destroyed() |
After a successful delete |
saved() |
After any of the three above, and after action() |
protected function afterValidate(): void { $this->email = strtolower($this->email); } public function saved(): void { $this->dispatch('users-updated')->to('users-table'); }
Closing with events
To refresh a listing when the modal closes:
public function saved(): void { $this->closeModalWithEvents([ 'users-table' => ['refresh', ['page' => 1]], // to a specific component ['user-saved', ['id' => $this->model->id]], // global, with payload 'something-happened', // global, no payload ]); }
Per-modal options
Override any of these statics on your modal class.
| Method | Default | Effect |
|---|---|---|
modalSize() |
'' |
Class on .modal-dialog (modal-sm, modal-lg, modal-xl, modal-fullscreen, …) |
modalCentered() |
true |
Vertically centers the dialog |
modalScrollable() |
true |
Scrolls the body instead of the page |
closeModalOnClickAway() |
true |
Backdrop click dismisses |
closeModalOnEscape() |
true |
Escape dismisses |
destroyOnClose() |
true |
Tears the component down after hiding |
dispatchCloseEvent() |
true |
Dispatches a modalClosed Livewire event on close |
public static function modalSize(): string { return 'modal-xl'; }
Customising labels
Every label is a public property you can set on the class or pass through
arguments:
public string $createTitle = 'Cadastrar usuário'; public string $editTitle = 'Editar usuário'; public string $deleteTitle = 'Remover usuário'; public string $deleteMessage = 'Esta ação não pode ser desfeita. Continuar?'; public string $modalBtn = 'Salvar'; public string $deleteBtn = 'Remover'; public string $modalClose = 'Cancelar'; public string $showClose = 'Voltar';
To restrict what gets persisted, set $fillable. Leave it empty and the
validated data is saved as-is:
public array $fillable = ['name', 'email'];
Upgrading from 1.x
2.0 requires Livewire 4, Laravel 11+ and PHP 8.2+.
- Stacked-modal calls must be removed.
skipPreviousModal(),skipPreviousModals(),destroySkippedModals()andforceClose()were no-ops backed by a commented-out component stack. Delete the calls. - Re-publish your views. Views now live under the
modal-crudnamespace and publish toresources/views/vendor/modal-crud. The oldmodalcrud::prefix still resolves but is deprecated. In 1.x the publish tag wrote to a directory Laravel never read, so your published copies were being ignored — check them against the new defaults before re-publishing. vendor:publish --tag=modal-jsis optional now. Deletepublic/vendor/modal-crud/jsunless you customised it; the package serves its own scripts.ChrisHenrique\ModalCrud\Modalis nowModalAssets. TheModalfacade andapp('modal')are unchanged. If you were type-hinting the concrete class, rename it.<x-modal-component>was removed. It rendered a view outside the package and was used by nothing.$this->component['attributes']was removed from the descriptor; usearguments.- Add
authorizeModal()to every modal that touches a record. 1.x had no authorization hook at all, so this is likely a gap in your application, not just an upgrade step.
See the CHANGELOG for the full list.
Testing
composer test # phpunit composer analyse # phpstan composer format # pint
License
MIT. See LICENSE.
Issues and pull requests: chrishenrique/livewire-modal-crud.