Skip to content

Pro Component — This trait requires an Aura UI Pro license.

Overview

The WithAuraForm trait enhances Livewire form components with dirty state tracking, unsaved change warnings, and streamlined validation handling. It monitors which form fields have been modified, prevents accidental navigation away from unsaved changes with a browser confirmation dialog, and provides helper methods for common form patterns like reset, save, and cancel.

Installation

Add the trait to your Livewire component:

use BlueStarSystem\AuraUIPro\Traits\WithAuraForm;
use Livewire\Component;

class EditProfile extends Component
{
    use WithAuraForm;

    // ...
}

Configuration

Tracked Properties

By default, the trait tracks all public properties on the component. To limit tracking to specific properties, define a $formFields property:

public array $formFields = ['name', 'email', 'bio', 'avatar'];

Validation Rules

Use standard Livewire validation by defining a rules() method or $rules property:

protected function rules(): array
{
    return [
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email,' . $this->userId,
        'bio' => 'nullable|string|max:500',
    ];
}

Provided Properties

Property Type Default Description
isDirty bool false Whether any tracked field has been modified from its original value.
dirtyFields array [] Array of field names that have been modified.
originalValues array [] Snapshot of field values at mount time (used for dirty comparison).

Provided Methods

Method Description
initializeForm() Takes a snapshot of current values. Called automatically in mount().
isFieldDirty(string $field): bool Check if a specific field has been modified.
getDirtyFields(): array Returns array of modified field names.
getOriginalValue(string $field): mixed Returns the original value of a field before modification.
resetForm() Revert all fields to their original values and clear dirty state.
markAsClean() Clear dirty state without reverting values (call after successful save).
confirmLeave(): bool Returns whether a "leave page" confirmation should be shown.

Blade Integration

The trait works with a small Alpine.js snippet to intercept browser navigation when the form is dirty:

<!-- resources/views/livewire/edit-profile.blade.php -->
<div
    x-data="{ dirty: @entangle('isDirty') }"
    x-on:beforeunload.window="if (dirty) $event.preventDefault()"
>
    <form wire:submit="save">
        <div class="space-y-4">
            <x-aura::input
                label="Full Name"
                name="name"
                wire:model.live="name"
                :error="$errors->first('name')"
            />

            <x-aura::input
                label="Email"
                name="email"
                type="email"
                wire:model.live="email"
                :error="$errors->first('email')"
            />

            <x-aura::editor
                label="Bio"
                name="bio"
                wire:model.live="bio"
                minHeight="120px"
            />
        </div>

        <div class="mt-6 flex items-center justify-between">
            <div>
                @if($isDirty)
                    <span class="text-sm text-amber-600">You have unsaved changes.</span>
                @endif
            </div>

            <div class="flex gap-3">
                <x-aura::button
                    type="button"
                    variant="ghost"
                    wire:click="resetForm"
                    :disabled="!$isDirty"
                >
                    Discard Changes
                </x-aura::button>

                <x-aura::button
                    type="submit"
                    :disabled="!$isDirty"
                >
                    Save Changes
                </x-aura::button>
            </div>
        </div>
    </form>
</div>

Examples

Edit Profile Form

<?php

namespace App\Livewire;

use BlueStarSystem\AuraUIPro\Traits\WithAuraForm;
use Livewire\Component;

class EditProfile extends Component
{
    use WithAuraForm;

    public string $name = '';
    public string $email = '';
    public string $bio = '';
    public ?string $phone = null;

    // Only track these fields for dirty state
    public array $formFields = ['name', 'email', 'bio', 'phone'];

    public function mount(): void
    {
        $user = auth()->user();
        $this->name = $user->name;
        $this->email = $user->email;
        $this->bio = $user->bio ?? '';
        $this->phone = $user->phone;

        // Snapshot current values as "clean" state
        $this->initializeForm();
    }

    protected function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email,' . auth()->id(),
            'bio' => 'nullable|string|max:500',
            'phone' => 'nullable|string|max:20',
        ];
    }

    public function save(): void
    {
        $this->validate();

        auth()->user()->update([
            'name' => $this->name,
            'email' => $this->email,
            'bio' => $this->bio,
            'phone' => $this->phone,
        ]);

        // Mark form as clean after successful save
        $this->markAsClean();

        $this->dispatch('toast', type: 'success', message: 'Profile updated.');
    }

    public function render()
    {
        return view('livewire.edit-profile');
    }
}

Create Record Form

class CreateCustomer extends Component
{
    use WithAuraForm;

    public string $name = '';
    public string $email = '';
    public string $company = '';
    public string $phone = '';
    public string $notes = '';

    public array $formFields = ['name', 'email', 'company', 'phone', 'notes'];

    public function mount(): void
    {
        $this->initializeForm();
    }

    protected function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:customers,email',
            'company' => 'nullable|string|max:255',
            'phone' => 'nullable|string|max:20',
            'notes' => 'nullable|string|max:1000',
        ];
    }

    public function save(): void
    {
        $this->validate();

        $customer = Customer::create([
            'name' => $this->name,
            'email' => $this->email,
            'company' => $this->company,
            'phone' => $this->phone,
            'notes' => $this->notes,
        ]);

        $this->dispatch('toast', type: 'success', message: 'Customer created.');
        $this->redirect(route('customers.show', $customer));
    }

    public function cancel(): void
    {
        if ($this->isDirty) {
            // The browser beforeunload handler will warn the user
            $this->redirect(route('customers.index'));
        } else {
            $this->redirect(route('customers.index'));
        }
    }

    public function render()
    {
        return view('livewire.create-customer');
    }
}

Showing Dirty Field Indicators

You can highlight individual fields that have been modified:

<div>
    <x-aura::input
        label="Name"
        name="name"
        wire:model.live="name"
        :error="$errors->first('name')"
        :class="$this->isFieldDirty('name') ? 'ring-2 ring-amber-300' : ''"
    />
    @if($this->isFieldDirty('name'))
        <p class="mt-1 text-xs text-amber-600">
            Modified (was: "{{ $this->getOriginalValue('name') }}")
        </p>
    @endif
</div>

Settings Form with Multiple Sections

class AppSettings extends Component
{
    use WithAuraForm;

    public string $siteName = '';
    public string $siteDescription = '';
    public string $contactEmail = '';
    public string $timezone = '';
    public string $dateFormat = '';
    public bool $registrationEnabled = true;
    public bool $maintenanceMode = false;

    public array $formFields = [
        'siteName', 'siteDescription', 'contactEmail',
        'timezone', 'dateFormat', 'registrationEnabled', 'maintenanceMode',
    ];

    public function mount(): void
    {
        $settings = Setting::all()->pluck('value', 'key');

        $this->siteName = $settings->get('site_name', '');
        $this->siteDescription = $settings->get('site_description', '');
        $this->contactEmail = $settings->get('contact_email', '');
        $this->timezone = $settings->get('timezone', 'UTC');
        $this->dateFormat = $settings->get('date_format', 'Y-m-d');
        $this->registrationEnabled = (bool) $settings->get('registration_enabled', true);
        $this->maintenanceMode = (bool) $settings->get('maintenance_mode', false);

        $this->initializeForm();
    }

    protected function rules(): array
    {
        return [
            'siteName' => 'required|string|max:255',
            'siteDescription' => 'nullable|string|max:500',
            'contactEmail' => 'required|email',
            'timezone' => 'required|timezone',
            'dateFormat' => 'required|string',
            'registrationEnabled' => 'boolean',
            'maintenanceMode' => 'boolean',
        ];
    }

    public function save(): void
    {
        $this->validate();

        $settings = [
            'site_name' => $this->siteName,
            'site_description' => $this->siteDescription,
            'contact_email' => $this->contactEmail,
            'timezone' => $this->timezone,
            'date_format' => $this->dateFormat,
            'registration_enabled' => $this->registrationEnabled,
            'maintenance_mode' => $this->maintenanceMode,
        ];

        foreach ($settings as $key => $value) {
            Setting::updateOrCreate(['key' => $key], ['value' => $value]);
        }

        $this->markAsClean();
        $this->dispatch('toast', type: 'success', message: 'Settings saved.');
    }

    public function render()
    {
        return view('livewire.app-settings');
    }
}

Discard Changes with Confirmation

@if($isDirty)
    <x-aura::button
        type="button"
        variant="ghost"
        x-on:click="if (confirm('Discard all unsaved changes?')) $wire.resetForm()"
    >
        Discard Changes
    </x-aura::button>
@endif

Live Demo

Demo data resets every 2 hours. Products you create will be visible in other demos until the next reset.

Accessibility

  • The "unsaved changes" warning text uses role="status" with aria-live="polite" to announce state changes.
  • The browser beforeunload confirmation is triggered only when isDirty is true, preventing accidental data loss.
  • The "Discard Changes" button is disabled when no changes exist, using aria-disabled="true".
  • The "Save" button is disabled when no changes exist, preventing unnecessary submissions.
  • Modified field indicators include text descriptions (not just color changes) for screen readers.
  • Form validation errors are associated with their inputs via aria-describedby.
  • The dirty state count is available for screen reader announcements (e.g., "3 unsaved changes").