Livewire
Com crear components reactius amb Laravel Livewire: propietats, accions, formularis, validació, events i navegació SPA sense escriure JavaScript.
Què és Livewire?#
Laravel Livewire és un framework full-stack que permet construir interfícies reactives i dinàmiques utilitzant exclusivament PHP. La seva filosofia central és clara: no hauries de necessitar escriure JavaScript per a la majoria d'interaccions habituals d'una aplicació web. Cerques en temps real, formularis amb validació instantània, modals, paginació dinàmica, ordenació de taules: tot això es pot aconseguir escrivint únicament classes PHP i plantilles Blade, sense tocar una sola línia de JavaScript.
Livewire funciona d'una manera elegant sota la superfície. Quan el servidor renderitza un component Livewire per primera vegada, genera l'HTML corresponent i l'envia al navegador com qualsevol pàgina Blade tradicional. A partir d'aquí, el JavaScript intern de Livewire pren el control: intercepta les interaccions de l'usuari (clics, escriptura de text, enviament de formularis), envia peticions AJAX al servidor amb les dades de la interacció, el servidor re-renderitza el component amb l'estat actualitzat, calcula les diferències entre l'HTML anterior i el nou, i finalment envia només els canvis al navegador per actualitzar el DOM. Tot aquest procés és completament transparent per al desenvolupador: tu escrius PHP i Blade, i Livewire s'encarrega de la resta.
Livewire és especialment adequat per a aplicacions CRUD, panells d'administració, dashboards, formularis complexos i qualsevol interfície on la interactivitat no requereixi una latència extremadament baixa. Si estàs construint un editor de gràfics en temps real o un joc, potser necessites un framework JavaScript complet com Vue o React amb Inertia. Però per a la gran majoria d'aplicacions web empresarials, Livewire ofereix tota la reactivitat necessària amb la simplicitat de treballar íntegrament amb l'ecosistema Laravel.
La versió actual és Livewire 3, que va representar una reescriptura significativa respecte a Livewire 2. Entre els canvis més importants hi ha la integració nativa amb Alpine.js (que ara s'inclou automàticament), l'ús d'atributs PHP 8 en lloc de propietats per a configuracions com la validació o l'URL binding, els objectes Form per a formularis, la navegació SPA amb wire:navigate, i un sistema de renderització molt més eficient. Si treballes amb un projecte antic que utilitza Livewire 2, la migració a la versió 3 requereix adaptar la sintaxi de wire:model (que ara és lazy per defecte) i substituir els listeners per l'atribut #[On], entre d'altres canvis.
Instal·lació#
Per instal·lar Livewire al teu projecte Laravel, només necessites afegir el paquet via Composer. Livewire s'integra directament amb l'ecosistema Laravel i no requereix cap configuració addicional de bundlers JavaScript ni passos de compilació:
composer require livewire/livewireUn cop instal·lat, pots publicar opcionalment el fitxer de configuració si necessites personalitzar el comportament per defecte de Livewire. Això crea un fitxer config/livewire.php on pots canviar coses com el directori dels components, el prefix de les rutes, o si vols que Livewire injecti automàticament els seus assets:
php artisan livewire:publish --configLivewire necessita incloure els seus fitxers CSS i JavaScript a les pàgines on s'utilitza. A Livewire 3, la injecció d'assets és automàtica per defecte: Livewire detecta quan un component es renderitza i afegeix automàticament els scripts i estils necessaris al HTML de la resposta. Això significa que normalment no necessites afegir res al teu layout.
Si prefereixes controlar manualment on s'inclouen els assets, pots desactivar la injecció automàtica al fitxer de configuració i utilitzar les directives Blade corresponents al teu layout:
<html>
<head>
@livewireStyles
</head>
<body>
{{ $slot }}
@livewireScripts
</body>
</html>Quan utilitzes components de pàgina completa amb l'atribut #[Layout], Livewire gestiona tot el layout i la injecció d'assets automàticament, cosa que simplifica encara més la configuració. En la majoria de projectes nous amb Livewire 3, no necessitaràs preocupar-te per la injecció d'assets: simplement instal·la el paquet i comença a crear components.
Crear components#
La manera més ràpida de crear un component Livewire és utilitzar la comanda Artisan corresponent. Aquesta comanda genera dos fitxers: una classe PHP que conté la lògica del component i una vista Blade que conté el HTML:
php artisan make:livewire SearchArticlesAixò crea dos fitxers al teu projecte. La classe del component és una classe PHP que estén Livewire\Component i conté totes les propietats i mètodes que defineixen el comportament del component. La vista Blade és una plantilla convencional on pots utilitzar totes les directives Blade habituals, a més de les directives específiques de Livewire:
// app/Livewire/SearchArticles.php
namespace App\Livewire;
use App\Models\Article;
use Livewire\Component;
class SearchArticles extends Component
{
public string $search = '';
public function render()
{
$articles = Article::where('title', 'like', "%{$this->search}%")
->published()
->limit(10)
->get();
return view('livewire.search-articles', [
'articles' => $articles,
]);
}
}{{-- resources/views/livewire/search-articles.blade.php --}}
<div>
<input wire:model.live="search" type="text" placeholder="Cercar articles...">
<ul>
@foreach($articles as $article)
<li>{{ $article->title }}</li>
@endforeach
</ul>
</div>Un detall important: la vista d'un component Livewire ha de tenir un únic element arrel. Tot el contingut del component ha d'estar embolcallat dins d'un sol element HTML (normalment un <div>). Això és necessari perquè Livewire pugui rastrejar i actualitzar el DOM del component correctament.
Per incloure un component Livewire dins d'una vista Blade, utilitza la sintaxi de components Blade amb el prefix livewire::
<livewire:search-articles />Si prefereixes un component més senzill que no necessiti un fitxer de vista separat, pots crear un component inline. En aquest cas, el mètode render() retorna directament una cadena de text amb el HTML del component, sense necessitat d'un fitxer Blade:
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public int $count = 0;
public function increment()
{
$this->count++;
}
public function render()
{
return <<<'BLADE'
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+1</button>
</div>
BLADE;
}
}Per crear un component inline directament amb Artisan, afegeix l'opció --inline:
php artisan make:livewire Counter --inlineLivewire també suporta components de pàgina completa, que substitueixen completament la vista d'una ruta. En lloc de renderitzar el component dins d'un layout Blade existent, el component de pàgina completa actua com la pàgina sencera. Això és especialment útil per a panells d'administració o qualsevol interfície on Livewire gestioni tota la pàgina. Utilitza l'atribut #[Layout] per indicar quin layout ha d'utilitzar:
namespace App\Livewire;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class Dashboard extends Component
{
public function render()
{
return view('livewire.dashboard');
}
}Després, registra la ruta directament al component de Livewire:
// routes/web.php
use App\Livewire\Dashboard;
Route::get('/dashboard', Dashboard::class);Pel que fa a les convencions de directori, pots organitzar els components en subdirectoris per mantenir el projecte net. Per exemple, php artisan make:livewire Admin/UserTable crea el component a app/Livewire/Admin/UserTable.php i la vista a resources/views/livewire/admin/user-table.blade.php. A la vista, l'inclouries com <livewire:admin.user-table /> utilitzant punts per separar els directoris.
Propietats#
Les propietats públiques d'un component Livewire són el mecanisme principal per gestionar l'estat. Qualsevol propietat declarada com a public a la classe del component està automàticament disponible a la vista Blade, sense necessitat de passar-la explícitament al mètode render(). A més, les propietats públiques es serialitzen i deserialitzen automàticament entre les peticions del servidor, mantenint l'estat del component entre interaccions de l'usuari.
class EditProfile extends Component
{
public string $name = '';
public string $email = '';
public string $bio = '';
public function render()
{
// $name, $email i $bio estan disponibles a la vista automàticament
return view('livewire.edit-profile');
}
}El vinculament de dades entre la vista i les propietats del component es fa amb la directiva wire:model. Aquesta directiva connecta un camp d'entrada amb una propietat pública, de manera que quan l'usuari modifica el valor del camp, la propietat s'actualitza al servidor. A Livewire 3, wire:model és lazy per defecte: la propietat només s'actualitza quan l'usuari surt del camp (event change). Això és diferent de Livewire 2, on wire:model era live per defecte.
Pots modificar el comportament de wire:model amb diversos modificadors segons les teves necessitats:
{{-- Lazy (per defecte): s'actualitza en perdre el focus --}}
<input wire:model="name" type="text">
{{-- Live: s'actualitza a cada teclejat --}}
<input wire:model.live="search" type="text">
{{-- Blur: s'actualitza explícitament en perdre el focus --}}
<input wire:model.blur="email" type="email">
{{-- Live amb debounce: espera 500ms després de l'última teclejada --}}
<input wire:model.live.debounce.500ms="search" type="text">
{{-- Throttle: com a màxim una actualització cada 1 segon --}}
<input wire:model.live.throttle.1s="query" type="text">El modificador live és ideal per a cerques en temps real on vols que els resultats s'actualitzin mentre l'usuari escriu. Combinat amb debounce, pots controlar la freqüència de les peticions al servidor per evitar sobrecàrrega: un debounce de 300ms significa que la petició només s'envia si l'usuari deixa d'escriure durant 300 mil·lisegons. El modificador blur és útil per a camps de formulari on la validació en temps real és important però no necessites actualitzar a cada teclejat, com un camp d'email que es valida quan l'usuari passa al següent camp. El modificador throttle garanteix que les peticions al servidor no superin una freqüència determinada, independentment de la velocitat d'escriptura de l'usuari.
Les propietats computades permeten definir valors derivats que es calculen sota demanda i es guarden en memòria cau durant la vida de la petició. S'utilitza l'atribut #[Computed] sobre un mètode, i després s'hi accedeix com si fos una propietat a la vista amb $this:
use Livewire\Attributes\Computed;
class UserList extends Component
{
public string $search = '';
public string $role = '';
#[Computed]
public function users()
{
return User::query()
->when($this->search, fn ($q) => $q->where('name', 'like', "%{$this->search}%"))
->when($this->role, fn ($q) => $q->where('role', $this->role))
->paginate(20);
}
public function render()
{
return view('livewire.user-list');
}
}<div>
@foreach($this->users as $user)
<p>{{ $user->name }}</p>
@endforeach
{{ $this->users->links() }}
</div>Fixa't que a la vista s'accedeix a la propietat computada amb $this->users (amb $this), no pas amb $users directament. Això és perquè les propietats computades no són propietats públiques reals, sinó mètodes que es comporten com a propietats. El valor es calcula la primera vegada que s'hi accedeix durant una petició i es guarda en memòria cau: si accedeixes a $this->users diverses vegades dins de la mateixa petició, la consulta a la base de dades només s'executa una vegada.
Hi ha algunes restriccions importants sobre el tipus de dades que poden ser propietats públiques de Livewire. Com que les propietats es serialitzen a cada petició (es converteixen a JSON i es tornen a convertir a PHP), han de ser serialitzables: cadenes, números, booleans, arrays, col·leccions Eloquent, models Eloquent, enums PHP, objectes DateTime i Carbon. No pots utilitzar closures, recursos, connexions de base de dades ni altres objectes no serialitzables com a propietats públiques.
Per protegir propietats sensibles que no s'haurien de poder modificar des del client, utilitza l'atribut #[Locked]. Una propietat bloquejada pot ser llegida a la vista però qualsevol intent de modificar-la des del frontend llançarà una excepció:
use Livewire\Attributes\Locked;
class OrderDetail extends Component
{
#[Locked]
public int $orderId;
public function mount(int $orderId)
{
$this->orderId = $orderId;
}
}Això és especialment important per a propietats com IDs, preus o qualsevol valor que determini la seguretat de l'operació. Sense #[Locked], un usuari malintencionat podria manipular les peticions AJAX per canviar el valor d'una propietat pública.
Una altra funcionalitat molt útil és el vinculament de propietats amb la URL. L'atribut #[Url] sincronitza una propietat amb els paràmetres de la query string, de manera que l'estat del component es reflecteix a la URL i es pot compartir o afegir als marcadors del navegador:
use Livewire\Attributes\Url;
class ArticleList extends Component
{
#[Url]
public string $search = '';
#[Url(as: 'cat')]
public string $category = '';
#[Url(history: true)]
public int $page = 1;
}Amb aquesta configuració, quan l'usuari escriu "laravel" al camp de cerca, la URL es transforma en /articles?search=laravel. L'opció as permet canviar el nom del paràmetre a la URL (en lloc de category apareixerà cat). L'opció history: true fa que cada canvi de valor afegeixi una entrada a l'historial del navegador, permetent que l'usuari navegui enrere i endavant amb els botons del navegador.
Accions#
Les accions són mètodes públics del component que es poden cridar directament des de la vista Blade com a resposta a events del DOM. Quan un usuari fa clic a un botó, envia un formulari o prem una tecla, Livewire intercepta l'event, envia una petició AJAX al servidor, executa el mètode corresponent i actualitza la vista amb el nou estat del component.
La directiva més comuna és wire:click, que crida un mètode quan l'usuari fa clic sobre un element. Però Livewire suporta pràcticament qualsevol event del DOM:
class TaskManager extends Component
{
public array $tasks = [];
public string $newTask = '';
public function addTask()
{
if (empty($this->newTask)) {
return;
}
$this->tasks[] = [
'text' => $this->newTask,
'done' => false,
];
$this->newTask = '';
}
public function toggleTask(int $index)
{
$this->tasks[$index]['done'] = !$this->tasks[$index]['done'];
}
public function removeTask(int $index)
{
array_splice($this->tasks, $index, 1);
}
public function render()
{
return view('livewire.task-manager');
}
}<div>
<form wire:submit="addTask">
<input wire:model="newTask" type="text" placeholder="Nova tasca...">
<button type="submit">Afegir</button>
</form>
<ul>
@foreach($tasks as $index => $task)
<li>
<input wire:click="toggleTask({{ $index }})"
type="checkbox"
{{ $task['done'] ? 'checked' : '' }}>
<span class="{{ $task['done'] ? 'line-through' : '' }}">
{{ $task['text'] }}
</span>
<button wire:click="removeTask({{ $index }})">Eliminar</button>
</li>
@endforeach
</ul>
</div>Com pots veure, els mètodes poden rebre paràmetres directament des de la vista. Això és molt útil per identificar quin element concret ha activat l'acció, com l'índex d'una llista o l'ID d'un registre de la base de dades.
Livewire també inclou unes "accions màgiques" que permeten manipular l'estat del component directament des de la vista sense necessitat de definir mètodes dedicats a la classe PHP. Aquestes accions són convenients per a operacions senzilles:
{{-- Refresca tot el component --}}
<button wire:click="$refresh">Refrescar</button>
{{-- Assigna un valor a una propietat --}}
<button wire:click="$set('status', 'active')">Activar</button>
{{-- Alterna un booleà --}}
<button wire:click="$toggle('showDetails')">Mostra/Amaga detalls</button>Per a accions que requereixen una confirmació de l'usuari abans d'executar-se, Livewire permet afegir diàlegs de confirmació natius del navegador. Això és particularment útil per a operacions destructives com eliminar registres:
<button wire:click="deleteArticle({{ $article->id }})"
wire:confirm="Estàs segur que vols eliminar aquest article?">
Eliminar
</button>Quan treballes amb events de teclat, pots especificar tecles concretes per filtrar quan s'ha d'executar l'acció. Això et permet crear interaccions de teclat precises sense necessitat de gestionar events JavaScript manualment:
{{-- Només quan es prem Enter --}}
<input wire:keydown.enter="search" type="text">
{{-- Quan es prem Escape --}}
<div wire:keydown.escape="closeModal">
{{-- Combinació de tecles --}}
<input wire:keydown.ctrl.s="save" type="text">Les accions també es poden controlar temporalment amb debounce i throttle per evitar que l'usuari faci clic repetidament en un botó que desencadena una operació costosa:
{{-- Espera 300ms abans d'executar --}}
<input wire:keydown.debounce.300ms="search" type="text">
{{-- Bloqueja durant la petició --}}
<button wire:click="save" wire:loading.attr="disabled">
Desar
</button>La directiva wire:loading mostra un indicador de càrrega mentre una acció s'està executant al servidor. Pots mostrar spinners, desactivar botons o canviar el text d'un botó per donar feedback visual a l'usuari durant les operacions que triguen uns mil·lisegons:
<button wire:click="save">
<span wire:loading.remove wire:target="save">Desar</span>
<span wire:loading wire:target="save">Desant...</span>
</button>Cicle de vida#
Cada component Livewire passa per un cicle de vida amb diversos punts d'extensió on pots intervenir. Entendre quan s'executa cada hook és essencial per escriure components correctes i eficients, ja que determina on has de col·locar la lògica d'inicialització, transformació de dades i neteja.
El mètode mount() s'executa una sola vegada, quan el component es crea per primera vegada. És l'equivalent al constructor d'un component i el lloc ideal per inicialitzar propietats amb dades que provenen de paràmetres, de la base de dades o de serveis injectats. A diferència del constructor PHP, mount() suporta la injecció de dependències del contenidor de serveis de Laravel:
class EditArticle extends Component
{
public string $title = '';
public string $body = '';
public string $category = '';
public function mount(Article $article)
{
$this->title = $article->title;
$this->body = $article->body;
$this->category = $article->category;
}
public function render()
{
return view('livewire.edit-article');
}
}El mètode hydrate() s'executa a cada petició posterior, just després que Livewire reconstrueixi l'estat del component a partir de les dades serialitzades. Això és útil per re-establir objectes o connexions que no es poden serialitzar. El mètode dehydrate() és l'invers: s'executa just abans que l'estat del component es serialitzi per enviar-lo al client. Aquí pots fer tasques de neteja o transformació de dades abans de la serialització:
class ReportGenerator extends Component
{
public string $dateRange = 'last_30_days';
protected $reportService;
public function hydrate()
{
// Es crida a cada petició posterior al mount()
$this->reportService = app(ReportService::class);
}
public function dehydrate()
{
// Es crida abans de serialitzar l'estat
// Ideal per netejar objectes no serialitzables
}
}Els hooks updating i updated s'executen quan una propietat canvia de valor. El hook updating es crida abans que el canvi s'apliqui (i permet prevenir-lo), mentre que updated es crida després. Pots crear hooks genèrics per a totes les propietats o específics per a una propietat concreta:
class UserSettings extends Component
{
public string $email = '';
public string $username = '';
// Es crida abans que qualsevol propietat canviï
public function updating($property, $value)
{
logger("La propietat {$property} canviarà a {$value}");
}
// Es crida després que qualsevol propietat hagi canviat
public function updated($property, $value)
{
logger("La propietat {$property} ha canviat a {$value}");
}
// Hook específic per a la propietat 'email'
public function updatedEmail($value)
{
$this->validateOnly('email');
}
// Hook específic per a la propietat 'username'
public function updatingUsername($value)
{
// Convertir a minúscules automàticament
$this->username = strtolower($value);
}
}Els hooks específics per propietat segueixen la convenció updated{NomPropietat} i updating{NomPropietat}, on el nom de la propietat va en PascalCase. Aquests hooks són molt útils per a validació en temps real, transformació automàtica de valors o per desencadenar accions secundàries quan un valor canvia.
El mètode boot() és un hook especial dissenyat principalment per a l'ús dins de traits. S'executa a cada petició (inclosa la primera) i és útil per inicialitzar comportament compartit entre múltiples components:
trait WithSoftDeletes
{
public bool $showDeleted = false;
public function bootWithSoftDeletes()
{
// S'executa a cada petició per a qualsevol
// component que utilitzi aquest trait
}
}Formularis i validació#
Una de les funcionalitats més potents de Livewire és la gestió de formularis amb validació en temps real. Livewire 3 introdueix els objectes Form, que permeten encapsular tota la lògica d'un formulari (propietats, regles de validació, mètodes d'emmagatzematge) en una classe separada, mantenint el component principal net i organitzat.
Per crear un objecte Form, crea una classe que estengui Livewire\Form. Dins d'aquesta classe, defineixes les propietats del formulari i les regles de validació amb l'atribut #[Validate]:
// app/Livewire/Forms/ArticleForm.php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ArticleForm extends Form
{
#[Validate('required|min:3|max:255')]
public string $title = '';
#[Validate('required|min:10')]
public string $body = '';
#[Validate('required|in:tech,ciencia,cultura')]
public string $category = '';
#[Validate('nullable|url')]
public string $source_url = '';
public function store()
{
$this->validate();
Article::create($this->all());
$this->reset();
}
public function update(Article $article)
{
$this->validate();
$article->update($this->all());
}
}Després, utilitza l'objecte Form al component. El component queda molt més net perquè tota la lògica del formulari viu a la classe Form:
// app/Livewire/CreateArticle.php
namespace App\Livewire;
use App\Livewire\Forms\ArticleForm;
use Livewire\Component;
class CreateArticle extends Component
{
public ArticleForm $form;
public function save()
{
$this->form->store();
session()->flash('message', 'Article creat correctament.');
}
public function render()
{
return view('livewire.create-article');
}
}A la vista Blade, les propietats del formulari s'accedeixen amb el prefix form., tant per al vinculament de dades com per als missatges d'error:
<div>
@if(session('message'))
<div class="bg-green-100 text-green-800 p-3 rounded">
{{ session('message') }}
</div>
@endif
<form wire:submit="save">
<div>
<label for="title">Títol</label>
<input wire:model="form.title" id="title" type="text">
@error('form.title')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
<div>
<label for="body">Contingut</label>
<textarea wire:model="form.body" id="body" rows="6"></textarea>
@error('form.body')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
<div>
<label for="category">Categoria</label>
<select wire:model="form.category" id="category">
<option value="">Selecciona...</option>
<option value="tech">Tecnologia</option>
<option value="ciencia">Ciència</option>
<option value="cultura">Cultura</option>
</select>
@error('form.category')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
<div>
<label for="source_url">URL font (opcional)</label>
<input wire:model="form.source_url" id="source_url" type="url">
@error('form.source_url')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
<button type="submit">Crear article</button>
</form>
</div>Si prefereixes validació en temps real a mesura que l'usuari omple el formulari, pots combinar wire:model.live o wire:model.blur amb el mètode validateOnly() dins dels hooks updated. D'aquesta manera, cada camp es valida individualment quan l'usuari acaba d'escriure-hi, mostrant errors immediatament sense esperar l'enviament del formulari:
class CreateArticle extends Component
{
public ArticleForm $form;
public function updated($property)
{
$this->validateOnly($property);
}
public function save()
{
$this->form->store();
}
}Si no vols utilitzar objectes Form i prefereixes definir tot directament al component, pots utilitzar l'atribut #[Validate] directament sobre les propietats del component o el mètode $this->validate() amb regles definides manualment:
class ContactForm extends Component
{
#[Validate('required|min:2')]
public string $name = '';
#[Validate('required|email')]
public string $email = '';
#[Validate('required|min:20')]
public string $message = '';
public function send()
{
$validated = $this->validate();
// Enviar el correu electrònic
Mail::to('info@example.com')->send(
new ContactMessage($validated)
);
$this->reset();
session()->flash('success', 'Missatge enviat correctament.');
}
public function render()
{
return view('livewire.contact-form');
}
}Per a un exemple complet de CRUD, vegem com construir un gestor d'articles amb creació, edició i eliminació. L'objecte Form es pot reutilitzar tant per crear com per editar, omplint-lo amb les dades existents:
class ArticleManager extends Component
{
public ArticleForm $form;
public bool $editing = false;
public ?int $editingId = null;
public function edit(Article $article)
{
$this->form->fill($article->toArray());
$this->editing = true;
$this->editingId = $article->id;
}
public function save()
{
if ($this->editing) {
$article = Article::findOrFail($this->editingId);
$this->form->update($article);
} else {
$this->form->store();
}
$this->editing = false;
$this->editingId = null;
$this->form->reset();
session()->flash('message', 'Article desat correctament.');
}
public function delete(Article $article)
{
$article->delete();
session()->flash('message', 'Article eliminat.');
}
public function cancel()
{
$this->editing = false;
$this->editingId = null;
$this->form->reset();
}
public function render()
{
return view('livewire.article-manager', [
'articles' => Article::latest()->paginate(10),
]);
}
}Pujada de fitxers#
La pujada de fitxers amb Livewire és sorprenentment senzilla. Gràcies al trait WithFileUploads, pots gestionar la pujada de fitxers amb la mateixa sintaxi wire:model que fas servir per a qualsevol altre camp de formulari. Livewire s'encarrega de tot el procés: puja el fitxer temporalment al servidor, permet previsualitzar-lo i finalment l'emmagatzema a la ubicació definitiva quan s'envia el formulari.
Per habilitar la pujada de fitxers, afegeix el trait WithFileUploads al component i crea una propietat pública per al fitxer:
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
class UploadAvatar extends Component
{
use WithFileUploads;
public $photo;
public function save()
{
$this->validate([
'photo' => 'required|image|max:2048', // Màxim 2MB
]);
$path = $this->photo->store('avatars', 'public');
auth()->user()->update(['avatar' => $path]);
session()->flash('message', 'Avatar actualitzat.');
}
public function render()
{
return view('livewire.upload-avatar');
}
}La vista pot incloure una previsualització del fitxer pujat. Quan l'usuari selecciona un fitxer, Livewire el puja automàticament a un directori temporal i genera una URL temporal per a la previsualització. Aquesta URL només és accessible durant la sessió actual i el fitxer temporal s'elimina automàticament:
<div>
<form wire:submit="save">
<div>
<label for="photo">Selecciona una foto</label>
<input wire:model="photo" id="photo" type="file">
@error('photo')
<span class="text-red-500">{{ $message }}</span>
@enderror
</div>
@if($photo)
<div>
<p>Previsualització:</p>
<img src="{{ $photo->temporaryUrl() }}" class="w-32 h-32 rounded-full">
</div>
@endif
<button type="submit">
<span wire:loading.remove wire:target="photo">Desar avatar</span>
<span wire:loading wire:target="photo">Pujant...</span>
</button>
</form>
</div>Per a la pujada de múltiples fitxers, simplement canvia la propietat a un array i afegeix l'atribut multiple al camp d'entrada. La validació s'aplica a cada fitxer individualment:
class UploadGallery extends Component
{
use WithFileUploads;
public array $photos = [];
public function save()
{
$this->validate([
'photos' => 'required|array|max:5',
'photos.*' => 'image|max:4096',
]);
foreach ($this->photos as $photo) {
$photo->store('gallery', 'public');
}
$this->photos = [];
}
}<div>
<input wire:model="photos" type="file" multiple>
<div wire:loading wire:target="photos">
Pujant fitxers...
</div>
@if($photos)
<p>{{ count($photos) }} fitxers seleccionats</p>
@endif
</div>Pots mostrar una barra de progrés durant la pujada utilitzant els events JavaScript de Livewire per a fitxers, que permeten rastrejar el percentatge de pujada en temps real. Això es fa amb la directiva Alpine integrada que escolta els events de progrés de Livewire:
<div
x-data="{ uploading: false, progress: 0 }"
x-on:livewire-upload-start="uploading = true"
x-on:livewire-upload-finish="uploading = false; progress = 0"
x-on:livewire-upload-cancel="uploading = false"
x-on:livewire-upload-error="uploading = false"
x-on:livewire-upload-progress="progress = $event.detail.progress"
>
<input wire:model="photo" type="file">
<div x-show="uploading">
<div class="bg-gray-200 rounded-full h-2">
<div class="bg-blue-500 h-2 rounded-full"
:style="'width: ' + progress + '%'"></div>
</div>
<span x-text="progress + '%'"></span>
</div>
</div>Events#
El sistema d'events de Livewire permet la comunicació entre components. Quan tens múltiples components a una mateixa pàgina, els events són el mecanisme per fer que un component notifiqui als altres sobre canvis d'estat o accions importants. Per exemple, quan un component de formulari crea un nou registre, pot emetre un event perquè un component de llista es refresqui automàticament.
Per emetre un event des d'un component, utilitza el mètode $this->dispatch(). Per escoltar events, utilitza l'atribut #[On] sobre un mètode del component receptor:
// Component que emet l'event
class CreateArticle extends Component
{
public ArticleForm $form;
public function save()
{
$this->form->store();
$this->form->reset();
// Emetre event amb dades opcionals
$this->dispatch('article-created', title: $this->form->title);
}
}
// Component que escolta l'event
use Livewire\Attributes\On;
class ArticleList extends Component
{
#[On('article-created')]
public function refreshList($title = null)
{
// Aquest mètode s'executa quan es rep l'event
// El component es re-renderitza automàticament
if ($title) {
session()->flash('message', "Article '{$title}' creat.");
}
}
public function render()
{
return view('livewire.article-list', [
'articles' => Article::latest()->paginate(10),
]);
}
}Quan vols enviar un event només al component pare o a un component fill específic, pots utilitzar els mètodes dedicats per controlar l'abast de l'event. Això evita que tots els components de la pàgina rebin events que no els corresponen:
// Enviar event només al component pare
$this->dispatch('item-saved')->to(ParentComponent::class);
// Enviar event a si mateix (útil per refrescar)
$this->dispatch('refresh')->self();Livewire també permet emetre events cap al navegador que es poden escoltar amb JavaScript o Alpine.js. Això és útil quan necessites interactuar amb biblioteques JavaScript de tercers, tancar un modal amb Alpine o mostrar una notificació:
// Al component PHP
$this->dispatch('show-notification', [
'type' => 'success',
'message' => 'Operació completada.',
]);{{-- A la vista amb Alpine.js --}}
<div
x-data="{ show: false, message: '', type: '' }"
x-on:show-notification.window="
show = true;
message = $event.detail.message;
type = $event.detail.type;
setTimeout(() => show = false, 3000);
"
>
<div x-show="show" x-transition
:class="type === 'success' ? 'bg-green-100' : 'bg-red-100'"
class="p-4 rounded">
<span x-text="message"></span>
</div>
</div>La comunicació entre components pare i fill és un patró especialment comú. Un component pare pot passar dades als fills a través de propietats, i els fills poden comunicar canvis al pare a través d'events. Això crea un flux de dades unidireccional similar al que trobes a frameworks JavaScript com React:
// Component pare
class OrderPage extends Component
{
public array $items = [];
#[On('item-added')]
public function addItem($productId, $quantity)
{
$this->items[] = [
'product_id' => $productId,
'quantity' => $quantity,
];
}
#[On('item-removed')]
public function removeItem($index)
{
array_splice($this->items, $index, 1);
}
public function render()
{
return view('livewire.order-page');
}
}{{-- Vista del pare --}}
<div>
<livewire:product-selector />
<livewire:order-summary :items="$items" />
</div>Navegació SPA#
Una de les funcionalitats més impressionants de Livewire 3 és la navegació SPA (Single Page Application). Amb la directiva wire:navigate, pots convertir la teva aplicació Laravel en una experiència similar a una SPA sense necessitat de frameworks JavaScript com Vue o React, sense API REST ni cap configuració complexa.
Quan afegeixes wire:navigate a un enllaç, Livewire intercepta el clic, realitza una petició AJAX per obtenir el contingut de la nova pàgina, i substitueix el contingut del <body> sense recarregar tota la pàgina. Això elimina el parpelleig blanc entre pàgines, manté els scripts JavaScript en memòria i ofereix una experiència de navegació molt més fluida:
{{-- En lloc de --}}
<a href="/articles">Articles</a>
{{-- Utilitza --}}
<a href="/articles" wire:navigate>Articles</a>El resultat visual és significatiu: la pàgina es carrega gairebé instantàniament perquè no hi ha recàrrega completa del navegador. Els estils CSS es mantenen, els scripts JavaScript no es tornen a executar, i l'experiència és equivalent a la d'una SPA moderna.
Livewire també implementa prefetching automàtic: quan l'usuari passa el cursor per sobre d'un enllaç amb wire:navigate, Livewire comença a carregar la pàgina de destinació abans que l'usuari faci clic. Això significa que quan finalment clica, la pàgina ja està parcialment o completament carregada, donant la sensació de navegació instantània:
{{-- Prefetch al hover (comportament per defecte) --}}
<a href="/articles" wire:navigate>Articles</a>
{{-- Desactivar prefetch per a enllaços que canvien freqüentment --}}
<a href="/dashboard" wire:navigate.hover="false">Dashboard</a>Quan necessites que certs elements persisteixin entre navegacions (per exemple, un reproductor d'àudio o un comptador), pots utilitzar la directiva @persist. Els elements marcats amb @persist no es destrueixen ni es recreen durant la navegació SPA, sinó que es mantenen intactes:
{{-- Al layout principal --}}
<body>
<x-header />
@persist('player')
<div id="audio-player">
<audio src="/podcast.mp3" controls></audio>
</div>
@endpersist
{{ $slot }}
<x-footer />
</body>En aquest exemple, si l'usuari està escoltant un podcast i navega a una altra pàgina, la reproducció no s'interromp perquè l'element del reproductor persisteix entre navegacions.
Livewire inclou una barra de progrés configurable que apareix a la part superior de la pàgina durant la navegació per donar feedback visual a l'usuari. Pots personalitzar el color i el comportament d'aquesta barra:
{{-- Al layout --}}
@livewireScriptConfig([
'navigate' => [
'showProgressBar' => true,
'progressBarColor' => '#2299dd',
],
])Per activar la navegació SPA a tota l'aplicació de manera global, el més pràctic és afegir wire:navigate als enllaços del layout principal (navegació, menú lateral, peu de pàgina). D'aquesta manera, tota la navegació de l'aplicació funciona com una SPA sense haver de modificar cada enllaç individualment.
Lazy Loading#
El lazy loading de components és una tècnica d'optimització que permet diferir la càrrega de components pesats fins que realment es necessiten. En lloc de carregar tots els components de la pàgina immediatament, un component lazy es renderitza inicialment com un placeholder buit i carrega el contingut real de manera asíncrona, després que la pàgina principal s'hagi renderitzat.
Això és especialment útil per a components que fan consultes costoses a la base de dades, components que apareixen sota el fold (la part de la pàgina que l'usuari no veu immediatament) o qualsevol component que alenteixi la càrrega inicial de la pàgina:
{{-- Component amb lazy loading --}}
<livewire:heavy-report lazy />
{{-- Amb contingut placeholder personalitzat --}}
<livewire:sales-chart lazy>
<x-slot:placeholder>
<div class="animate-pulse bg-gray-200 h-64 rounded">
<p class="text-center pt-24 text-gray-500">Carregant gràfic...</p>
</div>
</x-slot:placeholder>
</livewire:sales-chart>Dins del component, pots definir un mètode placeholder() per retornar la vista que es mostra mentre el component es carrega. Això permet crear skeletons o indicadors de càrrega personalitzats que coincideixin amb la forma final del component:
class SalesChart extends Component
{
public function placeholder()
{
return view('livewire.placeholders.chart');
}
public function render()
{
// Aquesta consulta costosa no bloqueja la càrrega inicial
$data = Sale::query()
->selectRaw('MONTH(created_at) as month, SUM(amount) as total')
->groupByRaw('MONTH(created_at)')
->get();
return view('livewire.sales-chart', ['data' => $data]);
}
}El lazy loading és particularment valuós en dashboards on tens múltiples widgets, cadascun amb la seva pròpia consulta a la base de dades. Sense lazy loading, l'usuari hauria d'esperar que totes les consultes es completessin abans de veure res. Amb lazy loading, la pàgina es carrega immediatament amb placeholders, i cada widget apareix de manera independent quan les seves dades estan llestes.
Polling#
El polling és el mecanisme més senzill de Livewire per a actualitzacions en temps real. Amb la directiva wire:poll, un component es refresca automàticament a intervals regulars, permetent mostrar dades actualitzades sense que l'usuari hagi de recarregar la pàgina manualment.
L'interval per defecte és de 2,5 segons, però és recomanable ajustar-lo segons les necessitats del teu cas d'ús. Per a dades que canvien rarament, un interval més llarg redueix la càrrega al servidor:
{{-- Actualitza cada 2,5 segons (per defecte) --}}
<div wire:poll>
Visitants actius: {{ $activeVisitors }}
</div>
{{-- Actualitza cada 15 segons --}}
<div wire:poll.15s>
Comandes pendents: {{ $pendingOrders }}
</div>
{{-- Crida un mètode específic en cada poll --}}
<div wire:poll.5s="checkNewMessages">
@foreach($messages as $message)
<p>{{ $message->text }}</p>
@endforeach
</div>Una consideració important és l'eficiència: si la pestanya del navegador no és visible, les peticions de polling consumeixen recursos innecessàriament. El modificador visible assegura que el polling només s'executa quan l'element és visible a la finestra del navegador:
{{-- Només fa polling quan l'element és visible --}}
<div wire:poll.10s.visible>
<h3>Estadístiques en temps real</h3>
<p>Vendes avui: {{ $salesToday }}</p>
</div>
{{-- Només fa polling quan la pestanya és activa --}}
<div wire:poll.30s.keep-alive>
Última actualització: {{ now()->format('H:i:s') }}
</div>El polling és ideal per a panells de monitorització, comptadors d'estadístiques, llistes de notificacions o qualsevol element que necessiti mostrar dades raonablement actualitzades. Per a actualitzacions veritablement en temps real (com un xat), considera combinar Livewire amb Laravel Echo i websockets en lloc de polling, ja que el polling sempre implica un petit retard entre l'event real i la seva visualització.
class LiveDashboard extends Component
{
#[Computed]
public function stats()
{
return [
'active_users' => User::where('last_active_at', '>', now()->subMinutes(5))->count(),
'pending_orders' => Order::where('status', 'pending')->count(),
'revenue_today' => Order::whereDate('created_at', today())->sum('total'),
];
}
public function render()
{
return view('livewire.live-dashboard');
}
}<div wire:poll.10s>
<div class="grid grid-cols-3 gap-4">
<div class="p-4 bg-white rounded shadow">
<h4>Usuaris actius</h4>
<p class="text-2xl font-bold">{{ $this->stats['active_users'] }}</p>
</div>
<div class="p-4 bg-white rounded shadow">
<h4>Comandes pendents</h4>
<p class="text-2xl font-bold">{{ $this->stats['pending_orders'] }}</p>
</div>
<div class="p-4 bg-white rounded shadow">
<h4>Ingressos avui</h4>
<p class="text-2xl font-bold">{{ number_format($this->stats['revenue_today'], 2) }} EUR</p>
</div>
</div>
</div>Integració amb Alpine.js#
Livewire 3 inclou Alpine.js automàticament, cosa que significa que pots utilitzar Alpine dins de qualsevol component Livewire sense instal·lar res addicional. Aquesta combinació és extremadament potent: Livewire gestiona la lògica del servidor i l'estat persistent, mentre Alpine gestiona les interaccions purament de client com mostrar/amagar elements, animacions, transicions i manipulacions del DOM que no necessiten comunicació amb el servidor.
La regla general és senzilla: si una interacció necessita accedir a dades del servidor o modificar l'estat persistent, utilitza Livewire. Si la interacció és purament visual i no necessita el servidor (com obrir un menú desplegable, mostrar un tooltip o alternar la visibilitat d'un element), utilitza Alpine. Combinar ambdues eines et permet crear interfícies molt responsives sense sacrificar la simplicitat de treballar amb PHP:
<div>
{{-- Alpine per a interaccions de client --}}
<div x-data="{ open: false }">
<button @click="open = !open">
Opcions
</button>
<div x-show="open" x-transition @click.outside="open = false">
{{-- Livewire per a accions del servidor --}}
<button wire:click="duplicate({{ $article->id }})">Duplicar</button>
<button wire:click="archive({{ $article->id }})">Arxivar</button>
<button wire:click="delete({{ $article->id }})"
wire:confirm="Segur?">Eliminar</button>
</div>
</div>
</div>La directiva @entangle crea un vinculament bidireccional entre una propietat de Livewire i una variable d'Alpine. Quan el valor canvia a Alpine, es sincronitza amb Livewire al servidor, i viceversa. Això és útil quan necessites que l'estat sigui accessible tant des de PHP com des de JavaScript:
<div
x-data="{ showFilters: @entangle('showFilters') }"
>
<button @click="showFilters = !showFilters">
Filtres
</button>
{{-- Alpine controla l'animació de mostrar/amagar --}}
<div x-show="showFilters" x-transition.duration.300ms>
{{-- Livewire gestiona els filtres reals --}}
<select wire:model.live="category">
<option value="">Totes</option>
<option value="tech">Tecnologia</option>
<option value="ciencia">Ciència</option>
</select>
<input wire:model.live.debounce.300ms="search"
type="text"
placeholder="Cercar...">
</div>
</div>Si vols que l'entangle sigui només live al costat del client (sense enviar peticions al servidor cada vegada que Alpine modifica el valor), pots afegir el modificador .live per controlar quan es sincronitza:
{{-- Sincronització completa (cada canvi va al servidor) --}}
<div x-data="{ count: @entangle('count') }">
{{-- Sincronització live (canvis d'Alpine van immediatament al servidor) --}}
<div x-data="{ count: @entangle('count').live }">L'objecte $wire permet accedir directament al component Livewire des de JavaScript Alpine. Pots llegir propietats, cridar mètodes i escoltar events del component Livewire dins de codi Alpine. Això és molt útil quan necessites coordinar comportaments complexos entre el client i el servidor:
<div x-data>
{{-- Llegir una propietat de Livewire --}}
<span x-text="$wire.count"></span>
{{-- Cridar un mètode de Livewire --}}
<button @click="$wire.increment()">+1</button>
{{-- Cridar un mètode i esperar la resposta --}}
<button @click="
let result = await $wire.calculateTotal();
alert('Total: ' + result);
">
Calcular
</button>
{{-- Assignar un valor a una propietat de Livewire --}}
<button @click="$wire.status = 'active'">Activar</button>
</div>Un patró avançat molt comú és utilitzar Alpine per a la gestió d'un modal, combinat amb Livewire per carregar i gestionar les dades del modal. Alpine s'encarrega d'obrir i tancar el modal amb animacions suaus, mentre Livewire carrega el contingut del modal des del servidor:
class ArticlePage extends Component
{
public ?Article $selectedArticle = null;
public bool $showModal = false;
public function openModal(Article $article)
{
$this->selectedArticle = $article;
$this->showModal = true;
}
public function closeModal()
{
$this->showModal = false;
$this->selectedArticle = null;
}
public function render()
{
return view('livewire.article-page', [
'articles' => Article::latest()->paginate(12),
]);
}
}<div>
<div class="grid grid-cols-3 gap-4">
@foreach($articles as $article)
<div class="p-4 border rounded cursor-pointer"
wire:click="openModal({{ $article->id }})">
<h3>{{ $article->title }}</h3>
<p>{{ Str::limit($article->excerpt, 100) }}</p>
</div>
@endforeach
</div>
{{-- Modal gestionat per Alpine + Livewire --}}
<div
x-data="{ show: @entangle('showModal') }"
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@keydown.escape.window="$wire.closeModal()"
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
style="display: none;"
>
<div @click.outside="$wire.closeModal()"
class="bg-white rounded-lg shadow-xl max-w-2xl w-full p-6">
@if($selectedArticle)
<h2 class="text-xl font-bold">{{ $selectedArticle->title }}</h2>
<p class="mt-4">{{ $selectedArticle->body }}</p>
<div class="mt-6 flex justify-end">
<button wire:click="closeModal"
class="px-4 py-2 bg-gray-200 rounded">
Tancar
</button>
</div>
@endif
</div>
</div>
{{ $articles->links() }}
</div>Aquesta combinació de Livewire i Alpine aprofita el millor de cada eina: Alpine proporciona transicions CSS suaus i gestió del DOM instantània al client, mentre Livewire s'encarrega de carregar les dades de l'article des del servidor i gestionar l'estat del modal de manera segura. El resultat és una interfície ràpida, fluida i completament construïda amb PHP i Blade, sense necessitat d'un framework JavaScript complet.