1. Čo mení Livewire 4
Livewire 4 je najväčšie vydanie v histórii frameworku. Samotný model komponentu sa nezmenil — PHP trieda a Blade šablóna, stav na serveri, HTML po drôte — no takmer všetko okolo neho sa posunulo: kde komponenty žijú, ako sa píšu a akú časť stránky zasiahne jedna aktualizácia.
Päť najdôležitejších vecí
| Novinka | Čo prináša |
|---|---|
| Jednosúborové komponenty | Trieda aj značkovanie žijú v jednom súbore .blade.php. Žiadne preskakovanie medzi dvoma priečinkami kvôli dvadsaťriadkovému komponentu. |
| Ostrovy | Izolované oblasti vnútri komponentu, ktoré sa prekresľujú samostatne. Tikajúce počítadlo tržieb už znova nespúšťa dotazy celého dashboardu. |
| Sloty | Rodič vkladá značkovanie do potomka — vyhodnocuje sa v kontexte rodiča, takže wire:click v slote volá metódu rodiča. |
| Optimistické UI | wire:show, wire:text, wire:bind aktualizujú DOM okamžite na klientovi, bez cesty na server. |
| Drag and drop | wire:sort je zabudovaný. Žiadny SortableJS, žiadny lepiaci kód. |
Je to prepísanie aplikácie?
Nie. Livewire 4 si zachováva vysokú spätnú kompatibilitu: triedne komponenty fungujú ďalej. Jednosúborový formát je predvolený pre nové komponenty, nie nútená migrácia. Existuje sada nekompatibilných zmien, ktoré si oplatí prečítať pred upgradom — sú zhrnuté v porovnaní Livewire 3 a 4 a oficiálna dokumentácia k upgradu je smerodajný zoznam.
wire:model, ostrovy a sloty. Zvyšok modelu
komponentu bude pôsobiť známo.
2. Inštalácia a požiadavky
composer require livewire/livewire:^4.0
php artisan optimize:clear
Ako v tretej verzii sa assety vkladajú automaticky. @livewireStyles a
@livewireScripts treba umiestniť ručne len pri neštandardnom layoute alebo prísnej
Content Security Policy.
/livewire/ sa stalo /livewire-{hash}/. Ak máte pravidlá firewallu, výnimky
WAF, obchádzky CDN alebo location bloky v nginxe naviazané na doslovnú cestu /livewire/,
treba ich rozšíriť na nový tvar — inak bude aplikácia v produkcii vyzerať pokazene, hoci na notebooku
funguje bezchybne.
Publikovanie konfigurácie
php artisan livewire:publish --configNastavenia, ktoré sú vo štvrtej verzii nové alebo premenované:
| Nastavenie | Účel |
|---|---|
component_locations | Priečinky, v ktorých sa hľadajú komponenty. Predvolene resources/views/components a resources/views/livewire. |
component_namespaces | Pomenované korene komponentov, napr. pages::. |
component_layout | Vo v3 layout. Používa namespace layouts::. |
component_placeholder | Vo v3 lazy_placeholder. |
make_command | Určuje, čo generuje make:livewire: jednosúborový alebo triedny komponent. |
smart_wire_keys | Teraz predvolene true. |
csp_safe | Vytvorí build, ktorý vyhovuje prísnejším Content Security Policy. |
Voľba predvoleného štýlu komponentov
// Naďalej generovať triedne komponenty namiesto jednosúborových
'make_command' => [
'type' => 'class',
],3. Jednosúborové komponenty
Hlavná zmena v každodennej práci. Komponent je teraz jeden Blade súbor, ktorý začína PHP blokom s anonymnou triedou a pokračuje značkovaním.
php artisan make:livewire post.create
# resources/views/components/post/⚡create.blade.php<?php
use Livewire\Component;
use App\Models\Post;
use Livewire\Attributes\Validate;
new class extends Component {
#[Validate('required|string|min:5|max:180')]
public string $title = '';
#[Validate('required|string|min:50')]
public string $body = '';
public function save()
{
$this->validate();
Post::create([
'title' => $this->title,
'body' => $this->body,
'user_id' => auth()->id(),
]);
$this->reset();
$this->dispatch('notify', message: 'Post created');
}
};
?>
<div>
<form wire:submit="save">
<input type="text" wire:model="title" placeholder="Title">
@error('title') <span class="error">{{ $message }}</span> @enderror
<textarea wire:model="body" placeholder="Body"></textarea>
@error('body') <span class="error">{{ $message }}</span> @enderror
<button type="submit">Save post</button>
</form>
</div>
Verejné vlastnosti sú v značkovaní dostupné ako bežné premenné — {{ $title }} funguje
rovnako ako v samostatnom súbore šablóny.
O tom emoji
Predvolený názov súboru nesie predponu ⚡. Je to vizuálny znak, ktorý odlišuje Livewire
komponenty od bežných Blade komponentov v tom istom priečinku, a je voliteľný —
v config/livewire.php sa dá vypnúť, ak vaše nástroje, terminál alebo tím uprednostňujú
obyčajné názvy.
Viacsúborové komponenty
Keď komponent prerastie pohodlnú veľkosť jedného súboru, vygenerujte ho ako viacsúborový komponent a trieda, šablóna a test budú opäť oddelené.
php artisan make:livewire post.create --mfcTriedne komponenty fungujú ďalej
<?php
namespace App\Livewire;
use Livewire\Component;
class CreatePost extends Component
{
public string $title = '';
public function render()
{
return view('livewire.create-post');
}
}app/Livewire.
4. Umiestnenie, pomenovanie a namespace
Livewire komponenty už predvolene nežijú vo vlastnom izolovanom priečinku
resources/views/livewire; sedia vedľa ostatných Blade komponentov.
resources/views/
├── components/
│ ├── ⚡counter.blade.php → <livewire:counter />
│ ├── post/
│ │ ├── ⚡create.blade.php → <livewire:post.create />
│ │ └── ⚡index.blade.php → <livewire:post.index />
│ └── button.blade.php (a plain Blade component)
└── pages/
└── ⚡dashboard.blade.php → <livewire:pages::dashboard />Vykresľovanie
{{-- Podľa mena --}}
<livewire:counter />
{{-- Vnorené priečinky bodkou --}}
<livewire:post.create />
{{-- S namespace --}}
<livewire:pages::post.create />
{{-- S props --}}
<livewire:post.create :title="$initialTitle" :author="$user" /><livewire:some-component> jednoducho nevykreslí — bez chyby, bez výstupu. Vždy
samouzatvárajte: <livewire:some-component />. Toto je najčastejšie prekvapenie pri
prenose Blade súborov z v3.
Smerovanie na komponent
Celostránkové komponenty majú vlastné makro routy. Starý zápis
Route::get('/dashboard', Dashboard::class) nahrádza:
use App\Livewire\Dashboard;
// Podľa triedy
Route::livewire('/dashboard', Dashboard::class);
// Podľa mena komponentu — bežná voľba pre komponenty založené na šablóne
Route::livewire('/dashboard', 'pages::dashboard')
->middleware('auth')
->name('dashboard');Layouty
'component_layout' => 'layouts::app',use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
new class extends Component {
#[Layout('layouts::app')]
#[Title('Dashboard')]
public function render() { /* … */ }
};5. Vlastnosti a props
Verejné vlastnosti sú stále stavom komponentu, stále sa medzi požiadavkami serializujú do snapshotu
a stále platia rovnaké obmedzenia typov: skaláry, polia skalárov, Eloquent modely a kolekcie,
Carbon, enumy.
new class extends Component {
public string $name = '';
public ?int $age = null;
public array $tags = [];
public bool $isPublic = false;
};Props od rodiča: #[Prop]
Hodnoty odovzdané z rodičovskej šablóny sa dajú deklarovať výslovne. Verejné rozhranie komponentu je tak čitateľné na prvý pohľad a oddeľuje „toto prichádza zvonku“ od „toto je interný stav“.
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $type = 'info';
#[Prop]
public bool $dismissible = false;
};
?>
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $slot }}
@if ($dismissible)
<button wire:click="$dispatch('dismiss')">×</button>
@endif
</div><livewire:alert type="warning" dismissible class="mt-4">
The invoice is overdue.
</livewire:alert>mount() beží stále ako prvý
use App\Models\Order;
new class extends Component {
public Order $order;
public string $mode;
public function mount(Order $order, string $mode = 'compact'): void
{
$this->order = $order;
$this->mode = $mode;
}
};#[Locked] je rovnako dôležitý ako predtým
use Livewire\Attributes\Locked;
new class extends Component {
#[Locked]
public int $invoiceId; // klient nemôže podstrčiť iné id
public string $note = '';
};6. wire:model vo štvrtej verzii
wire:model sa vo v4 správajú inak a oba zlyhávajú ticho, nie hlučne.
Zmena 1: modifikátory teraz riadia synchronizáciu na klientovi
Vo v3 .blur a .change určovali len to, kedy odíde sieťová požiadavka.
Vo v4 určujú, kedy sa hodnota vôbec synchronizuje — aj na klientovi. Pre staré správanie
pridajte .live.
{{-- Livewire 3 --}}
<input wire:model.blur="title">
{{-- Ekvivalent rovnakého správania vo v4 --}}
<input wire:model.live.blur="title">Zmena 2: udalosti už nebublajú od potomkov
wire:model na obale už nezachytáva udalosti bublajúce z vnorených polí. Ak ste sa na to
spoliehali, pridajte .deep.
{{-- Livewire 3: obal zachytil udalosť poľa --}}
<div wire:model="value">
<input type="text">
</div>
{{-- Livewire 4: zapneme výslovne --}}
<div wire:model.deep="value">
<input type="text">
</div>Zvyšok bez zmeny
{{-- Predvolene odložené --}}
<input type="text" wire:model="name">
{{-- Požiadavka pri každej zmene --}}
<input type="text" wire:model.live="search">
{{-- Živé vyhľadávanie s oneskorením --}}
<input type="text" wire:model.live.debounce.400ms="search">
{{-- Vnorené dáta --}}
<input wire:model="form.address.city">
<input wire:model="post.title">post.title) stále vyžaduje zodpovedajúce validačné pravidlo.
Je to ochrana pred hromadným priradením a do v4 prešla nezmenená.
7. Akcie, async a renderless
Akcie sa deklarujú presne ako predtým — verejné metódy volané zo značkovania.
new class extends Component {
public function save()
{
$this->validate();
// …
}
};<button wire:click="save">Save</button>
<form wire:submit="save">…</form>
<button wire:click="delete({{ $post->id }})"
wire:confirm="Delete this post? This cannot be undone.">Delete</button>Akcie bez prekreslenia
Keď akcia nemení nič, čo používateľ vidí — zvýšenie počítadla zobrazení, zápis riadku auditu —, vynechanie prekreslenia ušetrí celý prechod šablónou aj porovnanie DOM.
use Livewire\Attributes\Renderless;
new class extends Component {
#[Renderless]
public function incrementViewCount(): void
{
$this->post->increment('views');
}
};{{-- Alebo zo strany šablóny --}}
<button type="button" wire:click.renderless="incrementViewCount">Track</button>Imperatívnym ekvivalentom vnútri metódy je stále $this->skipRender().
Asynchrónne akcie
#[Async] (alebo modifikátor .async) vykoná akciu paralelne,
mimo bežného frontu požiadaviek. Ideálne pre „vystrelil a zabudol“: analytika, logovanie, zahrievanie
cache.
use Livewire\Attributes\Async;
new class extends Component {
#[Async]
public function logInteraction(string $element): void
{
Analytics::record($element, auth()->id());
}
};<button wire:click.async="logInteraction('cta-hero')">Get started</button>Magické akcie
<button wire:click="$refresh">Refresh</button>
<button wire:click="$set('tab', 'settings')">Settings</button>
<button wire:click="$toggle('showFilters')">Filters</button>
<button wire:click="$dispatch('open-modal', { name: 'create' })">New</button>
<button wire:click="$parent.closeModal()">Close</button>protected alebo private, aby sa nedali zavolať z klienta.
8. Životný cyklus a hooky
Životný cyklus je oproti tretej verzii nezmenený. Znalosť poradia stále odstráni väčšinu mätúcich chýb.
1. Component instance created
2. boot()
3. mount($params)
4. booted()
5. render() 1. POST /livewire-{hash}/update arrives with the state snapshot
2. Snapshot checksum verified
3. Component instance created
4. boot()
5. State restored from the snapshot
6. hydrate() / hydrateFoo()
7. booted()
8. updating($prop, $value) / updatingFoo($value)
9. Properties receive new values
10. updated($prop, $value) / updatedFoo($value)
11. Queued actions run
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — state packed back into the snapshot
14. Response: markup + effectsnew class extends Component {
public float $price = 0;
public function boot(): void {}
public function booted(): void {}
public function hydrate(): void {}
public function dehydrate(): void {}
public function updating(string $property, mixed $value): void {}
public function updated(string $property, mixed $value): void
{
$this->validateOnly($property);
}
// Pre konkrétnu vlastnosť
public function updatedPrice(mixed $value): void
{
$this->price = round((float) $value, 2);
}
// Vnorený kľúč form.email
public function updatedFormEmail(mixed $value): void
{
$this->form['email'] = strtolower(trim($value));
}
};9. Validácia a Form objekty
Validácia prešla z v3 bez zmeny, vrátane atribútov a Form objektov.
use Livewire\Attributes\Validate;
new class extends Component {
#[Validate('required|string|min:2|max:80')]
public string $name = '';
#[Validate('required|email:rfc,dns')]
public string $email = '';
#[Validate('accepted', message: 'Please agree to the processing of your data.')]
public bool $consent = false;
public function submit(): void
{
$data = $this->validate();
Contact::create($data);
$this->reset();
}
};Podmienené pravidlá
protected function rules(): array
{
return [
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($this->userId),
],
'password' => $this->userId ? 'nullable|min:8|confirmed' : 'required|min:8|confirmed',
];
}Form objekty
<?php
namespace App\Livewire\Forms;
use App\Models\Post;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post = null;
#[Validate('required|string|min:5|max:180')]
public string $title = '';
#[Validate('required|string|min:50')]
public string $body = '';
public function setPost(Post $post): void
{
$this->post = $post;
$this->title = $post->title;
$this->body = $post->body;
}
public function store(): Post
{
$this->validate();
return Post::create($this->except('post'));
}
public function update(): void
{
$this->validate();
$this->post->update($this->except('post'));
}
}<?php
use Livewire\Component;
use App\Livewire\Forms\PostForm;
new class extends Component {
public PostForm $form;
public function save()
{
$this->form->post ? $this->form->update() : $this->form->store();
$this->redirect(route('posts.index'), navigate: true);
}
};
?>
<div>
<form wire:submit="save">
<input wire:model="form.title">
@error('form.title') <span>{{ $message }}</span> @enderror
<textarea wire:model="form.body"></textarea>
@error('form.body') <span>{{ $message }}</span> @enderror
<button type="submit">Save</button>
</form>
</div>Chyby v JavaScripte
Štvrtá verzia sprístupňuje validačné chyby na klientovi cez magickú vlastnosť $errors.
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>10. Ostrovy (islands)
Ostrovy sú vlajkovou novinkou Livewire 4 a tou, ktorá mení uvažovanie o veľkosti komponentu. Ostrov je oblasť vnútri komponentu, ktorá sa prekresľuje nezávisle: pri aktualizácii sa prepočíta a zaplátkuje len tento fragment, nie celý komponent.
@island
<div>Revenue: {{ $this->revenue }}</div>
@endislandPrečo na tom záleží
Vo v3 bol štandardným liekom na „dashboard je pomalý“ rozpad na šesť podkomponentov, aby aktualizácia jedného widgetu nespúšťala znova dotazy ostatných piatich. Ostrovy dajú rovnakú izoláciu bez hranice komponentu: jeden komponent, jedna trieda, viacero nezávisle sa aktualizujúcich oblastí.
<?php
use Livewire\Component;
use Livewire\Attributes\Computed;
new class extends Component {
#[Computed]
public function revenue()
{
return Order::whereMonth('created_at', now()->month)->sum('total');
}
#[Computed]
public function queue()
{
return Job::pending()->count();
}
#[Computed]
public function feed()
{
return Activity::latest()->take(20)->get();
}
};
?>
<div>
@island(name: 'revenue')
<div class="card">
<h3>Revenue this month</h3>
<p class="stat">{{ number_format($this->revenue, 2) }} €</p>
</div>
@endisland
@island(name: 'queue', poll: '5s')
<div class="card">
<h3>Jobs pending</h3>
<p class="stat">{{ $this->queue }}</p>
</div>
@endisland
@island(name: 'feed', lazy: true)
@placeholder
<div class="card skeleton animate-pulse h-64"></div>
@endplaceholder
<div class="card">
@foreach ($this->feed as $activity)
<div wire:key="activity-{{ $activity->id }}">{{ $activity->summary }}</div>
@endforeach
</div>
@endisland
</div>Možnosti
| Možnosť | Účinok |
|---|---|
name | Identifikuje ostrov, aby naň mohli cieliť akcie a JavaScript. Viacero ostrovov s rovnakým menom sa vždy vykresľuje ako skupina. |
lazy | Vykreslí sa, keď ostrov príde do viditeľnej oblasti (intersection observer). |
defer | Vykreslí sa hneď po načítaní stránky bez ohľadu na viditeľnosť. |
always | Vynúti aktualizáciu ostrova pri každom prekreslení rodiča. |
skip | Úplne vynechá prvé vykreslenie. |
Zacielenie ostrova z akcie
@island(name: 'revenue')
Revenue: {{ $this->revenue }}
@endisland
<button wire:click="$refresh" wire:island="revenue">Refresh revenue</button>Append a prepend — nekonečný zoznam bez inštalatérstva
@island(name: 'feed')
@foreach ($this->posts as $post)
<article wire:key="post-{{ $post->id }}">{{ $post->title }}</article>
@endforeach
@endisland
<button wire:click="loadMore" wire:island.append="feed">Load more</button>To isté riadené z Alpine alebo čistého JavaScriptu:
<button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
Load more
</button>Polling v rámci ostrova
@island(name: 'queue')
<div wire:poll.3s>
Jobs pending: {{ $this->queue }}
</div>
@endisland
Polling obnovuje len ostrov. Porovnajte to s v3, kde wire:poll na dashboarde
spúšťal každých pár sekúnd znova všetky dotazy na stránke.
11. Sloty a preposielanie atribútov
Livewire komponenty teraz prijímajú obsah slotu tak, ako to Blade komponenty vždy vedeli — s jednou
dôležitou zvláštnosťou: obsah slotu sa vyhodnocuje v kontexte rodiča.
wire:click napísaný v slote volá metódu rodiča, nie potomka.
Predvolený slot
<livewire:modal>
<h2>Create a post</h2>
<form wire:submit="save">
<input wire:model="title">
<button type="submit">Save</button>
</form>
</livewire:modal><?php
use Livewire\Component;
new class extends Component {
public bool $isOpen = false;
public function toggle(): void
{
$this->isOpen = ! $this->isOpen;
}
};
?>
<div wire:show="isOpen" class="modal">
<button wire:click="toggle" class="modal__close">×</button>
<div class="modal__body">
{{ $slot }}
</div>
</div>
V tomto príklade wire:submit="save" a wire:model="title" patria
rodičovskému komponentu, zatiaľ čo wire:click="toggle" patrí modálu. Presne také rozdelenie
chcete: modál vlastní otváranie a zatváranie, rodič vlastní formulár.
Pomenované sloty
<livewire:modal>
<wire:slot name="header">
<h2>Create a post</h2>
</wire:slot>
<form wire:submit="save">
<input wire:model="title">
</form>
<wire:slot name="footer">
<button wire:click="save">Save</button>
<button wire:click="$parent.close()">Cancel</button>
</wire:slot>
</livewire:modal><div class="modal">
@if ($header = $slot('header'))
<div class="modal__header">{{ $header }}</div>
@endif
<div class="modal__body">{{ $slot }}</div>
@if ($footer = $slot('footer'))
<div class="modal__footer">{{ $footer }}</div>
@endif
</div>Preposielanie atribútov
Rovnako ako Blade komponenty môže Livewire komponent prijímať a zlučovať ľubovoľné HTML atribúty
cez $attributes.
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $type = 'info';
};
?>
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $slot }}
</div><livewire:alert type="danger" class="mt-6" data-testid="overdue-alert">
This invoice is 14 days overdue.
</livewire:alert>12. Vnorené komponenty a wire:key
Vnáranie funguje ako vo v3: každý potomok je nezávislá jednotka s vlastným stavom a vlastným cyklom aktualizácie.
<div>
@foreach ($orders as $order)
<livewire:order-row :order="$order" :key="'order-'.$order->id" />
@endforeach
</div>Inteligentné kľúče
Vo štvrtej verzii je smart_wire_keys zapnuté predvolene, takže Livewire odvodí kľúče sám
v mnohých situáciách s cyklami, kde predtým bol potrebný výslovný. Ubudne tým opakujúceho sa kódu,
no kľúče to nerobí zbytočnými.
wire:key naviazaný na id záznamu.
Inteligentné kľúče sú pohodlie pre jednoduché prípady, nie náhrada za to, aby ste frameworku povedali,
čo riadok v skutočnosti je.
@foreach ($rows as $row)
<div wire:key="row-{{ $row->id }}">…</div>
@endforeachReaktívne props
use Livewire\Attributes\Reactive;
new class extends Component {
#[Reactive]
public int $quantity;
};Volanie rodiča
<button wire:click="$parent.refreshList()">Refresh the list</button>Obojsmerná väzba na komponent
use Livewire\Attributes\Modelable;
new class extends Component {
#[Modelable]
public int $value = 0;
};<livewire:rating-input wire:model.live="review.rating" />13. Udalosti
API udalostí sa oproti tretej verzii nezmenilo.
// Odoslanie
$this->dispatch('post-created');
$this->dispatch('post-created', postId: $post->id, title: $post->title);
$this->dispatch('refresh')->to(OrderList::class);
$this->dispatch('recalculate')->self();use Livewire\Attributes\On;
new class extends Component {
#[On('post-created')]
public function onPostCreated(int $postId, string $title): void
{
// …
}
};<button wire:click="$dispatch('open-modal', { name: 'create-order' })">New order</button>Udalosti do prehliadača
$this->dispatch('notify', type: 'success', message: 'Order saved');<div x-data="{ show: false, message: '' }"
x-on:notify.window="message = $event.detail.message; show = true; setTimeout(() => show = false, 3000)"
x-show="show"
x-transition
class="toast">
<span x-text="message"></span>
</div>14. Vypočítané vlastnosti
#[Computed] funguje ako vo v3 a mimoriadne dobre sa kombinuje s ostrovmi: ostrov, ktorý
číta vypočítanú vlastnosť, ju prepočíta iba vtedy, keď sa aktualizuje práve tento ostrov.
use Livewire\Attributes\Computed;
new class extends Component {
public array $items = [];
#[Computed]
public function products()
{
return Product::whereIn('id', array_keys($this->items))->get();
}
#[Computed]
public function subtotal(): float
{
return $this->products->sum(
fn ($product) => $product->price * $this->items[$product->id]
);
}
#[Computed]
public function total(): float
{
return round($this->subtotal * 1.2, 2);
}
};@island(name: 'totals')
<p>Subtotal: {{ number_format($this->subtotal, 2) }} €</p>
<p>Total incl. VAT: {{ number_format($this->total, 2) }} €</p>
@endislandCachovanie nad rámec požiadavky
#[Computed(persist: true, seconds: 300)]
public function statistics(): array
{
return [
'orders' => Order::whereMonth('created_at', now()->month)->count(),
'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
];
}
#[Computed(cache: true, key: 'global-stats')]
public function globalStats(): array
{
return app(StatsService::class)->build();
}Vyprázdnenie cache
unset($this->products, $this->subtotal, $this->total);$this->products, nikdy nie $products.
15. Stav v URL a v relácii
use Livewire\Attributes\Url;
use Livewire\Attributes\Session;
new class extends Component {
#[Url]
public string $search = '';
#[Url(as: 'cat')]
public ?int $categoryId = null;
#[Url(except: '')]
public string $sort = 'popular';
#[Url(keep: true)]
public int $perPage = 24;
#[Url(history: true)]
public string $tab = 'all';
#[Session]
public bool $sidebarCollapsed = false;
#[Session(key: 'admin.table.density')]
public string $density = 'comfortable';
};Výsledok: /catalog?search=laptop&cat=5&sort=price-asc&tab=sale
Flash správy
public function save(): void
{
$this->validate();
Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);
session()->flash('status', 'Settings saved');
}@if (session('status'))
<div class="alert alert-success">{{ session('status') }}</div>
@endif#[Url] vlastnosť volá pri zmene History API. Pri poliach meniacich sa s každým
stlačením klávesy ju kombinujte s wire:model.live.debounce.
16. Nahrávanie súborov
Nahrávanie funguje ako vo verzii 3, cez trait WithFileUploads.
<?php
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
new class extends Component {
use WithFileUploads;
#[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')]
public $avatar;
public function save(): void
{
$this->validate();
$path = $this->avatar->store('avatars', 'public');
auth()->user()->update(['avatar_path' => $path]);
$this->reset('avatar');
$this->dispatch('notify', message: 'Avatar updated');
}
};
?>
<div>
<form wire:submit="save">
<input type="file" wire:model="avatar" accept="image/*">
<div wire:loading wire:target="avatar">Uploading…</div>
@if ($avatar)
<img src="{{ $avatar->temporaryUrl() }}" alt="Preview" class="preview">
@endif
@error('avatar') <p class="error">{{ $message }}</p> @enderror
<button type="submit" wire:loading.attr="disabled">Save</button>
</form>
</div>Presný indikátor priebehu
<div x-data="{ progress: 0, uploading: false }"
x-on:livewire-upload-start="uploading = true"
x-on:livewire-upload-finish="uploading = false; progress = 0"
x-on:livewire-upload-error="uploading = false"
x-on:livewire-upload-progress="progress = $event.detail.progress">
<input type="file" wire:model="avatar">
<div x-show="uploading" class="progress">
<div class="progress__bar" :style="`width: ${progress}%`"></div>
</div>
</div>'temporary_file_upload' => [
'disk' => 's3',
'rules' => ['file', 'max:12288'],
'directory' => 'livewire-tmp',
'middleware' => 'throttle:60,1',
'preview_mimes' => ['png', 'jpeg', 'jpg', 'webp', 'gif', 'mp4', 'pdf'],
'max_upload_time' => 5,
],upload_max_filesize, post_max_size v PHP a
client_max_body_size v nginxe musia prevyšovať vaše pravidlo max:, inak
nahrávanie zlyhá bez zrozumiteľnej chyby.
17. Stránkovanie
use Livewire\WithPagination;
use Livewire\Attributes\Url;
new class extends Component {
use WithPagination;
#[Url]
public string $search = '';
public string $sortField = 'created_at';
public string $sortDirection = 'desc';
public function updatedSearch(): void
{
$this->resetPage();
}
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function with(): array
{
return [
'orders' => Order::query()
->with('customer')
->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
->orderBy($this->sortField, $this->sortDirection)
->paginate(25),
];
}
};Ostrovy menia stránkovanie na nekonečný zoznam
<div>
<input type="search" wire:model.live.debounce.400ms="search">
@island(name: 'rows')
@foreach ($orders as $order)
<div wire:key="order-{{ $order->id }}">{{ $order->number }}</div>
@endforeach
@endisland
<button wire:click="nextPage" wire:island.append="rows">Load more</button>
</div>paginate() dotaz COUNT(*) navyše. Ak
nezobrazujete celkový počet strán — a pri postupnom dočítavaní ho zvyčajne nezobrazujete —,
simplePaginate() je citeľne lacnejší.
18. Stavy načítavania
Rodina wire:loading sa nezmenila a v4 pridáva na štýlovanie atribúty data-loading.
<div wire:loading>Loading…</div>
<div wire:loading.remove>Content</div>
<button wire:click="save">Save</button>
<span wire:loading wire:target="save">Saving…</span>
<span wire:loading wire:target="save,delete,publish">Working…</span>
<div wire:loading wire:target.except="search">Updating…</div>
<button wire:click="save" wire:loading.attr="disabled">Save</button>
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Save</button>
{{-- Indikátor sa zobrazí, iba ak požiadavka trvá dlhšie ako prah --}}
<div wire:loading.delay>Loading…</div>
<div wire:loading.delay.long>Loading…</div>Štýlovanie z CSS
[data-loading] .btn {
opacity: 0.5;
pointer-events: none;
}Neuložené zmeny a pripojenie
<input wire:model="title">
<span wire:dirty wire:target="title">You have unsaved changes</span>
<div wire:offline class="banner banner--warn">
No connection to the server. Changes are not being saved.
</div>19. Optimistické UI na klientovi
Celá trieda interakcií server nikdy nepotrebovala: otvoriť panel, ukázať počítadlo znakov, zafarbiť pole načerveno, keď je text príliš dlhý. Štvrtá verzia im dáva plnohodnotné direktívy, ktoré menia DOM okamžite, úplne bez požiadavky.
wire:show
{{-- Prepína viditeľnosť cez CSS, okamžite, bez round tripu --}}
<div wire:show="showModal" class="modal">…</div>
<button wire:click="$toggle('showModal')">Open</button>wire:text
{{-- Text nasleduje vlastnosť na klientovi --}}
<span wire:text="title"></span>
<input wire:model="title">wire:bind
<textarea wire:model="message" maxlength="280"></textarea>
<span wire:text="message.length"></span> / 280
<span wire:bind:class="message.length > 240 && 'text-red-500'">
Getting long
</span>Akcie iba na klientovi cez $js
<?php
new class extends Component {
public function save() { /* … */ }
};
?>
<div>
<button wire:click="save">Save</button>
{{-- Beží výlučne na klientovi --}}
<button x-on:click="$wire.$js.showToast = true">Show toast</button>
</div>wire:show / wire:text pre spätnú väzbu, ktorú má používateľ
pocítiť okamžite; bežnú akciu pre všetko, čo sa musí uložiť.
20. Drag and drop
Zmena poradia kedysi znamenala pridať SortableJS, napojiť jeho callbacky na akciu Livewire a udržiavať oba modely v synchronizácii. Vo verzii 4 je to direktíva.
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
{{ $task->title }}
</li>
@endforeach
</ul>new class extends Component {
public function reorder(array $order): void
{
foreach ($order as $position => $id) {
Task::where('id', $id)->update(['position' => $position]);
}
}
};Úchyty na ťahanie
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
<span wire:sort:handle class="cursor-grab">⠿</span>
<span>{{ $task->title }}</span>
{{-- Interaktívne prvky by nemali spúšťať ťahanie --}}
<button wire:sort:ignore wire:click="delete({{ $task->id }})">Delete</button>
</li>
@endforeach
</ul>Ťahanie medzi zoznamami
<div class="board">
@foreach ($columns as $column)
<ul wire:sort="moveCard" wire:sort:group="board" wire:key="column-{{ $column->id }}">
@foreach ($column->cards as $card)
<li wire:sort:item="{{ $card->id }}" wire:key="card-{{ $card->id }}">
{{ $card->title }}
</li>
@endforeach
</ul>
@endforeach
</div>21. Scoped CSS a skripty
Keďže komponent je teraz jeden súbor, patria doň aj jeho štýly a skripty. Livewire obmedzí CSS na komponent, takže nemôže presiaknuť do zvyšku stránky.
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $plan = 'starter';
};
?>
<div class="card">
<h3 class="title">{{ ucfirst($plan) }}</h3>
<p class="price">{{ $this->price }} € / month</p>
</div>
<style>
/* Scoped to this component — no global .title collision */
.card { border: 1px solid #e4e2da; border-radius: 14px; padding: 24px; }
.title { font-size: 20px; font-weight: 700; }
.price { color: #2d6a4f; }
</style>Zámerne globálne štýly
<style global>
:root { --brand: #f53004; }
</style>Skripty komponentu
@script
<script>
const chart = new Chart(document.getElementById('sales'), {
type: 'line',
data: @json($chartData),
});
$wire.on('data-updated', ({ points }) => {
chart.data.datasets[0].data = points;
chart.update();
});
</script>
@endscript{{-- Knižnicu tretej strany načítame raz pre celú stránku --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets23. Integrácia s JavaScriptom
Alpine.js sa stále dodáva s Livewire a $wire zostáva mostom z JavaScriptu do stavu
komponentu.
<div x-data="{ open: false }">
<button x-on:click="open = !open">Details</button>
<div x-show="open">
<p x-text="$wire.title"></p>
<button x-on:click="$wire.title = 'New title'">Rename</button>
<button x-on:click="$wire.save()">Save</button>
<button x-on:click="await $wire.calculate(); open = false">Calculate</button>
</div>
</div>{{-- Obojsmerná väzba do Alpine --}}
<div x-data="{ query: $wire.entangle('search') }">
<input x-model="query">
</div>wire:ref — pomenovanie prvkov a komponentov
<livewire:modal wire:ref="modal" />
<button x-on:click="$refs.modal.open()">Open the modal</button>#[Json] — vrátenie dát priamo do JavaScriptu
use Livewire\Attributes\Json;
new class extends Component {
#[Json]
public function searchSuggestions(string $term): array
{
return Product::search($term)->take(5)->pluck('name')->all();
}
};<input x-on:input.debounce.300ms="suggestions = await $wire.searchSuggestions($event.target.value)">Interceptory
Verzia 4 nahrádza hooky commit a request z v3 metódami
interceptMessage() a interceptRequest().
document.addEventListener('livewire:init', () => {
Livewire.interceptRequest(({ options, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload(); // CSRF token vypršal
}
});
});
Livewire.interceptMessage(({ component, succeed }) => {
succeed(() => {
console.debug('component updated', component.name);
});
});
});// Programový prístup
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());Ignorovanie podstromu
<div wire:ignore>
<select id="select2-field">…</select>
</div>
<div wire:ignore.self>…</div>24. Testovanie
Pest je odporúčaný spôsob testovania komponentov Livewire 4. Komponenty založené na šablóne môžu mať test hneď vedľa komponentu namiesto samostatného stromu.
php artisan make:livewire post.create --test
# resources/views/components/post/create.test.phpTestovanie komponentu založeného na šablóne
Na komponent sa odkazujte menom v bodkovej notácii, nie triedou:
use Livewire\Livewire;
it('renders', function () {
Livewire::test('post.create')
->assertStatus(200);
});
it('validates required fields', function () {
Livewire::test('post.create')
->set('title', '')
->set('body', 'too short')
->call('save')
->assertHasErrors(['title' => 'required']);
});
it('stores a valid post', function () {
Livewire::actingAs($user)
->test('post.create')
->set('title', 'A perfectly reasonable title')
->set('body', str_repeat('Body copy that is long enough. ', 3))
->call('save')
->assertHasNoErrors()
->assertDispatched('notify');
expect(Post::where('title', 'A perfectly reasonable title')->exists())->toBeTrue();
});Komponenty založené na triede
use App\Livewire\Counter;
it('increments', function () {
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1);
});Hlavné asercie
| Metóda | Overuje |
|---|---|
assertSet() / assertNotSet() | Hodnoty vlastností |
assertSee() / assertDontSee() | Vykreslený výstup |
assertViewHas() | Dáta odovzdané šablóne |
assertHasErrors() / assertHasNoErrors() | Validáciu |
assertDispatched() | Udalosti |
assertRedirect() | Presmerovania |
assertUnauthorized() / assertForbidden() | Autorizáciu |
assertStatus() | HTTP status |
// Nahrávanie súborov
use Illuminate\Http\UploadedFile;
Livewire::test('avatar-uploader')
->set('avatar', UploadedFile::fake()->image('avatar.jpg', 400, 400))
->call('save')
->assertHasNoErrors();
// Komponent vnútri stránky
$this->get('/dashboard')
->assertSeeLivewire('pages::dashboard');25. Bezpečnosť a výkon
Kontrolný zoznam bezpečnosti
$this->authorize()v každej akcii, ktorá mení dáta.- Všetky identifikátory označené
#[Locked]. - Pomocné metódy deklarované ako
protectedaleboprivate, aby ich klient nemohol volať. - Validačné pravidlá neobsahujú služobné polia (
user_id,role,price). - Žiadne tokeny, kľúče ani nadbytočné osobné údaje vo verejných vlastnostiach — snapshot ide do prehliadača v čitateľnej podobe.
- Nahrávanie súborov obmedzené podľa MIME typu a veľkosti.
- Prihlasovacie a kontaktné formuláre za rate limiterom.
{!! !!}iba pre dôveryhodné alebo sanitizované HTML.- Optimistické UI sa nikdy nepovažuje za smerodajné.
public function delete(int $postId): void
{
$post = Post::findOrFail($postId);
$this->authorize('delete', $post);
$post->delete();
}Výkon
| Príznak | Riešenie vo v4 |
|---|---|
| Aktualizácia widgetu spustí znova všetky dotazy stránky | Zabaliť oblasti do @island |
| Obrovské HTML stránky | Dostať kolekcie modelov z verejných vlastností; načítať ich v with() alebo #[Computed] |
| Požiadavka pri každom stlačení klávesy | wire:model.live.debounce.400ms |
| Záťaž z pollingu | Polling vnútri ostrova, pridať .visible |
| Round trip kvôli prepnutiu panelu | wire:show namiesto akcie |
| Inkrement počítadla prekresľuje stránku | #[Renderless] |
| Dotazy N+1 | Eager loading cez with(); Model::preventLazyLoading() pri vývoji |
| Pomalé prvé vykreslenie ťažkého bloku | @island(lazy: true) s @placeholder |
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}26. Časté chyby a ťahák
Komponent sa vôbec nevykreslí
Tag nie je samouzatvorený. Vo v4 <livewire:my-component> nevytvorí nič — a to
potichu. Píšte <livewire:my-component />.
V produkcii všetko vracia 404, lokálne funguje
Endpoint Livewire sa presunul z /livewire/ na /livewire-{hash}/. Pravidlo
firewallu, výnimka WAF, pravidlo CDN alebo location v nginxe, ktoré sú stále naviazané na starú
doslovnú cestu, zablokujú aktualizačné požiadavky.
Pole prestalo synchronizovať po upgrade
wire:model.blur a .change teraz riadia synchronizáciu na klientovi, nielen
sieťovú komunikáciu. Pridajte .live: wire:model.live.blur="title".
Obal prestal zachytávať svoje pole
Bublanie udalostí je predvolene vypnuté. Použite wire:model.deep.
Modifikátory wire:transition nič nerobia
.opacity, .scale a .duration.200ms boli odstránené — v4
používa natívne View Transitions API. Animáciu namiesto toho vyjadrite v CSS.
Ostrov sa nikdy neaktualizuje
- Ostrov nemá
name, ale akcia cieli na meno. wire:island="…"ukazuje na meno, ktoré vo vykreslenom výstupe neexistuje.- Oblasť závisí od stavu rodiča, ktorý sa reálne nezmenil — pridajte
always: true.
Obsah slotu volá nesprávnu metódu
Je to zámer: markup slotu sa vyhodnocuje v kontexte rodiča. Ak chcete volať potomka, použite
udalosť alebo wire:ref.
Komponenty Volt prestali fungovať
Volt sa stal súčasťou jadra. Nahraďte Livewire\Volt\Component za Livewire\Component,
Volt::route() za Route::livewire(), Volt::test() za
Livewire::test() a odstráňte service provider a balík Volt.
Ťahák direktív
| Direktíva | Účel | Nové vo v4 |
|---|---|---|
wire:model | Väzba poľa na vlastnosť | zmenená sémantika |
wire:model.deep | Zachytiť udalosti z potomkov | áno |
wire:click / wire:submit | Zavolať akciu | — |
wire:click.async | Vykonať akciu paralelne | áno |
wire:click.renderless | Preskočiť prekreslenie | áno |
wire:island | Zacieliť ostrov z akcie | áno |
wire:island.append | Pridať do ostrova namiesto nahradenia | áno |
wire:show | Prepnúť viditeľnosť na klientovi | áno |
wire:text | Naviazať text na klientovi | áno |
wire:bind | Reaktívne naviazať atribút | áno |
wire:sort | Zmena poradia cez drag and drop | áno |
wire:intersect | Akcia pri vstupe prvku do viewportu | áno |
wire:ref | Pomenovať prvok alebo komponent pre JS | áno |
wire:navigate | Navigácia v štýle SPA | — |
wire:navigate:scroll | Zachovať scroll kontajnera | premenované |
wire:loading / wire:dirty / wire:offline | Stav požiadavky | — |
wire:poll | Periodická aktualizácia | v rámci ostrova |
wire:key | Identita prvku v cykle | smart keys predvolene zapnuté |
wire:ignore | Vylúčiť podstrom z morphingu | — |
Blade direktívy
| Direktíva | Účel |
|---|---|
@island … @endisland | Nezávisle vykresľovaná oblasť |
@placeholder … @endplaceholder | Zástupný obsah pre lenivý ostrov |
<wire:slot name="…"> | Obsah pomenovaného slotu |
@script … @endscript | JavaScript v rámci komponentu |
@assets … @endassets | Assety stránky načítané raz |
@persist … @endpersist | Zachovať prvok naprieč navigáciami |
<style> / <style global> | Scoped / globálne CSS komponentu |
PHP atribúty
| Atribút | Účel |
|---|---|
#[Validate] | Validačné pravidlo |
#[Locked] | Zablokovať zápis z frontendu |
#[Computed] | Cachovaná odvodená hodnota |
#[Url] / #[Session] | Uchovanie stavu |
#[On] | Poslucháč udalosti |
#[Prop] | Deklarovať prop odovzdaný rodičom |
#[Reactive] / #[Modelable] | Väzba rodič – potomok |
#[Async] | Vykonať akciu paralelne |
#[Renderless] | Preskočiť prekreslenie |
#[Json] | Vrátiť dáta priamo do JavaScriptu |
#[Lazy] / #[Layout] / #[Title] | Načítavanie a rámec stránky |
Príkazy Artisan
php artisan make:livewire post.create # jednosúborový
php artisan make:livewire post.create --mfc # viacsúborový
php artisan make:livewire post.create --test # s testom
php artisan make:livewire pages::dashboard # s namespace
php artisan livewire:publish --config
php artisan optimize:clearOficiálne zdroje
- Dokumentácia Livewire: livewire.laravel.com/docs
- Dokumentácia k upgradu: livewire.laravel.com/docs/upgrading
- Dokumentácia Laravel: laravel.com/docs
- Alpine.js: alpinejs.dev
Ak sa táto dokumentácia a oficiálny zdroj líšia, pravdu má oficiálny zdroj. Našli ste chybu? Dajte nám vedieť.