Livewire 4 — kompletný sprievodca
Livewire 4 si zachováva známy model komponentu a prestavuje všetko okolo neho: jednosúborové komponenty, ostrovy s nezávislým vykresľovaním, sloty v kontexte rodiča, vstavaný drag and drop a klientské direktívy, ktoré vôbec nechodia na server. Sprievodca pokrýva framework celý, s výslovným označením rozdielov štvrtej verzie.
1. Čo mení Livewire 4
Livewire 4 je najväčšie vydanie v histórii frameworku. Samotný model komponentu sa nezmenil: PHP trieda, Blade šablóna, stav na serveri, HTML po drôte. Zmenilo sa všetko okolo: 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 a značkovanie v jednom .blade.php. Už žiadne skákanie medzi dvoma priečinkami kvôli komponentu na dvadsať riadkov. |
| Ostrovy (islands) | Izolované oblasti vnútri komponentu, ktoré sa prekresľujú samostatne. Počítadlo tržieb už nespúšťa znova dotazy celého dashboardu. |
| Sloty | Rodič vkladá značkovanie do potomka a to sa vyhodnocuje v kontexte rodiča: wire:click v slote volá metódu rodiča. |
| Optimistické UI | wire:show, wire:text, wire:bind menia DOM okamžite, bez volania servera. |
| Drag and drop | wire:sort je vstavaný. Ž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álny upgrade guide zostáva primárnym zdrojom.
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 --config| Nastavenie | Účel |
|---|---|
component_locations | Priečinky prehľadávané kvôli komponentom. Predvolene resources/views/components a resources/views/livewire. |
component_namespaces | Pomenované korene, napríklad pages::. |
component_layout | V3 to bolo layout. Používa namespace layouts::. |
component_placeholder | V3 to bolo lazy_placeholder. |
make_command | Čo generuje make:livewire: jednosúborový alebo triedny komponent. |
smart_wire_keys | Teraz predvolene true. |
csp_safe | Build kompatibilný s prísnejšou CSP. |
// 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: 'Príspevok bol vytvorený');
}
};
?>
<div>
<form wire:submit="save">
<input type="text" wire:model="title" placeholder="Nadpis">
@error('title') <span class="error">{{ $message }}</span> @enderror
<textarea wire:model="body" placeholder="Text"></textarea>
@error('body') <span class="error">{{ $message }}</span> @enderror
<button type="submit">Uložiť</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úť.
Viacsúborové komponenty
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');
}
}4. Umiestnenie, pomenovanie a namespace
resources/views/
├── components/
│ ├── ⚡counter.blade.php → <livewire:counter />
│ ├── post/
│ │ ├── ⚡create.blade.php → <livewire:post.create />
│ │ └── ⚡index.blade.php → <livewire:post.index />
│ └── button.blade.php (bežný Blade komponent)
└── pages/
└── ⚡dashboard.blade.php → <livewire:pages::dashboard />{{-- Podľa mena --}}
<livewire:counter />
{{-- Vnorené priečinky bodkou --}}
<livewire:post.create />
{{-- S namespace --}}
<livewire:pages::post.create />
{{-- S parametrami --}}
<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
use App\Livewire\Dashboard;
// Podľa triedy
Route::livewire('/dashboard', Dashboard::class);
// Podľa mena komponentu
Route::livewire('/dashboard', 'pages::dashboard')
->middleware('auth')
->name('dashboard');Layout
'component_layout' => 'layouts::app',use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
new class extends Component {
#[Layout('layouts::app')]
#[Title('Ovládací panel')]
public function render() { /* … */ }
};5. Vlastnosti a props
new class extends Component {
public string $name = '';
public ?int $age = null;
public array $tags = [];
public bool $isPublic = false;
};Props od rodiča: #[Prop]
<?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">
Faktúra je po splatnosti.
</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ý
use Livewire\Attributes\Locked;
new class extends Component {
#[Locked]
public int $invoiceId; // klient nepodstrčí cudzie id
public string $note = '';
};6. wire:model vo štvrtej verzii
wire:model sa správajú inak a oba zlyhávajú ticho, bez chyby.
Zmena 1: modifikátory riadia synchronizáciu na klientovi
{{-- 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
{{-- 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
<button wire:click="save">Uložiť</button>
<form wire:submit="save">…</form>
<button wire:click="delete({{ $post->id }})"
wire:confirm="Zmazať príspevok? Akciu nemožno vrátiť.">Zmazať</button>Akcie bez prekreslenia
use Livewire\Attributes\Renderless;
new class extends Component {
#[Renderless]
public function incrementViewCount(): void
{
$this->post->increment('views');
}
};<button type="button" wire:click.renderless="incrementViewCount">Započítať</button>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')">Začať</button><button wire:click="$refresh">Obnoviť</button>
<button wire:click="$set('tab', 'settings')">Nastavenia</button>
<button wire:click="$toggle('showFilters')">Filtre</button>
<button wire:click="$parent.closeModal()">Zavrieť</button>8. Životný cyklus a hooky
1. Vytvorí sa inštancia komponentu
2. boot()
3. mount($params)
4. booted()
5. render() 1. Príde POST /livewire-{hash}/update so snapshotom stavu
2. Overí sa kontrolný súčet snapshotu
3. Vytvorí sa inštancia komponentu
4. boot()
5. Stav sa obnoví zo snapshotu
6. hydrate() / hydrateFoo()
7. booted()
8. updating($prop, $value) / updatingFoo($value)
9. Vlastnosti dostanú nové hodnoty
10. updated($prop, $value) / updatedFoo($value)
11. Vykonajú sa akcie z frontu
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — stav sa zabalí späť do snapshotu
14. Odpoveď: značkovanie + 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 updated(string $property, mixed $value): void
{
$this->validateOnly($property);
}
public function updatedPrice(mixed $value): void
{
$this->price = round((float) $value, 2);
}
};9. Validácia a Form objekty
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: 'Je potrebný súhlas so spracovaním údajov.')]
public bool $consent = false;
public function submit(): void
{
$data = $this->validate();
Contact::create($data);
$this->reset();
}
};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 store(): Post
{
$this->validate();
return Post::create($this->except('post'));
}
public function update(): void
{
$this->validate();
$this->post->update($this->except('post'));
}
}Chyby v JavaScripte
<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>Tržby: {{ $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();
}
};
?>
<div>
@island(name: 'revenue')
<div class="card">
<h3>Tržby za mesiac</h3>
<p class="stat">{{ number_format($this->revenue, 2, ',', ' ') }} €</p>
</div>
@endisland
@island(name: 'queue', poll: '5s')
<div class="card">
<h3>Úloh vo fronte</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">…</div>
@endisland
</div>Možnosti
| Možnosť | Účinok |
|---|---|
name | Meno ostrova, aby ho mohli zacieliť akcie a JavaScript. Viac ostrovov s rovnakým menom sa vždy vykresľuje ako skupina. |
lazy | Vykreslí sa, keď sa dostane do viewportu. |
defer | Vykreslí sa hneď po načítaní stránky, bez ohľadu na viditeľnosť. |
always | Vynúti aktualizáciu pri každom vykreslení rodiča. |
skip | Preskočí prvotné vykreslenie. |
Zacielenie ostrova z akcie
@island(name: 'revenue')
Tržby: {{ $this->revenue }}
@endisland
<button wire:click="$refresh" wire:island="revenue">Obnoviť tržby</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">Načítať ďalšie</button><button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
Načítať ďalšie
</button>Polling v rámci ostrova
@island(name: 'queue')
<div wire:poll.3s>
Úloh vo fronte: {{ $this->queue }}
</div>
@endisland11. 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>Vytvoriť príspevok</h2>
<form wire:submit="save">
<input wire:model="title">
<button type="submit">Uložiť</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>Pomenované sloty
<livewire:modal>
<wire:slot name="header">
<h2>Vytvoriť príspevok</h2>
</wire:slot>
<form wire:submit="save">
<input wire:model="title">
</form>
<wire:slot name="footer">
<button wire:click="save">Uložiť</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
<?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">
Faktúra je 14 dní po splatnosti.
</livewire:alert>12. Vnorené komponenty a wire:key
<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ý.
wire:key naviazaný na id záznamu.
@foreach ($rows as $row)
<div wire:key="row-{{ $row->id }}">…</div>
@endforeachuse Livewire\Attributes\Reactive;
new class extends Component {
#[Reactive]
public int $quantity;
};<button wire:click="$parent.refreshList()">Obnoviť zoznam</button>use Livewire\Attributes\Modelable;
new class extends Component {
#[Modelable]
public int $value = 0;
};13. Udalosti
API udalostí sa oproti tretej verzii nezmenilo.
$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
{
// …
}
};$this->dispatch('notify', type: 'success', message: 'Objednávka bola uložená');<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
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]
);
}
};@island(name: 'totals')
<p>Medzisúčet: {{ number_format($this->subtotal, 2, ',', ' ') }} €</p>
@endisland#[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'),
];
}$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;
#[Session]
public bool $sidebarCollapsed = false;
};public function save(): void
{
$this->validate();
session()->flash('status', 'Nastavenia boli uložené');
}#[Url] vlastnosť volá pri zmene History API. Pri poliach meniacich sa s každým
stlačením klávesy kombinujte s wire:model.live.debounce.
16. Nahrávanie súborov
<?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');
}
};
?>
<div>
<form wire:submit="save">
<input type="file" wire:model="avatar" accept="image/*">
<div wire:loading wire:target="avatar">Nahrávame…</div>
@if ($avatar)
<img src="{{ $avatar->temporaryUrl() }}" alt="Náhľad" class="preview">
@endif
@error('avatar') <p class="error">{{ $message }}</p> @enderror
<button type="submit" wire:loading.attr="disabled">Uložiť</button>
</form>
</div><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-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>upload_max_filesize, post_max_size v PHP a
client_max_body_size v nginxe musia prevyšovať vaše pravidlo max:.
17. Stránkovanie
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $search = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function with(): array
{
return [
'orders' => Order::query()
->with('customer')
->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
->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">Načítať ďalšie</button>
</div>paginate() dotaz COUNT(*) navyše.
simplePaginate() je citeľne lacnejší.
18. Stavy načítavania
<div wire:loading>Načítava sa…</div>
<div wire:loading.remove>Obsah</div>
<button wire:click="save">Uložiť</button>
<span wire:loading wire:target="save">Ukladáme…</span>
<button wire:click="save" wire:loading.attr="disabled">Uložiť</button>
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Uložiť</button>
<div wire:loading.delay>Načítava sa…</div>
<div wire:loading.delay.long>Načítava sa…</div>Štýlovanie z CSS
[data-loading] .btn {
opacity: 0.5;
pointer-events: none;
}<input wire:model="title">
<span wire:dirty wire:target="title">Máte neuložené zmeny</span>
<div wire:offline class="banner banner--warn">
Nie ste pripojení k serveru.
</div>19. Optimistické UI na klientovi
Celá trieda interakcií server nikdy nepotrebovala: otvoriť panel, ukázať počítadlo znakov, zafarbiť pole načerveno. Štvrtá verzia im dáva plnohodnotné direktívy, ktoré menia DOM okamžite, úplne bez požiadavky.
{{-- Prepína viditeľnosť cez CSS, okamžite --}}
<div wire:show="showModal" class="modal">…</div>
<button wire:click="$toggle('showModal')">Otvoriť</button>{{-- Text nasleduje vlastnosť na klientovi --}}
<span wire:text="title"></span>
<input wire:model="title"><textarea wire:model="message" maxlength="280"></textarea>
<span wire:text="message.length"></span> / 280
<span wire:bind:class="message.length > 240 && 'text-red-500'">
Začína byť dlhé
</span>20. Drag and drop
<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>
<button wire:sort:ignore wire:click="delete({{ $task->id }})">Zmazať</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
<?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 }} € / mesiac</p>
</div>
<style>
/* Scoped na tento komponent — žiadna globálna kolízia na .title */
.card { border: 1px solid #e4e2da; border-radius: 14px; padding: 24px; }
.title { font-size: 20px; font-weight: 700; }
.price { color: #2d6a4f; }
</style><style global>
:root { --brand: #f53004; }
</style>@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>
@endscript23. Integrácia s JavaScriptom
<div x-data="{ open: false }">
<button x-on:click="open = !open">Detaily</button>
<div x-show="open">
<p x-text="$wire.title"></p>
<button x-on:click="$wire.save()">Uložiť</button>
<button x-on:click="await $wire.calculate(); open = false">Prepočítať</button>
</div>
</div>wire:ref
<livewire:modal wire:ref="modal" />
<button x-on:click="$refs.modal.open()">Otvoriť modal</button>#[Json]
use Livewire\Attributes\Json;
new class extends Component {
#[Json]
public function searchSuggestions(string $term): array
{
return Product::search($term)->take(5)->pluck('name')->all();
}
};Interceptory
document.addEventListener('livewire:init', () => {
Livewire.interceptRequest(({ options, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload();
}
});
});
Livewire.interceptMessage(({ component, succeed }) => {
succeed(() => {
console.debug('komponent aktualizovaný', component.name);
});
});
});<div wire:ignore>
<select id="select2-field">…</select>
</div>24. Testovanie
Pest je odporúčaný spôsob testovania komponentov Livewire 4.
php artisan make:livewire post.create --test
# resources/views/components/post/create.test.phpuse Livewire\Livewire;
it('sa vykreslí', function () {
Livewire::test('post.create')
->assertStatus(200);
});
it('validuje povinné polia', function () {
Livewire::test('post.create')
->set('title', '')
->call('save')
->assertHasErrors(['title' => 'required']);
});
it('uloží správny príspevok', function () {
Livewire::actingAs($user)
->test('post.create')
->set('title', 'Celkom rozumný nadpis')
->set('body', str_repeat('Dostatočne dlhý text príspevku. ', 3))
->call('save')
->assertHasNoErrors()
->assertDispatched('notify');
});| Metóda | Overuje |
|---|---|
assertSet() / assertNotSet() | Hodnoty vlastností |
assertSee() / assertDontSee() | Výstup |
assertHasErrors() / assertHasNoErrors() | Validáciu |
assertDispatched() | Udalosti |
assertRedirect() | Presmerovania |
assertForbidden() | Autorizáciu |
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. - Validačné pravidlá neobsahujú služobné polia (
user_id,role,price). - Žiadne tokeny ani kľúče 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 formuláre za rate limiterom.
- Optimistické UI sa nikdy nepovažuje za smerodajné.
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í |
| Požiadavka pri každom stlačení klávesy | wire:model.live.debounce.400ms |
| Záťaž z pollingu | Polling vnútri ostrova plus .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() |
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}26. Časté chyby a ťahák
Komponent sa vôbec nevykreslí
Tag nie je samouzatvorený. Píšte <livewire:my-component />.
V produkcii všetko 404, lokálne funguje
Endpoint Livewire sa presunul z /livewire/ na /livewire-{hash}/. Pravidlo
firewallu, výnimka WAF alebo location v nginxe blokujú aktualizačné požiadavky.
Pole prestalo synchronizovať po upgrade
Pridajte .live: wire:model.live.blur="title".
Obal prestal zachytávať svoje pole
Použite wire:model.deep.
Modifikátory wire:transition nič nerobia
Boli odstránené — v4 používa natívne View Transitions API. Animáciu vyjadrite v CSS.
Ostrov sa neaktualizuje
- Ostrov nemá
name, ale akcia cieli na meno. wire:island="…"ukazuje na meno, ktoré vo výstupe neexistuje.- Oblasť závisí od stavu rodiča, ktorý sa reálne nezmenil — pridajte
always: true.
Komponenty Volt prestali fungovať
Volt sa stal súčasťou jadra. Nahraďte Livewire\Volt\Component za
Livewire\Component, Volt::route() za Route::livewire(),
odstráňte service provider a balík.
Ť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.async | Vykonať akciu paralelne | áno |
wire:click.renderless | Preskočiť prekreslenie | áno |
wire:island | Zacieliť ostrov | áno |
wire:island.append | Pridať do ostrova | á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 | Drag and drop | áno |
wire:intersect | Akcia pri vstupe do viewportu | áno |
wire:ref | Meno prvku pre JS | áno |
wire:navigate:scroll | Zachovať scroll kontajnera | premenované |
wire:poll | Periodická aktualizácia | v rámci ostrova |
wire:key | Identita prvku v cykle | smart keys predvolene |
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 |
<style> / <style global> | Scoped / globálne CSS |
PHP atribúty
| Atribút | Účel |
|---|---|
#[Validate] / #[Locked] | Validácia a ochrana pred zápisom |
#[Computed] | Cachovaná odvodená hodnota |
#[Url] / #[Session] | Uchovanie stavu |
#[On] / #[Prop] | Udalosti a props |
#[Async] / #[Renderless] | Paralelnosť a preskočenie renderu |
#[Json] | Dáta priamo do JavaScriptu |
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 livewire:publish --config
php artisan optimize:clearOficiálne zdroje
- Dokumentácia Livewire: livewire.laravel.com/docs
- Upgrade guide: livewire.laravel.com/docs/upgrading
- Alpine.js: alpinejs.dev