mckenziearts / livewire-unsaved-changes
A Livewire component to display an unsaved changes bar with Alpine.js
Fund package maintenance!
mckenziearts
www.paypal.com/paypalme/monneyarthur
Installs: 13
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/mckenziearts/livewire-unsaved-changes
Requires
- php: ^8.3
- illuminate/contracts: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- illuminate/view: ^11.0|^12.0
- livewire/livewire: ^3.0|^4.0
- spatie/laravel-package-tools: ^1.19
Requires (Dev)
- laravel/pint: ^1.25.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- pestphp/pest-plugin-type-coverage: ^4.0
- phpstan/phpstan: ^2.1.27
- rector/rector: ^2.1.7
README
Introduction
A beautiful, animated "unsaved changes" bar for Laravel Livewire forms. Powered by Alpine.js for instant UI feedback without server round-trips.
Features
- Animated slide-in bar when form changes are detected
- No server requests until save - instant UI feedback
- Prevent navigation with unsaved changes (optional)
- Customizable position (top/bottom)
Installation
composer require mckenziearts/livewire-unsaved-changes
Styling
The component uses Tailwind CSS classes. Choose the appropriate method based on your project setup:
Option 1: Project with Tailwind CSS (Recommended)
If your project already uses Tailwind CSS, simply add the package's views to your source paths in your CSS file:
@import 'tailwindcss'; @source '../../vendor/mckenziearts/livewire-unsaved-changes/resources/views/**/*.blade.php';
This allows Tailwind to scan the component's Blade files and generate the necessary classes automatically.
Option 2: Project without Tailwind CSS
If your project doesn't use Tailwind CSS, publish the pre-compiled CSS assets:
php artisan vendor:publish --tag="livewire-unsaved-changes-assets"
Then include the CSS in your layout:
<link rel="stylesheet" href="{{ asset('vendor/livewire-unsaved-changes/unsaved-changes.css') }}">
Optional Configuration
Publish the config file:
php artisan vendor:publish --tag="livewire-unsaved-changes-config"
Publish the translations:
php artisan vendor:publish --tag="livewire-unsaved-changes-translations"
Usage
Basic Usage
Wrap your form with the <x-unsaved-changes> component and use x-model to bind your inputs:
<x-unsaved-changes :$form save-method="save"> <div class="space-y-4"> <input x-model="form.name" type="text" /> <input x-model="form.email" type="email" /> </div> </x-unsaved-changes>
In your Livewire component:
<?php declare(strict_types=1); namespace App\Livewire; use Livewire\Component; class Settings extends Component { public array $form = [ 'name' => '', 'email' => '', ]; public function mount(): void { $this->form = [ 'name' => auth()->user()->name, 'email' => auth()->user()->email, ]; } public function save(array $form): void { $this->validate([ 'form.name' => 'required|string|max:255', 'form.email' => 'required|email', ]); auth()->user()->update($form); $this->form = $form; session()->flash('success', 'Settings saved!'); } }
With Livewire Form Objects
<?php declare(strict_types=1); namespace App\Livewire\Forms; use Livewire\Form; class SettingsForm extends Form { public string $name = ''; public string $email = ''; public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email'], ]; } }
<?php declare(strict_types=1); namespace App\Livewire; use App\Livewire\Forms\SettingsForm; use Livewire\Component; class Settings extends Component { public SettingsForm $form; public function mount(): void { $this->form->name = auth()->user()->name; $this->form->email = auth()->user()->email; } public function save(array $form): void { $this->form->fill($form); $this->form->validate(); auth()->user()->update($this->form->all()); session()->flash('success', 'Settings saved!'); } }
<x-unsaved-changes :form="$form->all()" save-method="save"> <input x-model="form.name" type="text" /> <input x-model="form.email" type="email" /> </x-unsaved-changes>
Using the Trait (Optional)
The package includes a trait that provides a default saveChanges method:
declare(strict_types=1); use ShopperLabs\LivewireUnsavedChanges\Traits\WithUnsavedChanges; class Settings extends Component { use WithUnsavedChanges; public array $form = []; public function saveChanges(array $form): void { parent::saveChanges($form); // Updates $this->form // Your save logic here auth()->user()->update($this->form); } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
form |
array |
[] |
The form data to track |
save-method |
string |
saveChanges |
The Livewire method to call on save |
color |
string |
blue |
Save button color (see available colors below) |
position |
string |
bottom |
Bar position: top or bottom |
prevent-navigation |
bool |
false |
Show browser confirmation when leaving with unsaved changes |
message |
string |
(translated) | Custom message text |
save-label |
string |
(translated) | Custom save button label |
discard-label |
string |
(translated) | Custom discard button label |
Available Colors
The color prop accepts any of the following Tailwind CSS color names:
red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose
Examples
Position at top:
<x-unsaved-changes :$form position="top"> ... </x-unsaved-changes>
Prevent navigation:
<x-unsaved-changes :$form prevent-navigation> ... </x-unsaved-changes>
Custom color:
<x-unsaved-changes :$form color="green"> ... </x-unsaved-changes>
Custom labels:
<x-unsaved-changes :$form message="You have pending changes" save-label="Save now" discard-label="Reset" > ... </x-unsaved-changes>
Configuration
// config/unsaved-changes.php return [ // Bar position: 'bottom' or 'top' 'position' => 'bottom', // Show browser confirmation when leaving with unsaved changes 'prevent_navigation' => false, ];
Translations (optional)
The package includes English and French translations. You can publish and customize them:
php artisan vendor:publish --tag="livewire-unsaved-changes-translations"
Important: Use x-model, not wire:model
This component requires x-model for form bindings. Using wire:model will not work because:
wire:modelsyncs data with the server (Livewire)- The component tracks changes using Alpine.js (client-side)
- Alpine won't detect changes made via
wire:model
{{-- Correct --}} <input x-model="form.name" /> {{-- Won't work --}} <input wire:model="form.name" />
This is by design - the whole point is to avoid server requests until the user saves.
Customization
To customize the component markup, publish the views:
php artisan vendor:publish --tag="livewire-unsaved-changes-views"
License
MIT License. See LICENSE for more information.