Alpine.js

Com utilitzar Alpine.js amb Laravel per afegir interactivitat lleugera: directives, propietats màgiques, plugins i integració amb Blade.

Què és Alpine.js?#

Alpine.js és un framework JavaScript lleuger dissenyat per afegir interactivitat directament al teu HTML sense necessitar un pas de compilació, un DOM virtual ni una arquitectura de components complexa. Sovint se'l descriu com el "jQuery de l'era moderna", però amb una diferència fonamental: Alpine és reactiu. Això vol dir que quan les dades canvien, la interfície s'actualitza automàticament, sense que hagis de manipular el DOM manualment com feies amb jQuery.

La filosofia d'Alpine.js és senzilla i poderosa: en lloc de crear components en fitxers JavaScript separats, declares el comportament interactiu directament dins del teu markup HTML. Aquesta aproximació encaixa perfectament amb el model de treball de Blade, on les vistes ja estan definides en fitxers de plantilla. Amb Alpine, afegeixes atributs especials (anomenats directives) als teus elements HTML, i automàticament obtens reactivitat, gestió d'esdeveniments i manipulació del DOM sense sortir del teu fitxer Blade.

Alpine va ser creat per Caleb Porzio, el mateix desenvolupador darrere de Livewire. Això no és cap coincidència: tots dos projectes comparteixen la mateixa visió de fer que el desenvolupament web amb Laravel sigui productiu i agradable sense necessitat de dominar frameworks JavaScript complexos. Alpine es va concebre específicament per cobrir aquells casos d'ús on necessites un dropdown, un modal, unes pestanyes o un toggle, però no necessites ni vols la complexitat d'instal·lar i configurar Vue, React o Svelte.

Un dels avantatges més destacats d'Alpine és la seva mida. Amb aproximadament 15 KB minificat i comprimit, Alpine és significativament més petit que Vue (~30 KB) o React (~40 KB o més amb ReactDOM). Aquesta petjada mínima fa que sigui ideal per a projectes on el rendiment de càrrega és important i on la interactivitat necessària és relativament senzilla. No estàs carregant tot un framework per mostrar i amagar un menú desplegable.

Ara bé, quan hauries d'utilitzar Alpine en lloc de Livewire o d'un framework JavaScript complet? La regla general és aquesta: utilitza Alpine per a interactivitat purament visual que no necessita comunicació amb el servidor. Dropdowns, modals, pestanyes, toggles, comptadors de caràcters, mostrar i amagar camps de formulari: tot això és territori d'Alpine. Quan necessites interactivitat que impliqui dades del servidor (cerca en temps real, filtrat, CRUD, actualitzacions dinàmiques), Livewire és l'eina adequada. I quan estàs construint una aplicació amb interfícies molt complexes tipus SPA, llavors és el moment de considerar Vue amb Inertia o React. La bona notícia és que Alpine i Livewire funcionen junts de manera nativa, ja que Livewire inclou Alpine per defecte.

Instal·lació#

Hi ha dues maneres principals d'integrar Alpine.js al teu projecte Laravel. La primera i més recomanada per a projectes que ja utilitzen Vite (que és el cas per defecte a Laravel) és instal·lar-lo via npm. La segona és utilitzar un CDN, que pot ser útil per a prototipatge ràpid o projectes molt senzills.

Per instal·lar Alpine via npm, executa la següent comanda al directori arrel del teu projecte:

npm install alpinejs

Un cop instal·lat, has d'importar Alpine i inicialitzar-lo al teu fitxer JavaScript principal. Normalment, aquest fitxer és resources/js/app.js, que és el punt d'entrada que Vite processa:

// resources/js/app.js
import Alpine from 'alpinejs';
 
window.Alpine = Alpine;
Alpine.start();

La línia window.Alpine = Alpine és important perquè fa que Alpine sigui accessible globalment. Això és necessari per poder registrar stores, plugins i components reutilitzables des d'altres scripts. La crida a Alpine.start() inicialitza el framework i comença a processar totes les directives Alpine que trobi al DOM.

Si prefereixes un enfocament més senzill sense bundler, pots incloure Alpine directament via CDN al teu layout Blade. Aquesta opció és perfecta per a proves ràpides o projectes petits:

{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'La meva aplicació')</title>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
    {{ $slot }}
</body>
</html>

L'atribut defer és important quan utilitzes el CDN. Assegura que Alpine s'executa després que tot el HTML s'hagi carregat, evitant que intenti processar directives abans que existeixin al DOM. Amb la instal·lació via npm i Vite, això ja es gestiona automàticament gràcies a la manera com Vite injecta els scripts.

Quan utilitzes Vite, recorda incloure el teu fitxer JavaScript al layout amb la directiva @vite:

<head>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

Directives fonamentals#

Les directives són el cor d'Alpine.js. Són atributs HTML especials que comencen amb x- i que indiquen a Alpine com ha de comportar-se un element. Cada directiva té un propòsit específic, i combinant-les pots crear interaccions complexes sense escriure ni una sola línia de JavaScript en un fitxer separat.

x-data#

La directiva x-data és la base de qualsevol component Alpine. Declara un bloc reactiu i defineix les dades que el component utilitzarà. Tots els elements fills dins d'un element amb x-data tenen accés a les dades declarades. Pensa en x-data com l'equivalent d'un data() a Vue o un useState a React, però definit directament al HTML.

La forma més senzilla d'utilitzar x-data és amb un objecte JavaScript en línia:

<div x-data="{ obert: false, comptador: 0, nom: 'Alpine' }">
    <p x-text="nom"></p>
    <p x-text="comptador"></p>
    <button @click="comptador++">Incrementar</button>
</div>

En aquest exemple, l'element <div> i tots els seus descendents poden accedir a les propietats obert, comptador i nom. Quan qualsevol d'aquestes propietats canvia (per exemple, en fer clic al botó que incrementa el comptador), Alpine actualitza automàticament qualsevol part del DOM que depengui d'aquella dada.

Per a components amb més lògica, és recomanable extreure les dades i mètodes a una funció JavaScript. Això manté el markup net i permet reutilitzar la lògica:

// resources/js/app.js
import Alpine from 'alpinejs';
 
Alpine.data('formulariContacte', () => ({
    nom: '',
    email: '',
    missatge: '',
    enviant: false,
 
    async enviar() {
        this.enviant = true;
        try {
            const response = await fetch('/api/contacte', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    nom: this.nom,
                    email: this.email,
                    missatge: this.missatge,
                }),
            });
            if (response.ok) {
                this.nom = '';
                this.email = '';
                this.missatge = '';
            }
        } finally {
            this.enviant = false;
        }
    },
}));
 
window.Alpine = Alpine;
Alpine.start();

Ara pots utilitzar aquest component al teu Blade de manera molt neta:

<form x-data="formulariContacte" @submit.prevent="enviar">
    <input type="text" x-model="nom" placeholder="Nom">
    <input type="email" x-model="email" placeholder="Email">
    <textarea x-model="missatge" placeholder="Missatge"></textarea>
    <button type="submit" :disabled="enviant">
        <span x-show="!enviant">Enviar</span>
        <span x-show="enviant">Enviant...</span>
    </button>
</form>

Un aspecte important d'x-data és que els àmbits es poden nidificar. Quan tens un x-data dins d'un altre, el fill té accés tant a les seves pròpies dades com a les del pare. Si hi ha conflicte de noms, les dades del fill tenen prioritat:

<div x-data="{ color: 'blau', mida: 'gran' }">
    <p x-text="color"></p> {{-- Mostra: blau --}}
 
    <div x-data="{ color: 'vermell', pes: 'lleuger' }">
        <p x-text="color"></p> {{-- Mostra: vermell (sobreescriu el pare) --}}
        <p x-text="mida"></p>  {{-- Mostra: gran (heretat del pare) --}}
        <p x-text="pes"></p>   {{-- Mostra: lleuger (propi del fill) --}}
    </div>
</div>

x-show i x-if#

Aquestes dues directives controlen la visibilitat d'elements, però ho fan de maneres molt diferents. Entendre quan utilitzar cadascuna és clau per construir interfícies eficients.

x-show mostra o amaga un element canviant la propietat CSS display. L'element sempre existeix al DOM; simplement es fa visible o invisible. Això el fa ideal per a elements que canvien de visibilitat freqüentment, com menús desplegables o panells que s'obren i tanquen, perquè no hi ha cap cost de crear o destruir elements al DOM:

<div x-data="{ visible: false }">
    <button @click="visible = !visible">
        Mostrar/Amagar detalls
    </button>
 
    <div x-show="visible" class="mt-4 p-4 bg-gray-100 rounded">
        <p>Aquests són els detalls addicionals del producte.</p>
        <p>Sempre existeixen al DOM, però es mostren o s'amaguen.</p>
    </div>
</div>

x-if, en canvi, afegeix o elimina completament l'element del DOM. Requereix que s'utilitzi dins d'una etiqueta <template>, ja que Alpine necessita un contenidor que no es renderitzi per si mateix. x-if és adequat quan tens contingut que rarament es mostra o que consumeix recursos significatius quan existeix al DOM:

<div x-data="{ mostrarFormulari: false }">
    <button @click="mostrarFormulari = true">
        Registrar-se
    </button>
 
    <template x-if="mostrarFormulari">
        <form class="mt-4 p-6 border rounded">
            <h3>Formulari de registre</h3>
            <input type="text" placeholder="Nom d'usuari">
            <input type="email" placeholder="Correu electrònic">
            <button type="submit">Crear compte</button>
        </form>
    </template>
</div>

La diferència pràctica és que x-show és més ràpid per a canvis freqüents de visibilitat (ja que no manipula el DOM), mentre que x-if és millor per a contingut condicional que apareix rarament (ja que estalvia memòria quan no és visible).

Alpine inclou la directiva x-transition per afegir animacions suaus quan elements apareixen o desapareixen amb x-show. Sense cap configuració addicional, x-transition aplica una transició d'opacitat i escala que fa que els canvis de visibilitat siguin molt més agradables visualment:

<div x-data="{ obert: false }">
    <button @click="obert = !obert">Notificació</button>
 
    <div x-show="obert"
         x-transition:enter="transition ease-out duration-300"
         x-transition:enter-start="opacity-0 transform scale-90"
         x-transition:enter-end="opacity-100 transform scale-100"
         x-transition:leave="transition ease-in duration-200"
         x-transition:leave-start="opacity-100 transform scale-100"
         x-transition:leave-end="opacity-0 transform scale-90"
         class="p-4 bg-green-100 text-green-800 rounded">
        Operació realitzada correctament.
    </div>
</div>

Si no necessites tant control, pots simplement escriure x-transition sense cap modificador i Alpine aplicarà unes transicions per defecte que funcionen bé en la majoria de casos. També pots utilitzar modificadors curts com x-transition.duration.500ms per controlar la durada o x-transition.scale.80 per ajustar l'escala inicial.

x-for#

La directiva x-for permet iterar sobre arrays i renderitzar un element per cada ítem. De la mateixa manera que x-if, ha d'anar dins d'una etiqueta <template>. Dins del bucle tens accés a cada element de l'array i al seu índex.

Un aspecte fonamental d'x-for és l'ús de :key. Quan proporcionas una clau única per a cada element, Alpine pot actualitzar el DOM de manera molt més eficient quan l'array canvia, perquè sap quins elements s'han afegit, eliminat o reordenat:

<div x-data="{
    tasques: [
        { id: 1, text: 'Revisar codi', completada: false },
        { id: 2, text: 'Escriure tests', completada: true },
        { id: 3, text: 'Desplegar a producció', completada: false }
    ]
}">
    <h3>Llista de tasques</h3>
 
    <template x-for="tasca in tasques" :key="tasca.id">
        <div class="flex items-center gap-2 p-2">
            <input type="checkbox"
                   :checked="tasca.completada"
                   @click="tasca.completada = !tasca.completada">
            <span :class="{ 'line-through text-gray-400': tasca.completada }"
                  x-text="tasca.text">
            </span>
        </div>
    </template>
</div>

Pots accedir a l'índex de cada element afegint un segon paràmetre al bucle. Això és útil quan necessites numerar els elements o aplicar estils alternats:

<template x-for="(element, index) in elements" :key="index">
    <div>
        <span x-text="index + 1"></span>.
        <span x-text="element.nom"></span>
    </div>
</template>

Els bucles niuats també funcionen perfectament. Cada bucle interior té accés a les variables del bucle exterior, cosa que permet renderitzar estructures de dades complexes com taules o llistes agrupades.

x-bind i x-on#

x-bind permet vincular dinàmicament qualsevol atribut HTML a una expressió Alpine. La notació abreujada és :atribut, que és molt més concisa i és la forma que veuràs a la pràctica. Pots vincular classes CSS, atributs disabled, src d'imatges, href d'enllaços, o qualsevol altre atribut HTML:

<div x-data="{ actiu: false, url: '/perfil', imatge: '/img/avatar.jpg' }">
    {{-- Vincular una classe CSS condicionalment --}}
    <button :class="{ 'bg-blue-500 text-white': actiu, 'bg-gray-200': !actiu }"
            @click="actiu = !actiu">
        Canviar estat
    </button>
 
    {{-- Vincular un href dinàmic --}}
    <a :href="url">Anar al perfil</a>
 
    {{-- Vincular l'atribut src --}}
    <img :src="imatge" alt="Avatar">
 
    {{-- Vincular l'atribut disabled --}}
    <button :disabled="!actiu">Acció</button>
</div>

x-on és la directiva per gestionar esdeveniments. La notació abreujada és @event, i accepta qualsevol esdeveniment del DOM: click, submit, input, keydown, mouseenter, scroll, i molts més. El que fa x-on especialment potent són els seus modificadors, que permeten controlar el comportament de l'event listener sense codi addicional.

Els modificadors d'esdeveniments són una de les funcionalitats més útils d'Alpine. .prevent crida a event.preventDefault() automàticament, essencial per a formularis que vols gestionar amb JavaScript. .stop atura la propagació de l'esdeveniment. .outside s'activa quan l'esdeveniment passa fora de l'element, perfecte per tancar dropdowns. .window i .document vinculen l'event listener a window o document respectivament. .once fa que l'listener s'executi una sola vegada. .debounce i .throttle controlen la freqüència d'execució, ideals per a inputs de cerca:

<div x-data="{ cerca: '' }">
    {{-- .prevent evita l'enviament del formulari --}}
    <form @submit.prevent="cercarResultats()">
        {{-- .debounce.500ms espera 500ms després de l'última tecla --}}
        <input type="text"
               x-model="cerca"
               @input.debounce.500ms="cercarResultats()">
    </form>
 
    {{-- .outside tanca el dropdown en clicar fora --}}
    <div x-data="{ obert: false }" @click.outside="obert = false">
        <button @click="obert = !obert">Opcions</button>
        <div x-show="obert">Contingut del dropdown</div>
    </div>
 
    {{-- Modificadors de teclat --}}
    <input type="text"
           @keydown.enter="enviar()"
           @keydown.escape="cancelar()"
           @keydown.ctrl.s.prevent="desar()">
</div>

Els modificadors de teclat permeten respondre a tecles específiques de manera molt neta. Pots combinar tecles modificadores com .ctrl, .shift, .alt i .meta amb qualsevol tecla per crear dreceres de teclat directament al markup.

x-model#

La directiva x-model proporciona vinculació bidireccional entre un element de formulari i una propietat Alpine. Quan l'usuari escriu en un input, la propietat s'actualitza automàticament, i quan la propietat canvia des del codi, l'input reflecteix el nou valor. Funciona amb tots els tipus d'elements de formulari: inputs de text, textareas, selects, checkboxes i radio buttons.

La vinculació bidireccional és el que fa que els formularis dinàmics siguin tan fàcils de construir amb Alpine. En lloc d'haver d'afegir event listeners manualment i actualitzar el DOM, simplement declares x-model i Alpine s'encarrega de tot:

<div x-data="{
    nom: '',
    email: '',
    pais: 'AD',
    acceptaTermes: false,
    tipusCompte: 'personal',
    interessos: []
}">
    {{-- Input de text --}}
    <input type="text" x-model="nom" placeholder="Nom complet">
 
    {{-- Textarea --}}
    <textarea x-model="email" placeholder="Correu electrònic"></textarea>
 
    {{-- Select --}}
    <select x-model="pais">
        <option value="AD">Andorra</option>
        <option value="ES">Espanya</option>
        <option value="FR">França</option>
    </select>
 
    {{-- Checkbox --}}
    <label>
        <input type="checkbox" x-model="acceptaTermes">
        Accepto els termes i condicions
    </label>
 
    {{-- Radio buttons --}}
    <label>
        <input type="radio" x-model="tipusCompte" value="personal">
        Personal
    </label>
    <label>
        <input type="radio" x-model="tipusCompte" value="empresa">
        Empresa
    </label>
 
    {{-- Checkboxes múltiples (array) --}}
    <label><input type="checkbox" x-model="interessos" value="php"> PHP</label>
    <label><input type="checkbox" x-model="interessos" value="laravel"> Laravel</label>
    <label><input type="checkbox" x-model="interessos" value="alpine"> Alpine.js</label>
 
    <p>Interessos seleccionats: <span x-text="interessos.join(', ')"></span></p>
</div>

x-model admet diversos modificadors que canvien el seu comportament. El modificador .lazy fa que el valor només s'actualitzi quan l'element perd el focus (en lloc de cada tecla), cosa que pot ser útil per a validacions que no vols executar constantment. .number converteix automàticament el valor a número. .debounce afegeix un retard abans d'actualitzar el valor, ideal per a camps de cerca on vols esperar que l'usuari acabi d'escriure:

<div x-data="{ cerca: '', quantitat: 0 }">
    <input type="text" x-model.lazy="cerca" placeholder="Cerca (actualitza al sortir)">
    <input type="number" x-model.number="quantitat" placeholder="Quantitat">
    <input type="text" x-model.debounce.300ms="cerca" placeholder="Cerca amb debounce">
</div>

x-text i x-html#

x-text estableix el contingut de text d'un element. És la manera segura de mostrar dades dinàmiques, ja que escapa automàticament qualsevol HTML, prevenint atacs XSS. Sempre que puguis, utilitza x-text en lloc d'x-html:

<div x-data="{ missatge: 'Hola, <strong>món</strong>!' }">
    {{-- x-text: mostra el text escapant HTML --}}
    <p x-text="missatge"></p>
    {{-- Resultat visible: Hola, <strong>món</strong>! --}}
</div>

x-html insereix HTML sense escapar dins d'un element. Això pot ser útil quan necessites renderitzar contingut HTML provinent d'una font de confiança, com un editor de text enriquit. Tanmateix, has de ser molt prudent amb x-html perquè si el contingut prové de l'usuari i no ha estat sanititzat correctament, pots exposar la teva aplicació a vulnerabilitats XSS:

<div x-data="{ contingut: '<strong>Text en negreta</strong> i <em>cursiva</em>' }">
    {{-- x-html: renderitza HTML directament --}}
    <div x-html="contingut"></div>
    {{-- Resultat visible: Text en negreta i cursiva (amb format) --}}
</div>

x-init i x-effect#

x-init executa codi quan un component Alpine s'inicialitza. Això és útil per carregar dades inicials, establir valors per defecte o executar qualsevol lògica de configuració. El codi dins d'x-init s'executa una sola vegada, quan el component es munta al DOM:

<div x-data="{ articles: [], carregant: true }"
     x-init="
        const response = await fetch('/api/articles');
        articles = await response.json();
        carregant = false;
     ">
 
    <div x-show="carregant">Carregant articles...</div>
 
    <template x-for="article in articles" :key="article.id">
        <div class="p-4 border-b">
            <h3 x-text="article.titol"></h3>
            <p x-text="article.resum"></p>
        </div>
    </template>
</div>

x-effect és diferent: executa codi de manera reactiva cada vegada que una de les seves dependències canvia. Alpine detecta automàticament quines propietats utilitza l'expressió dins d'x-effect i torna a executar el codi quan qualsevol d'elles canvia. Funciona de manera similar a watchEffect de Vue:

<div x-data="{ cerca: '', resultats: 0 }"
     x-effect="console.log('La cerca ha canviat a:', cerca)">
 
    <input type="text" x-model="cerca" placeholder="Cercar...">
    <p>Has cercat: <span x-text="cerca"></span></p>
</div>

Cada vegada que l'usuari escriu al camp de cerca, x-effect detecta que la propietat cerca ha canviat i executa el codi de nou. Això és molt útil per sincronitzar l'estat d'Alpine amb sistemes externs, enviar analítiques o actualitzar el localStorage.

x-ref#

La directiva x-ref permet assignar un nom de referència a un element del DOM per accedir-hi posteriorment a través de la propietat màgica $refs. Això és especialment útil quan necessites interactuar directament amb un element del DOM, com donar-li focus, llegir dimensions, o integrar-lo amb biblioteques JavaScript de tercers:

<div x-data="{ }">
    <input type="text" x-ref="campCerca" placeholder="Cercar...">
 
    <button @click="$refs.campCerca.focus()">
        Activar cerca
    </button>
 
    <button @click="$refs.campCerca.value = ''; $refs.campCerca.focus()">
        Netejar i enfocar
    </button>
</div>

Un cas d'ús habitual és amb diàlegs o modals, on necessites gestionar el focus per accessibilitat. Amb x-ref pots enfocar el primer camp d'un formulari quan el modal s'obre, o tornar el focus al botó que el va obrir quan es tanca.

Propietats màgiques#

Alpine.js inclou un conjunt de propietats i mètodes "màgics" que comencen amb el prefix $. Aquestes propietats estan disponibles automàticament dins de qualsevol expressió Alpine i proporcionen funcionalitats essencials que van més enllà del que ofereixen les directives per si soles.

$el fa referència a l'element DOM actual on s'avalua l'expressió. Això és útil quan necessites accedir a propietats de l'element en si, com les seves dimensions, posició o atributs personalitzats.

$refs és un objecte que conté tots els elements del component que tenen una directiva x-ref. Com hem vist a la secció anterior, permet accedir a qualsevol element referenciat dins del component.

$store dóna accés als stores globals d'Alpine, que veurem en detall a la secció següent. Permet compartir estat entre components que no tenen cap relació jeràrquica al DOM.

$watch permet observar una propietat i executar una funció cada vegada que canvia. A diferència d'x-effect, $watch et dóna accés tant al valor nou com a l'anterior:

<div x-data="{ quantitat: 1, preu: 10 }"
     x-init="
        $watch('quantitat', (nouValor, valorAnterior) => {
            console.log(`Quantitat canviada de ${valorAnterior} a ${nouValor}`);
        })
     ">
    <input type="number" x-model.number="quantitat">
    <p>Total: <span x-text="quantitat * preu"></span> EUR</p>
</div>

$dispatch és una eina per comunicar components Alpine entre si mitjançant esdeveniments personalitzats del DOM. Quan un component dispara un esdeveniment amb $dispatch, qualsevol altre component que estigui escoltant aquell esdeveniment pot reaccionar:

{{-- Component que dispara l'esdeveniment --}}
<div x-data>
    <button @click="$dispatch('notificacio', { missatge: 'Producte afegit al cistell!' })">
        Afegir al cistell
    </button>
</div>
 
{{-- Component que escolta l'esdeveniment --}}
<div x-data="{ missatge: '', visible: false }"
     @notificacio.window="missatge = $event.detail.missatge; visible = true;
                          setTimeout(() => visible = false, 3000)">
    <div x-show="visible" x-transition class="fixed top-4 right-4 p-4 bg-green-500 text-white rounded">
        <span x-text="missatge"></span>
    </div>
</div>

$nextTick espera que Alpine hagi acabat d'actualitzar el DOM i llavors executa el codi proporcionat. Això és necessari quan vols treballar amb el DOM actualitzat immediatament després de canviar una propietat reactiva, perquè les actualitzacions del DOM no són síncrones:

<div x-data="{ mostrar: false }">
    <button @click="mostrar = true; $nextTick(() => $refs.input.focus())">
        Obrir i enfocar
    </button>
 
    <div x-show="mostrar">
        <input type="text" x-ref="input">
    </div>
</div>

Sense $nextTick, el focus() podria executar-se abans que Alpine hagi renderitzat l'input al DOM, i per tant no tindria efecte.

$root retorna l'element arrel del component Alpine actual, és a dir, l'element que conté la directiva x-data. $data dóna accés a l'objecte de dades complet del component. I $id genera un identificador únic que és útil per vincular labels amb inputs de manera accessible:

<div x-data="{ nom: '' }">
    <label :for="$id('input')">Nom:</label>
    <input :id="$id('input')" type="text" x-model="nom">
</div>

Stores globals#

Quan necessites compartir estat entre diversos components Alpine que no estan niuats l'un dins l'altre, els stores globals són la solució. Un store és un objecte reactiu accessible des de qualsevol component Alpine de la pàgina a través de la propietat màgica $store.

Els stores es defineixen normalment al teu fitxer JavaScript principal, abans de cridar Alpine.start(). Pots crear tants stores com necessitis, cadascun amb el seu propi espai de noms:

// resources/js/app.js
import Alpine from 'alpinejs';
 
Alpine.store('modeFosc', {
    actiu: false,
 
    toggle() {
        this.actiu = !this.actiu;
        document.documentElement.classList.toggle('dark', this.actiu);
        localStorage.setItem('modeFosc', this.actiu);
    },
 
    init() {
        this.actiu = localStorage.getItem('modeFosc') === 'true';
        document.documentElement.classList.toggle('dark', this.actiu);
    },
});
 
Alpine.store('cistell', {
    articles: [],
 
    get total() {
        return this.articles.reduce((sum, article) => sum + article.preu * article.quantitat, 0);
    },
 
    get comptador() {
        return this.articles.reduce((sum, article) => sum + article.quantitat, 0);
    },
 
    afegir(producte) {
        const existent = this.articles.find(a => a.id === producte.id);
        if (existent) {
            existent.quantitat++;
        } else {
            this.articles.push({ ...producte, quantitat: 1 });
        }
    },
 
    eliminar(id) {
        this.articles = this.articles.filter(a => a.id !== id);
    },
});
 
window.Alpine = Alpine;
Alpine.start();

Un cop definits els stores, pots accedir-hi des de qualsevol component Alpine de qualsevol vista Blade. Això permet que la capçalera mostri el nombre d'articles al cistell mentre un botó a la pàgina de producte afegeixi articles, tot perfectament sincronitzat:

{{-- A la capçalera --}}
<div x-data>
    <span x-text="$store.cistell.comptador"></span> articles al cistell
    (<span x-text="$store.cistell.total.toFixed(2)"></span> EUR)
</div>
 
{{-- A un botó de mode fosc --}}
<div x-data>
    <button @click="$store.modeFosc.toggle()">
        <span x-show="!$store.modeFosc.actiu">Activar mode fosc</span>
        <span x-show="$store.modeFosc.actiu">Desactivar mode fosc</span>
    </button>
</div>
 
{{-- A una targeta de producte --}}
<div x-data="{ producte: { id: 1, nom: 'Llibre Laravel', preu: 29.99 } }">
    <h3 x-text="producte.nom"></h3>
    <p><span x-text="producte.preu"></span> EUR</p>
    <button @click="$store.cistell.afegir(producte)">Afegir al cistell</button>
</div>

Els stores globals són perfectes per a casos com el mode fosc o clar, preferències d'usuari, comptadors de notificacions, l'estat del cistell de compra, o qualsevol dada que necessiti ser consistent a tota la pàgina.

Plugins#

Alpine.js inclou un ecosistema de plugins oficials que amplien les seves funcionalitats per cobrir necessitats comunes. Cada plugin s'instal·la per separat i es registra abans d'inicialitzar Alpine.

Alpine Mask proporciona màscares d'entrada per a camps de formulari. Amb la directiva x-mask, pots forçar que l'usuari introdueixi dades en un format específic com números de telèfon, targetes de crèdit o dates. Això millora significativament l'experiència d'usuari perquè guia la introducció de dades:

npm install @alpinejs/mask
import Alpine from 'alpinejs';
import mask from '@alpinejs/mask';
 
Alpine.plugin(mask);
 
window.Alpine = Alpine;
Alpine.start();
<div x-data>
    <input type="text" x-mask="(+376) 999-999" placeholder="Telèfon Andorra">
    <input type="text" x-mask="9999 9999 9999 9999" placeholder="Targeta de crèdit">
    <input type="text" x-mask="99/99/9999" placeholder="Data (DD/MM/AAAA)">
</div>

Alpine Intersect afegeix la directiva x-intersect, que executa codi quan un element entra o surt del viewport. Utilitza l'API Intersection Observer del navegador i és ideal per implementar càrrega progressiva d'imatges, animacions d'entrada o scroll infinit:

npm install @alpinejs/intersect
<div x-data="{ visible: false }" x-intersect="visible = true">
    <div :class="{ 'opacity-100 translate-y-0': visible, 'opacity-0 translate-y-4': !visible }"
         class="transition duration-700">
        <p>Aquest contingut apareix amb animació quan és visible.</p>
    </div>
</div>

Alpine Persist permet emmagatzemar l'estat d'una propietat Alpine al localStorage del navegador, de manera que es manté entre càrregues de pàgina. Això és molt útil per a preferències d'usuari com el mode fosc, l'estat de col·lapse dels menús laterals o qualsevol configuració que l'usuari esperi que es recordi:

npm install @alpinejs/persist
<div x-data="{ barra: $persist(true) }">
    <button @click="barra = !barra">
        <span x-text="barra ? 'Amagar barra lateral' : 'Mostrar barra lateral'"></span>
    </button>
 
    <aside x-show="barra">
        Contingut de la barra lateral
    </aside>
</div>

Alpine Collapse proporciona transicions d'alçada suaus per a elements que s'expandeixen i es col·lapsen. A diferència d'x-show que simplement canvia display: none, x-collapse anima l'alçada de l'element des de 0 fins a la seva alçada natural, creant un efecte d'acordió molt polit:

npm install @alpinejs/collapse
<div x-data="{ obert: false }">
    <button @click="obert = !obert">Més informació</button>
 
    <div x-show="obert" x-collapse>
        <p class="p-4">
            Aquest contingut s'expandeix i es col·lapsa suaument
            amb una animació d'alçada. L'efecte és molt més natural
            que un simple show/hide.
        </p>
    </div>
</div>

Alpine Focus permet gestionar el focus dins d'un component, especialment el "focus trapping" que és essencial per a la accessibilitat en modals i diàlegs. Quan actives el focus trap, l'usuari no pot sortir del component amb la tecla Tab, cosa que és un requisit d'accessibilitat per a qualsevol modal:

npm install @alpinejs/focus
<div x-data="{ modalObert: false }">
    <button @click="modalObert = true">Obrir modal</button>
 
    <div x-show="modalObert" x-trap.noscroll="modalObert" class="fixed inset-0 flex items-center justify-center bg-black/50">
        <div class="bg-white p-8 rounded-lg max-w-md">
            <h2>Modal amb focus trap</h2>
            <input type="text" placeholder="Camp 1" class="block w-full mb-2 border p-2">
            <input type="text" placeholder="Camp 2" class="block w-full mb-2 border p-2">
            <button @click="modalObert = false">Tancar</button>
        </div>
    </div>
</div>

El modificador .noscroll impedeix que la pàgina de fons faci scroll mentre el modal està obert, millorant l'experiència d'usuari en dispositius mòbils.

Per registrar múltiples plugins alhora, simplement encadena les crides a Alpine.plugin() al teu fitxer JavaScript principal:

import Alpine from 'alpinejs';
import mask from '@alpinejs/mask';
import intersect from '@alpinejs/intersect';
import persist from '@alpinejs/persist';
import collapse from '@alpinejs/collapse';
import focus from '@alpinejs/focus';
 
Alpine.plugin(mask);
Alpine.plugin(intersect);
Alpine.plugin(persist);
Alpine.plugin(collapse);
Alpine.plugin(focus);
 
window.Alpine = Alpine;
Alpine.start();

Patrons comuns amb Blade#

A la pràctica, hi ha una sèrie de patrons d'interacció que es repeteixen en gairebé qualsevol aplicació web. Alpine.js permet implementar-los de manera elegant directament dins de les vistes Blade, sense necessitat de fitxers JavaScript separats. Vegem els patrons més habituals amb exemples complets.

Un modal complet necessita gestionar la visibilitat, un backdrop que cobreixi la pàgina, transicions suaus, el tancament amb la tecla Escape i el tancament en clicar fora del contingut del modal. Amb Alpine, tot això es pot aconseguir en poques línies:

<div x-data="{ obert: false }" @keydown.escape.window="obert = false">
    <button @click="obert = true"
            class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
        Obrir modal
    </button>
 
    {{-- Backdrop --}}
    <div x-show="obert"
         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"
         class="fixed inset-0 bg-black/50 z-40"
         @click="obert = false">
    </div>
 
    {{-- Contingut del modal --}}
    <div x-show="obert"
         x-transition:enter="transition ease-out duration-300"
         x-transition:enter-start="opacity-0 scale-95"
         x-transition:enter-end="opacity-100 scale-100"
         x-transition:leave="transition ease-in duration-200"
         x-transition:leave-start="opacity-100 scale-100"
         x-transition:leave-end="opacity-0 scale-95"
         class="fixed inset-0 z-50 flex items-center justify-center p-4"
         @click.self="obert = false">
        <div class="bg-white rounded-lg shadow-xl max-w-lg w-full p-6">
            <h2 class="text-xl font-bold mb-4">Confirmar acció</h2>
            <p class="text-gray-600 mb-6">
                Estàs segur que vols continuar amb aquesta acció?
                Aquesta operació no es pot desfer.
            </p>
            <div class="flex justify-end gap-3">
                <button @click="obert = false"
                        class="px-4 py-2 border rounded hover:bg-gray-50">
                    Cancel·lar
                </button>
                <button @click="obert = false"
                        class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
                    Confirmar
                </button>
            </div>
        </div>
    </div>
</div>

L'ús de @click.self al contenidor del modal és important: assegura que el clic es registra només si l'usuari clica directament al contenidor (el backdrop dins del z-50) i no a cap element fill. L'event @keydown.escape.window s'escolta a nivell de window perquè funcioni independentment de quin element tingui el focus.

Els menús desplegables són un dels patrons més comuns a qualsevol interfície web. El punt clau d'un bon dropdown és que es tanqui quan l'usuari clica fora, cosa que Alpine fa trivial amb el modificador .outside:

<div x-data="{ obert: false }" class="relative">
    <button @click="obert = !obert"
            class="flex items-center gap-2 px-4 py-2 border rounded">
        El meu compte
        <svg :class="{ 'rotate-180': obert }" class="w-4 h-4 transition-transform"
             fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
        </svg>
    </button>
 
    <div x-show="obert"
         @click.outside="obert = false"
         x-transition
         class="absolute right-0 mt-2 w-48 bg-white border rounded-lg shadow-lg py-1 z-50">
        <a href="/perfil" class="block px-4 py-2 hover:bg-gray-100">Perfil</a>
        <a href="/configuracio" class="block px-4 py-2 hover:bg-gray-100">Configuració</a>
        <hr class="my-1">
        <form method="POST" action="/logout">
            @csrf
            <button type="submit" class="block w-full text-left px-4 py-2 hover:bg-gray-100 text-red-600">
                Tancar sessió
            </button>
        </form>
    </div>
</div>

Tabs#

Les pestanyes són una manera excel·lent d'organitzar contingut relacionat sense necessitat de navegar a una altra pàgina. Amb Alpine, la lògica de les pestanyes és molt directa: una propietat controla quina pestanya està activa i es canvien les classes CSS i la visibilitat del contingut en conseqüència:

<div x-data="{ pestanya: 'general' }">
    <div class="flex border-b">
        <button @click="pestanya = 'general'"
                :class="pestanya === 'general'
                    ? 'border-b-2 border-blue-500 text-blue-600'
                    : 'text-gray-500 hover:text-gray-700'"
                class="px-6 py-3 font-medium">
            General
        </button>
        <button @click="pestanya = 'seguretat'"
                :class="pestanya === 'seguretat'
                    ? 'border-b-2 border-blue-500 text-blue-600'
                    : 'text-gray-500 hover:text-gray-700'"
                class="px-6 py-3 font-medium">
            Seguretat
        </button>
        <button @click="pestanya = 'notificacions'"
                :class="pestanya === 'notificacions'
                    ? 'border-b-2 border-blue-500 text-blue-600'
                    : 'text-gray-500 hover:text-gray-700'"
                class="px-6 py-3 font-medium">
            Notificacions
        </button>
    </div>
 
    <div class="p-6">
        <div x-show="pestanya === 'general'">
            <h3 class="text-lg font-bold mb-2">Configuració general</h3>
            <p>Nom, correu electrònic i preferències d'idioma.</p>
        </div>
        <div x-show="pestanya === 'seguretat'">
            <h3 class="text-lg font-bold mb-2">Seguretat</h3>
            <p>Contrasenya, autenticació de dos factors i sessions actives.</p>
        </div>
        <div x-show="pestanya === 'notificacions'">
            <h3 class="text-lg font-bold mb-2">Notificacions</h3>
            <p>Correu electrònic, push i preferències de freqüència.</p>
        </div>
    </div>
</div>

Accordion / FAQ#

Un acordió permet mostrar i amagar seccions de contingut de manera independent. És especialment útil per a pàgines de preguntes freqüents o seccions de documentació amb molts detalls. Combinat amb el plugin x-collapse, les transicions són suaus i naturals:

<div x-data="{ obert: null }" class="space-y-2">
    {{-- Pregunta 1 --}}
    <div class="border rounded-lg">
        <button @click="obert = obert === 1 ? null : 1"
                class="w-full flex justify-between items-center p-4 font-medium text-left">
            Què és Laravel?
            <svg :class="{ 'rotate-180': obert === 1 }" class="w-5 h-5 transition-transform"
                 fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
            </svg>
        </button>
        <div x-show="obert === 1" x-collapse>
            <div class="p-4 pt-0 text-gray-600">
                Laravel és un framework PHP de codi obert que segueix el patró
                Model-View-Controller. Està dissenyat per fer el desenvolupament
                web més ràpid i agradable.
            </div>
        </div>
    </div>
 
    {{-- Pregunta 2 --}}
    <div class="border rounded-lg">
        <button @click="obert = obert === 2 ? null : 2"
                class="w-full flex justify-between items-center p-4 font-medium text-left">
            Necessito saber PHP per utilitzar Laravel?
            <svg :class="{ 'rotate-180': obert === 2 }" class="w-5 h-5 transition-transform"
                 fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
            </svg>
        </button>
        <div x-show="obert === 2" x-collapse>
            <div class="p-4 pt-0 text-gray-600">
                Sí, Laravel és un framework PHP, de manera que és necessari
                tenir coneixements bàsics de PHP. Tanmateix, la sintaxi
                expressiva de Laravel fa que la corba d'aprenentatge sigui suau.
            </div>
        </div>
    </div>
 
    {{-- Pregunta 3 --}}
    <div class="border rounded-lg">
        <button @click="obert = obert === 3 ? null : 3"
                class="w-full flex justify-between items-center p-4 font-medium text-left">
            Alpine.js ve inclòs amb Laravel?
            <svg :class="{ 'rotate-180': obert === 3 }" class="w-5 h-5 transition-transform"
                 fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
            </svg>
        </button>
        <div x-show="obert === 3" x-collapse>
            <div class="p-4 pt-0 text-gray-600">
                Alpine.js no ve inclòs per defecte a Laravel, però és la recomanació
                oficial per afegir interactivitat lleugera a les vistes Blade.
                Es pot instal·lar amb una sola comanda npm.
            </div>
        </div>
    </div>
</div>

L'ús d'obert: null amb comparacions numèriques permet que només una secció estigui oberta a la vegada. Si volguessis que múltiples seccions poguessin estar obertes simultàniament, podries utilitzar un array o un objecte amb booleans per a cada secció.

Formulari dinàmic#

Els formularis dinàmics que mostren o amaguen camps segons les seleccions de l'usuari són un dels millors casos d'ús d'Alpine. En lloc de gestionar aquesta lògica al servidor amb recarregues de pàgina o escriure JavaScript complex, Alpine ho fa trivial:

<form x-data="{
    tipusCompte: 'personal',
    missatge: '',
    maxCaracters: 500,
    mostrarContrasenya: false,
    acceptaTermes: false
}" method="POST" action="/registre" class="max-w-lg mx-auto space-y-4">
    @csrf
 
    {{-- Selector de tipus de compte --}}
    <div>
        <label class="block font-medium mb-1">Tipus de compte</label>
        <select x-model="tipusCompte" class="w-full border rounded p-2">
            <option value="personal">Personal</option>
            <option value="empresa">Empresa</option>
            <option value="educacio">Educació</option>
        </select>
    </div>
 
    {{-- Camps comuns --}}
    <div>
        <label class="block font-medium mb-1">Nom complet</label>
        <input type="text" name="nom" class="w-full border rounded p-2" required>
    </div>
 
    {{-- Camp condicional: NIF empresa --}}
    <div x-show="tipusCompte === 'empresa'" x-transition>
        <label class="block font-medium mb-1">NIF de l'empresa</label>
        <input type="text" name="nif" class="w-full border rounded p-2">
    </div>
 
    {{-- Camp condicional: Centre educatiu --}}
    <div x-show="tipusCompte === 'educacio'" x-transition>
        <label class="block font-medium mb-1">Nom del centre educatiu</label>
        <input type="text" name="centre" class="w-full border rounded p-2">
    </div>
 
    {{-- Contrasenya amb toggle de visibilitat --}}
    <div>
        <label class="block font-medium mb-1">Contrasenya</label>
        <div class="relative">
            <input :type="mostrarContrasenya ? 'text' : 'password'"
                   name="password"
                   class="w-full border rounded p-2 pr-10">
            <button type="button"
                    @click="mostrarContrasenya = !mostrarContrasenya"
                    class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500">
                <span x-text="mostrarContrasenya ? 'Amagar' : 'Mostrar'"></span>
            </button>
        </div>
    </div>
 
    {{-- Textarea amb comptador de caràcters --}}
    <div>
        <label class="block font-medium mb-1">Missatge</label>
        <textarea x-model="missatge"
                  name="missatge"
                  :maxlength="maxCaracters"
                  class="w-full border rounded p-2"
                  rows="4"></textarea>
        <p class="text-sm mt-1"
           :class="missatge.length > maxCaracters * 0.9 ? 'text-red-600' : 'text-gray-500'">
            <span x-text="missatge.length"></span> / <span x-text="maxCaracters"></span> caràcters
        </p>
    </div>
 
    {{-- Acceptació de termes --}}
    <label class="flex items-center gap-2">
        <input type="checkbox" x-model="acceptaTermes">
        Accepto els termes i condicions
    </label>
 
    {{-- Botó d'enviament condicionat --}}
    <button type="submit"
            :disabled="!acceptaTermes"
            :class="acceptaTermes ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-300 cursor-not-allowed'"
            class="w-full py-2 text-white rounded font-medium transition-colors">
        Crear compte
    </button>
</form>

Fixa't en com el comptador de caràcters canvia de color quan s'apropa al límit, com els camps condicionals apareixen i desapareixen amb transicions suaus segons el tipus de compte seleccionat, i com el botó d'enviament està desactivat fins que l'usuari accepta els termes. Tot això sense escriure ni una sola línia de JavaScript en un fitxer separat.

Integració amb Blade components#

Una de les combinacions més potents de Laravel és utilitzar components Blade que encapsulen comportament Alpine al seu interior. Això permet crear elements d'interfície reutilitzables que són interactius per defecte.

El punt clau de la integració és com passes dades de Blade (que viu al servidor) a Alpine (que viu al client). La tècnica més habitual és utilitzar les funcions json_encode o la directiva @js de Blade per convertir variables PHP a JSON dins d'x-data:

{{-- Passant dades de Blade a Alpine --}}
<div x-data="{ productes: {{ Js::from($productes) }} }">
    <template x-for="producte in productes" :key="producte.id">
        <div class="p-4 border rounded mb-2">
            <h3 x-text="producte.nom" class="font-bold"></h3>
            <p x-text="producte.descripcio" class="text-gray-600"></p>
            <span x-text="producte.preu + ' EUR'" class="text-green-600 font-bold"></span>
        </div>
    </template>
</div>

La funció Js::from() de Laravel és la manera recomanada de serialitzar dades PHP a JavaScript dins de Blade. Gestiona correctament l'escapat de caràcters especials i produeix JSON vàlid que Alpine pot interpretar sense problemes.

Pots crear components Blade anònims que encapsulin comportament Alpine, fent-los reutilitzables a tota l'aplicació. Per exemple, un component de modal reutilitzable:

{{-- resources/views/components/modal.blade.php --}}
@props(['titol' => 'Modal', 'id' => 'modal'])
 
<div x-data="{ obert: false }"
     x-on:obrir-{{ $id }}.window="obert = true"
     x-on:tancar-{{ $id }}.window="obert = false"
     @keydown.escape.window="obert = false">
 
    {{-- Trigger --}}
    <div @click="obert = true">
        {{ $trigger ?? '' }}
    </div>
 
    {{-- Modal --}}
    <div x-show="obert" class="fixed inset-0 z-50 flex items-center justify-center">
        <div x-show="obert" x-transition:enter="ease-out duration-300"
             x-transition:enter-start="opacity-0"
             x-transition:enter-end="opacity-100"
             x-transition:leave="ease-in duration-200"
             x-transition:leave-start="opacity-100"
             x-transition:leave-end="opacity-0"
             class="fixed inset-0 bg-black/50"
             @click="obert = false"></div>
 
        <div x-show="obert" x-transition:enter="ease-out duration-300"
             x-transition:enter-start="opacity-0 scale-95"
             x-transition:enter-end="opacity-100 scale-100"
             class="relative bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6 z-10">
            <h2 class="text-xl font-bold mb-4">{{ $titol }}</h2>
            {{ $slot }}
            <button @click="obert = false" class="mt-4 text-gray-500 hover:text-gray-700">
                Tancar
            </button>
        </div>
    </div>
</div>

Ara pots utilitzar aquest component a qualsevol vista:

<x-modal titol="Confirmar eliminació" id="eliminar">
    <x-slot:trigger>
        <button class="px-4 py-2 bg-red-600 text-white rounded">Eliminar</button>
    </x-slot:trigger>
 
    <p>Estàs segur que vols eliminar aquest element? Aquesta acció no es pot desfer.</p>
    <div class="flex gap-3 mt-4">
        <button @click="$dispatch('tancar-eliminar')"
                class="px-4 py-2 border rounded">Cancel·lar</button>
        <form method="POST" action="/eliminar">
            @csrf
            @method('DELETE')
            <button type="submit" class="px-4 py-2 bg-red-600 text-white rounded">Sí, eliminar</button>
        </form>
    </div>
</x-modal>

Aquesta aproximació encapsula tota la lògica de visibilitat, transicions i tancament dins del component, de manera que qualsevol desenvolupador de l'equip pot crear modals sense preocupar-se de la implementació subjacent. L'ús d'esdeveniments personalitzats amb $dispatch permet obrir o tancar el modal des de qualsevol punt de la pàgina.

Els components Blade amb Alpine també es poden utilitzar per a elements més petits, com botons de toggle, tooltips, alertes descartables o qualsevol peça d'interfície que necessiti un mínim de comportament interactiu.

Alpine.js vs Livewire#

Entendre la diferència entre Alpine.js i Livewire és fonamental per prendre bones decisions arquitecturals als teus projectes Laravel. Tot i que tots dos comparteixen creador i filosofia, resolen problemes molt diferents.

Alpine.js opera exclusivament al client. Tot el seu codi s'executa al navegador de l'usuari, sense cap comunicació amb el servidor. Això el fa perfecte per a interaccions purament visuals: mostrar i amagar elements, canviar pestanyes, validar formularis al client, animar elements, gestionar desplegables i modals. Aquestes interaccions són immediates perquè no depenen de cap petició HTTP.

Livewire, en canvi, implica comunicació amb el servidor a cada interacció. Quan un usuari interactua amb un component Livewire, s'envia una petició AJAX al servidor, es processa la lògica PHP, i el servidor retorna l'HTML actualitzat que Livewire injecta al DOM. Això el fa ideal per a interaccions que necessiten accés a dades del servidor: cerques en temps real, filtrats, CRUD, actualitzacions que depenen de l'estat de la base de dades o lògica de negoci complexa.

La bona notícia és que no has de triar entre un o l'altre. Livewire inclou Alpine.js per defecte, i tots dos estan dissenyats per treballar junts de manera harmoniosa. La regla pràctica és senzilla: utilitza Livewire per a tot allò que necessiti dades del servidor, i utilitza Alpine per a la capa d'interacció visual que no requereix cap petició:

{{-- Livewire gestiona la cerca (necessita el servidor) --}}
{{-- Alpine gestiona el dropdown de filtres (purament visual) --}}
<div>
    {{-- Component Livewire per a la cerca --}}
    <div>
        <input wire:model.live="cerca" type="text" placeholder="Cercar productes...">
    </div>
 
    {{-- Alpine per al desplegable de filtres --}}
    <div x-data="{ filtresOberts: false }" class="relative">
        <button @click="filtresOberts = !filtresOberts">
            Filtres
        </button>
        <div x-show="filtresOberts" @click.outside="filtresOberts = false" x-transition
             class="absolute mt-2 p-4 bg-white border rounded shadow-lg">
            <label class="block">
                <input type="checkbox" wire:model.live="filtreActiu"> Només actius
            </label>
            <label class="block">
                <input type="checkbox" wire:model.live="filtreDestacats"> Només destacats
            </label>
        </div>
    </div>
 
    {{-- Resultats (Livewire) --}}
    @foreach($productes as $producte)
        <div class="p-4 border-b">{{ $producte->nom }}</div>
    @endforeach
</div>

En aquest exemple, el dropdown de filtres s'obre i es tanca amb Alpine (interacció visual instantània), mentre que els checkboxes dins del dropdown estan connectats a Livewire (actualitzen les dades al servidor). Cada eina fa el que millor sap fer, i el resultat és una interfície ràpida i fluida.

En resum, pensa en Alpine com la capa de "maquillatge" de la teva interfície: dropdowns, modals, animacions, toggles, validació visual. I pensa en Livewire com el "cervell" darrere de les interaccions que necessiten dades reals: cerques, filtrats, formularis que desen al servidor, actualitzacions en temps real. Junts, cobreixen pràcticament qualsevol necessitat d'interactivitat sense haver de recórrer a un framework JavaScript complet.