Skip to content
Brok UI

Loading…

No results

Livewire

Brok supports Livewire ^3.8.5 and ^4.4.1. Livewire binds to the primitives themselves. Native form controls receive wire:* on the real control. A component whose value or open state lives in Alpine exposes it through x-modelable, the contract that wire:model (and x-model) attach to. No Livewire-specific markup is rendered, so the same markup stays useful as plain Blade and HTML.

Note

CI runs the package contract against both supported Livewire majors. A real browser fixture also tests focus, validation, targeted loading, modal state, toast output, and destructive confirmation.

Model timing

Use deferred binding for normal forms, debounced live binding for search, and blur binding for early validation. Livewire 4.1 changed bare .blur: it delays client state but does not send a request. Use wire:model.live.blur when a server request must run on blur in both Livewire 3 and 4.

resources/views/livewire/profile-form.blade.php Blade
{{-- Deferred until the next action --}}
<brok:input wire:model="email" />

{{-- Debounced server requests --}}
<brok:input wire:model.live.debounce.300ms="search" />

{{-- Cross-major server request on blur --}}
<brok:input wire:model.live.blur="email" />

A bare wire:model.blur is still supported and forwarded unchanged. Its network timing follows the installed Livewire version. Input, Textarea, Select, Checkbox, Radio, Switch, Dropzone, and Input Group place the binding on their native control, not on a wrapper.

Alpine-held state

Some components keep their value in Alpine because no native control can carry it: the open state of Dialog, Alert Dialog, Sheet, Drawer, Popover, Dropdown and Collapsible; the active Tabs value; and the values of Rating, Tags Input, Input OTP, Select Advanced, Combobox, Date Picker, Date Range Picker, Time Picker, Number Field, Slider, Slider Pips, Toggle Group, Calendar, Mini Calendar, Wheel Picker, Angle Slider, Elastic Slider and Swap. Put wire:model on the component; the same attribute works with x-model in a plain Alpine scope.

resources/views/livewire/settings.blade.php Blade
<brok:tabs wire:model="tab">
    <brok:tabs.list>
        <brok:tabs.trigger value="profile">Profile</brok:tabs.trigger>
        <brok:tabs.trigger value="billing">Billing</brok:tabs.trigger>
    </brok:tabs.list>
    …
</brok:tabs>

<brok:rating wire:model.live="score" />

<brok:select-advanced wire:model="plan">
    <brok:select-advanced.option value="starter">Starter</brok:select-advanced.option>
    <brok:select-advanced.option value="pro">Pro</brok:select-advanced.option>
</brok:select-advanced>

<brok:tags-input wire:model="tags" />
<brok:date-picker wire:model="startsAt" />
<brok:slider wire:model.live="volume" />

The server property is the source of truth: on load it seeds the client state, and a value set on the server (a reset, a preset) updates the control, its label and its view. A range slider, a range calendar and the date range picker bind an array or a { start, end } pair; a multiple combobox or toggle group binds an array. .live sends the request on every change; a bare wire:model defers it to the next action.

A Choicebox group is the one native-control group that takes wire:model on the group: <brok:choicebox.group wire:model="plan"> binds one value for radio cards and an array for type="checkbox". Every modifier works, .blur included (it fires when focus leaves the group), and a server reset clears the checked card, also in a dialog that opens again. A single card can take wire:model too; it reaches the card's native control.

Validation and nested fields

Property validation gives early feedback. A save method must call the complete $this->validate() rule set before persistence.

app/Livewire/ProfileForm.php PHP
#[Validate('required|email')]
public string $email = '';

public array $profile = ['email' => ''];

protected function rules(): array
{
    return [
        'email' => ['required', 'email'],
        'profile.email' => ['required', 'email'],
    ];
}

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

    // Persist only after all rules pass.
}
resources/views/livewire/profile-form.blade.php Blade
<form wire:submit="save">
    <brok:field name="email" label="Email">
        <brok:input type="email" wire:model.live.blur="email" />
    </brok:field>

    <brok:field name="profile.email" label="Profile email">
        <brok:input type="email" wire:model.live.blur="profile.email" />
    </brok:field>

    <brok:button type="submit" wire-target="save" wire-loading-label="Saving…">
        Save
    </brok:button>
</form>

The Livewire and Laravel error key stays profile.email. Brok converts the plain form name to profile[email]. Use html-name only when the server needs another HTML name.

Loading and disabled states

Target the action that owns the request. Button adds wire:target, disables itself during that request, and can show delayed progress and a loading label. It does not disable an unrelated control.

resources/views/livewire/project-row.blade.php Blade
<brok:button
    wire:click="archive"
    wire-target="archive"
    wire-loading-label="Archiving…"
>
    Archive
</brok:button>

<p wire:loading.delay wire:target="archive" role="status">
    Archiving…
</p>

Search, pagination, and sorting

Reset pagination when a filter or page size changes. Use a server allowlist for every sort key. A field name from wire:click is still untrusted input.

app/Livewire/UserIndex.php PHP
public string $search = '';

public function updatedSearch(): void
{
    $this->resetPage();
}
resources/views/livewire/user-index.blade.php Blade
<brok:input-group>
    <brok:input-group.addon>
        <brok:icon name="search" />
    </brok:input-group.addon>
    <brok:input-group.input
        type="search"
        wire:model.live.debounce.300ms="search"
        placeholder="Search users…"
        aria-label="Search users"
    />
    <brok:input-group.clear />
</brok:input-group>
app/Livewire/UserIndex.php PHP
private const SORTABLE_FIELDS = ['name', 'email', 'created_at'];

#[Locked]
public string $sortField = 'name';

#[Locked]
public string $sortDirection = 'asc';

public function sortBy(string $field): void
{
    abort_unless(in_array($field, self::SORTABLE_FIELDS, true), 400);

    $this->sortDirection = $this->sortField === $field
        ? ($this->sortDirection === 'asc' ? 'desc' : 'asc')
        : 'asc';
    $this->sortField = $field;
    $this->resetPage();
}

Livewire Data Table requires a stable non-empty row key. It uses id by default or the configured row-key. It never falls back to a loop index.

Optimistic UI

Optimistic changes are suitable only for reversible, low-risk state. Keep the server authoritative and restore the previous state if the request fails. Do not use this pattern for payments, permissions, inventory, or deletion.

resources/views/livewire/favorite-button.blade.php Blade
<button
    type="button"
    x-data="{ active: @js($favorite), busy: false }"
    x-bind:aria-pressed="active"
    x-bind:disabled="busy"
    x-on:click="
        const previous = active;
        active = !active;
        busy = true;
        $wire.setFavorite(active)
            .catch(() => { active = previous })
            .finally(() => { busy = false });
    "
>
    Favorite
</button>

Focus and stable DOM

Keep the focused native control in the DOM. Do not change its element type, position, or key during a request. Never key search by its current value. Give repeated stateful rows a database-backed wire:key. Use wire:ignore only for a descendant region that a third-party library fully owns.

The Input Group clear control dispatches a native input event and returns focus to the input, so a debounced wire:model sees the change. The browser contract verifies that focus remains after a debounced request and DOM morph.

Set the initial state from Blade or bind the same state to Livewire. Dialog exposes its open state through x-modelable, so Blade/Alpine use does not need Livewire.

resources/views/livewire/project-modal.blade.php Blade
{{-- Blade/Alpine --}}
<brok:dialog :open="$showWelcome">
    <brok:dialog.content>…</brok:dialog.content>
</brok:dialog>

{{-- Bidirectional Livewire state --}}
<brok:dialog wire:model.live="confirmOpen">
    <brok:dialog.content>
        <brok:dialog.header>
            <brok:dialog.title>Delete project</brok:dialog.title>
        </brok:dialog.header>
        …
        <brok:dialog.close />
    </brok:dialog.content>
</brok:dialog>

Focus enters the dialog, remains trapped, and returns to the prior control after close. Escape and the backdrop close it. Closing writes false back to the server property, and a server-side $set('confirmOpen', true) opens it.

Flash messages and toast events

app/Livewire/ProfileForm.php PHP
public function save(): void
{
    $this->validate();

    // In-place update: raise the toast at once.
    $this->dispatch('toast', message: 'Profile saved.', variant: 'success');
}

public function archive(): void
{
    // Redirect: the toaster renders the flash on the next page.
    session()->flash('status', 'Project archived.');

    $this->redirectRoute('projects.index');
}
resources/views/livewire/profile-form.blade.php Blade
{{-- Once, in the layout --}}
<brok:toaster :flash="session('status')" flash-variant="success" />

The toaster listens for the toast window event (rename it with event="notify"); a Livewire dispatch and a plain CustomEvent both reach it, so no Livewire code is involved. Use the event for an in-place update and the flash for a redirect. Do not use both for one action, or the toast shows twice.

File uploads and progress

Dropzone forwards wire:model to the native file input and listens for the standard Livewire upload lifecycle. It renders an accessible progress bar and reports failure or cancellation. Client checks improve feedback; the server must validate again.

app/Livewire/DocumentUpload.php PHP
use Livewire\WithFileUploads;

public mixed $document = null;

public function saveUpload(): void
{
    $validated = $this->validate([
        'document' => ['required', 'file', 'mimes:pdf', 'max:10240'],
    ]);

    $validated['document']->store('documents');
}
resources/views/livewire/document-upload.blade.php Blade
<brok:dropzone
    name="document"
    wire:model="document"
    accept="application/pdf"
    :max-size="10"
/>

<brok:button wire:click="saveUpload" wire-target="saveUpload">
    Save upload
</brok:button>

Destructive confirmation

An alert dialog with wire-action asks first, prevents duplicate calls while busy, and restores trigger focus. The server action must still resolve the record and authorize the operation.

resources/views/livewire/user-row.blade.php Blade
<brok:alert-dialog wire-action="deleteUser" :wire-params="[$user->id]">
    <brok:alert-dialog.trigger>Delete user</brok:alert-dialog.trigger>
    <brok:alert-dialog.content>
        <brok:alert-dialog.header>
            <brok:alert-dialog.title>Delete this user?</brok:alert-dialog.title>
        </brok:alert-dialog.header>
        <brok:alert-dialog.footer>
            <brok:alert-dialog.cancel>Cancel</brok:alert-dialog.cancel>
            <brok:alert-dialog.action variant="destructive">Delete</brok:alert-dialog.action>
        </brok:alert-dialog.footer>
    </brok:alert-dialog.content>
</brok:alert-dialog>
app/Livewire/UserIndex.php PHP
public function deleteUser(int $userId): void
{
    $user = User::query()->findOrFail($userId);
    $this->authorize('delete', $user);
    $user->delete();
}

Without Livewire, the confirmation prompt remains usable but the server call is inert. Use a normal POST/DELETE form when a complete non-Livewire destructive flow is required.

Reference components

Component Contract
input-groupNative model on the input, a clear control that keeps focus, debounce through the directive.
fieldLabel, hint, the shared error bag, and accessible error linkage.
dialog, tabs, and the Alpine-held state list aboveBidirectional wire:model through x-modelable; server state seeds and updates the control.
toasterServer-dispatched toast events and flash input.
livewire-data-tableSearch, pagination, allowlisted sorting, selection, stable row keys.
alert-dialog (wire-action)Busy-safe destructive confirmation and focus return.
dropzoneNative file model plus upload progress, failure, and cancellation.

The retired livewire-modal, livewire-tabs, livewire-field, livewire-search, livewire-confirm and livewire-toast adapters are aliases: ui:add installs the primitive instead. Bind to the primitive.