Alpine.js
This content is not available in your language yet.
🏔️ Qu’est-ce qu’Alpine.js ?
Section intitulée « 🏔️ Qu’est-ce qu’Alpine.js ? »Alpine.js est un framework JavaScript minimaliste (~15kb gzippé) qui apporte la réactivité de Vue.js directement dans votre HTML. C’est l’outil parfait pour ajouter de l’interactivité sans la complexité d’un framework full-stack.
🆚 Alpine.js vs autres frameworks
Section intitulée « 🆚 Alpine.js vs autres frameworks »| Framework | Taille | Philosophie | Cas d’usage |
|---|---|---|---|
| Alpine.js | 15 KB | Déclaratif dans HTML | Interactivité légère, composants |
| jQuery | 87 KB | Impératif, manipulation DOM | Legacy, événements simples |
| React | 44 KB + | Component-based, Virtual DOM | SPA, apps complexes |
| Vue.js | 34 KB + | Component-based, réactif | SPA, apps moyennes-complexes |
| Vanilla JS | 0 KB | Natif navigateur | Micro-interactions |
✅ Pourquoi Alpine.js pour Drupal ?
Section intitulée « ✅ Pourquoi Alpine.js pour Drupal ? »Avantages spécifiques :
- ✅ Taille minimale : 15 KB vs 87 KB jQuery (déjà inclus dans Drupal)
- ✅ Pas de build : Fonctionne directement en CDN ou via Vite
- ✅ Syntaxe déclarative :
x-data,x-show,x-ondans le HTML - ✅ Réactivité automatique : Les données se synchronisent automatiquement
- ✅ Parfait avec Twig : Pas de conflit avec les templates Drupal
- ✅ Composants réutilisables : via
Alpine.data()etAlpine.store() - ✅ Compatible Tailwind : Même philosophie utility-first
Cas d’usage dans TailStore :
- 🛒 Panier e-commerce avec LocalStorage
- 🎨 Filtres produits dynamiques
- 📱 Menu mobile avec drawer
- 🖼️ Galeries images interactives
- 🔔 Notifications toast
- ✅ Formulaires avec validation
🏗️ Architecture Alpine.js + Drupal
Section intitulée « 🏗️ Architecture Alpine.js + Drupal »┌─────────────────────────────────────────┐│ Templates Twig (.html.twig) ││ Directives Alpine (x-data, x-on) │└──────────────┬──────────────────────────┘ │ ↓┌─────────────────────────────────────────┐│ Alpine.js (assets/main.js) ││ - Alpine.store (état global) ││ - Alpine.data (composants) │└──────────────┬──────────────────────────┘ │ ↓┌─────────────────────────────────────────┐│ LocalStorage / SessionStorage ││ (persistance données) │└─────────────────────────────────────────┘🎯 Alpine.js vs jQuery dans Drupal
Section intitulée « 🎯 Alpine.js vs jQuery dans Drupal »Approche jQuery (ancienne) :
// Impératif, verbeux(function ($, Drupal) { Drupal.behaviors.myFeature = { attach: function (context) { $('.btn', context).once('myFeature').on('click', function () { var count = parseInt($('.count').text()); count++; $('.count').text(count); }); } };})(jQuery, Drupal);Approche Alpine.js (moderne) :
<!-- Déclaratif, concis --><div x-data="{ count: 0 }"> <button @click="count++" class="btn">Increment</button> <span x-text="count" class="count"></span></div>📦 Installation
Section intitulée « 📦 Installation »Option 1 : CDN (simple)
Section intitulée « Option 1 : CDN (simple) »# tailstore.libraries.ymlalpine: version: 3.x header: true js: https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js: type: external minified: true attributes: defer: true
global: version: 1.0 css: theme: dist/assets/main.css: { minified: true } dependencies: - tailstore/alpineOption 2 : npm (recommandé)
Section intitulée « Option 2 : npm (recommandé) »cd themes/custom/tailstorenpm install alpinejsCréez assets/main.js :
// assets/main.jsimport './style.css'import Alpine from 'alpinejs';
// Plugins optionnels// import collapse from '@alpinejs/collapse';// import focus from '@alpinejs/focus';
// Alpine.plugin(collapse);// Alpine.plugin(focus);
// Rendre Alpine accessible globalementwindow.Alpine = Alpine;
// Démarrer AlpineAlpine.start();Vite gère automatiquement le bundling :
{ "scripts": { "dev": "vite", "build": "vite build" }}Déclarez dans libraries.yml :
global: version: 1.0 css: theme: dist/assets/main.css: { minified: true } js: dist/assets/main.js: { minified: true }🎯 Exercices pratiques
Section intitulée « 🎯 Exercices pratiques »Exercice 1 : Premier composant Alpine (compteur)
Section intitulée « Exercice 1 : Premier composant Alpine (compteur) »Objectif : Comprendre x-data, x-text, et @click.
-
Créez un template de test
test-alpine.html.twig:<div class="p-8"><div x-data="{ count: 0 }" class="text-center"><h2 class="text-2xl font-bold mb-4">Compteur Alpine.js</h2><p class="text-4xl mb-4" x-text="count"></p><div class="flex gap-2 justify-center"><button @click="count--" class="btn btn-secondary">-</button><button @click="count = 0" class="btn btn-outline">Reset</button><button @click="count++" class="btn btn-primary">+</button></div></div></div> -
Vérifiez qu’Alpine.js est chargé (F12 → Console) :
typeof Alpine !== 'undefined' // Doit retourner true -
Testez le compteur : les clics doivent mettre à jour le nombre
Validation : ✅ Le compteur s’incrémente/décrémente au clic
Exercice 2 : Toggle avec x-show
Section intitulée « Exercice 2 : Toggle avec x-show »Objectif : Maîtriser x-show et les transitions.
-
Créez un composant accordéon :
<div x-data="{ open: false }" class="border rounded-lg p-4"><button@click="open = !open"class="flex items-center justify-between w-full font-semibold"><span>Détails du produit</span><svgclass="w-5 h-5 transition-transform":class="{ 'rotate-180': open }"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><divx-show="open"x-transition:enter="transition ease-out duration-200"x-transition:enter-start="opacity-0 -translate-y-2"x-transition:enter-end="opacity-100 translate-y-0"x-transition:leave="transition ease-in duration-150"x-transition:leave-start="opacity-100 translate-y-0"x-transition:leave-end="opacity-0 -translate-y-2"class="mt-4 text-gray-600"><p>Contenu qui apparaît/disparaît avec animation.</p></div></div> -
Testez l’accordéon : le contenu doit apparaître avec animation
Validation : ✅ Le contenu apparaît/disparaît avec transition fluide
Exercice 3 : Boucle avec x-for
Section intitulée « Exercice 3 : Boucle avec x-for »Objectif : Afficher une liste dynamique avec x-for.
-
Créez une liste de tâches simple :
<divx-data="{newTask: '',tasks: ['Créer le thème', 'Ajouter Tailwind', 'Intégrer Alpine']}"class="p-8 max-w-md mx-auto"><h2 class="text-2xl font-bold mb-4">Todo List</h2><form @submit.prevent="tasks.push(newTask); newTask = ''" class="mb-4"><div class="flex gap-2"><inputx-model="newTask"type="text"placeholder="Nouvelle tâche..."class="form-input flex-1"><button type="submit" class="btn btn-primary">Ajouter</button></div></form><ul class="space-y-2"><template x-for="(task, index) in tasks" :key="index"><li class="flex items-center justify-between p-3 bg-gray-50 rounded"><span x-text="task"></span><button@click="tasks.splice(index, 1)"class="text-red-500 hover:text-red-700">✕</button></li></template></ul></div> -
Testez :
- Ajoutez des tâches
- Supprimez des tâches
- Vérifiez que tout est réactif
Validation : ✅ Les tâches s’ajoutent/suppriment instantanément
Exercice 4 : Store global (panier simple)
Section intitulée « Exercice 4 : Store global (panier simple) »Objectif : Utiliser Alpine.store() pour un état partagé.
-
Dans
assets/main.js, ajoutez un store :import Alpine from 'alpinejs';// Store panierAlpine.store('cart', {items: [],add(product) {this.items.push(product);},remove(index) {this.items.splice(index, 1);},get count() {return this.items.length;}});window.Alpine = Alpine;Alpine.start(); -
Créez un bouton “Ajouter au panier” :
<div x-data="{ product: { id: 1, name: 'T-Shirt', price: 29.99 } }"><button@click="$store.cart.add(product)"class="btn btn-primary">Ajouter au panier</button></div> -
Affichez le compteur du panier :
<div x-data><button class="relative btn btn-outline">🛒 Panier<spanx-show="$store.cart.count > 0"x-text="$store.cart.count"class="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center"></span></button></div> -
Testez : ajoutez des produits et vérifiez que le compteur se met à jour
Validation : ✅ Le compteur du panier se met à jour en temps réel
🛒 Composants TailStore
Section intitulée « 🛒 Composants TailStore »Panier (Cart)
Section intitulée « Panier (Cart) »// assets/stores/cart.jsdocument.addEventListener('alpine:init', () => { Alpine.store('cart', { items: JSON.parse(localStorage.getItem('cart') || '[]'),
get count() { return this.items.reduce((sum, item) => sum + item.quantity, 0); },
get total() { return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0); },
add(product) { const existing = this.items.find(i => i.id === product.id);
if (existing) { existing.quantity++; } else { this.items.push({ ...product, quantity: 1 }); }
this.save(); this.notify(`${product.name} ajouté au panier`); },
remove(productId) { this.items = this.items.filter(i => i.id !== productId); this.save(); },
updateQuantity(productId, quantity) { const item = this.items.find(i => i.id === productId); if (item) { item.quantity = Math.max(1, quantity); this.save(); } },
clear() { this.items = []; this.save(); },
save() { localStorage.setItem('cart', JSON.stringify(this.items)); },
notify(message) { // Dispatch event pour les toasts window.dispatchEvent(new CustomEvent('toast', { detail: { message, type: 'success' } })); } });});Template bouton “Ajouter au panier”
Section intitulée « Template bouton “Ajouter au panier” »{# node--product--card.html.twig #}<article class="card group" x-data="{ product: { id: {{ node.id }}, name: '{{ label|escape('js') }}', price: {{ node.field_price.value }}, image: '{{ file_url(node.field_images.0.entity.fileuri) }}' } }"> {# ... image et infos ... #}
<button @click="$store.cart.add(product)" class="btn btn-primary w-full" > <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/> </svg> Ajouter au panier </button></article>Mini-cart header
Section intitulée « Mini-cart header »{# block--mini-cart.html.twig #}<div x-data="{ open: false }" class="relative"> {# Trigger #} <button @click="open = !open" class="relative p-2 text-gray-600 hover:text-primary transition-colors" > <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/> </svg>
{# Badge count #} <span x-show="$store.cart.count > 0" x-text="$store.cart.count" class="absolute -top-1 -right-1 w-5 h-5 bg-primary text-white text-xs rounded-full flex items-center justify-center" ></span> </button>
{# Dropdown #} <div x-show="open" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-1" @click.outside="open = false" class="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-xl z-50" > <div class="p-4 border-b"> <h3 class="font-semibold">Votre panier</h3> </div>
<template x-if="$store.cart.items.length === 0"> <div class="p-8 text-center text-gray-500"> Votre panier est vide </div> </template>
<template x-if="$store.cart.items.length > 0"> <div> <ul class="divide-y max-h-64 overflow-y-auto"> <template x-for="item in $store.cart.items" :key="item.id"> <li class="p-4 flex gap-4"> <img :src="item.image" class="w-16 h-16 object-cover rounded"> <div class="flex-1 min-w-0"> <p x-text="item.name" class="font-medium truncate"></p> <p class="text-sm text-gray-500"> <span x-text="item.quantity"></span> × <span x-text="item.price.toFixed(2) + ' €'"></span> </p> </div> <button @click="$store.cart.remove(item.id)" class="text-gray-400 hover:text-red-500" > ✕ </button> </li> </template> </ul>
<div class="p-4 border-t bg-gray-50"> <div class="flex justify-between font-semibold mb-4"> <span>Total</span> <span x-text="$store.cart.total.toFixed(2) + ' €'"></span> </div> <a href="/checkout" class="btn btn-primary w-full"> Commander </a> </div> </div> </template> </div></div>Menu Mobile
Section intitulée « Menu Mobile »{# block--mobile-menu.html.twig #}<div x-data="{ open: false }" @toggle-mobile-menu.window="open = !open"> {# Overlay #} <div x-show="open" x-transition:enter="transition-opacity ease-linear duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition-opacity ease-linear duration-300" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="open = false" class="fixed inset-0 bg-black/50 z-40 md:hidden" ></div>
{# Drawer #} <aside x-show="open" x-transition:enter="transform transition ease-in-out duration-300" x-transition:enter-start="-translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transform transition ease-in-out duration-300" x-transition:leave-start="translate-x-0" x-transition:leave-end="-translate-x-full" class="fixed top-0 left-0 bottom-0 w-72 bg-white z-50 overflow-y-auto md:hidden" > <div class="p-4 border-b flex justify-between items-center"> <span class="font-bold text-xl">Menu</span> <button @click="open = false" class="p-2 hover:bg-gray-100 rounded"> ✕ </button> </div>
<nav class="p-4"> {{ content }} </nav> </aside></div>Sélecteur de quantité
Section intitulée « Sélecteur de quantité »{# field--field-quantity.html.twig #}<div x-data="{ quantity: 1 }" class="flex items-center border rounded-lg overflow-hidden w-fit"> <button @click="quantity = Math.max(1, quantity - 1)" class="px-3 py-2 bg-gray-100 hover:bg-gray-200 transition-colors" >−</button>
<input type="number" x-model.number="quantity" min="1" class="w-16 text-center border-0 focus:ring-0" >
<button @click="quantity++" class="px-3 py-2 bg-gray-100 hover:bg-gray-200 transition-colors" >+</button></div>Galerie produit
Section intitulée « Galerie produit »{# field--field-images--full.html.twig #}<div x-data="{ active: 0, images: {{ images|json_encode|raw }} }" class="grid gap-4"> {# Main image #} <div class="aspect-square overflow-hidden rounded-lg bg-gray-100"> <template x-for="(image, index) in images" :key="index"> <img x-show="active === index" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" :src="image.url" :alt="image.alt" class="w-full h-full object-cover" > </template> </div>
{# Thumbnails #} <div class="grid grid-cols-5 gap-2"> <template x-for="(image, index) in images" :key="index"> <button @click="active = index" :class="{ 'ring-2 ring-primary': active === index }" class="aspect-square overflow-hidden rounded-lg" > <img :src="image.url" :alt="image.alt" class="w-full h-full object-cover" > </button> </template> </div></div>Système de Toast notifications
Section intitulée « Système de Toast notifications »{# block--toasts.html.twig #}<div x-data="{ toasts: [], add(message, type = 'success') { const id = Date.now(); this.toasts.push({ id, message, type }); setTimeout(() => this.remove(id), 5000); }, remove(id) { this.toasts = this.toasts.filter(t => t.id !== id); } }" @toast.window="add($event.detail.message, $event.detail.type)" class="fixed bottom-4 right-4 z-50 space-y-2"> <template x-for="toast in toasts" :key="toast.id"> <div x-show="true" x-transition:enter="transform ease-out duration-300" x-transition:enter-start="translate-x-full opacity-0" x-transition:enter-end="translate-x-0 opacity-100" x-transition:leave="transform ease-in duration-200" x-transition:leave-start="translate-x-0 opacity-100" x-transition:leave-end="translate-x-full opacity-0" :class="{ 'bg-green-500': toast.type === 'success', 'bg-red-500': toast.type === 'error', 'bg-blue-500': toast.type === 'info' }" class="px-4 py-3 rounded-lg text-white shadow-lg flex items-center gap-2" > <span x-text="toast.message"></span> <button @click="remove(toast.id)" class="ml-2 hover:opacity-75">✕</button> </div> </template></div>🔗 Communication avec Drupal via htmx
Section intitulée « 🔗 Communication avec Drupal via htmx »Alpine.js gère l’UI, htmx gère les requêtes AJAX :
<div x-data="{ loading: false }" @htmx:before-request="loading = true" @htmx:after-request="loading = false"> <button hx-post="/api/cart/add" hx-vals='{"product_id": "{{ node.id }}"}' hx-swap="none" class="btn btn-primary" :class="{ 'opacity-50 cursor-wait': loading }" :disabled="loading" > <span x-show="!loading">Ajouter au panier</span> <span x-show="loading">Chargement...</span> </button></div>🧪 Debug Alpine
Section intitulée « 🧪 Debug Alpine »Ajoutez dans votre code pour debug :
<div x-data="{ items: [] }"> {# Debug panel #} <pre x-show="false" x-text="JSON.stringify($data, null, 2)"></pre></div>Ou utilisez l’extension navigateur Alpine.js devtools.
🧠 Bonnes pratiques essentielles
Section intitulée « 🧠 Bonnes pratiques essentielles »⚡ Performance
Section intitulée « ⚡ Performance »1. Lazy loading des stores
// ❌ Mauvais - Charge tout au démarrageAlpine.store('products', { items: await fetchAllProducts() // Bloquant});
// ✅ Bon - Charge à la demandeAlpine.store('products', { items: [], async load() { if (this.items.length === 0) { this.items = await fetchAllProducts(); } }});2. Utiliser x-cloak pour éviter le flash
/* Dans votre CSS */[x-cloak] { display: none !important;}<!-- Évite le flash de contenu non-initialisé --><div x-data="{ loaded: false }" x-cloak> <div x-show="loaded"> <!-- Contenu --> </div></div>3. Éviter les x-data trop volumineux
<!-- ❌ Mauvais - Trop de logique dans x-data --><div x-data="{ products: [], filters: {}, sort: 'name', async loadProducts() { /* 50 lignes */ }, filterProducts() { /* 30 lignes */ }, sortProducts() { /* 20 lignes */ }}">
<!-- ✅ Bon - Extraire dans Alpine.data() --><div x-data="productList">// assets/components/productList.jsAlpine.data('productList', () => ({ products: [], filters: {}, sort: 'name',
async loadProducts() { // Logique claire et testable },
filterProducts() { // ... }}));🔒 Sécurité
Section intitulée « 🔒 Sécurité »1. Échapper les données Drupal
<!-- ❌ Dangereux - XSS possible --><div x-data="{ name: '{{ node.title }}' }">
<!-- ✅ Sécurisé - Échappement JavaScript --><div x-data="{ name: '{{ node.title|escape('js') }}' }">
<!-- ✅ Encore mieux - JSON encode --><div x-data="{ product: {{ product|json_encode|raw }} }">2. Valider les inputs
Alpine.store('cart', { add(product) { // ✅ Validation if (!product?.id || !product?.price) { console.error('Invalid product'); return; }
// ✅ Sanitize this.items.push({ id: parseInt(product.id), name: String(product.name).slice(0, 100), price: parseFloat(product.price) }); }});3. Limiter les actions sensibles
<!-- ❌ Permet suppression directe --><button @click="$store.cart.items = []">Vider</button>
<!-- ✅ Confirmation requise --><button @click="confirm('Vider le panier ?') && $store.cart.clear()">Vider</button>📝 Conventions de code
Section intitulée « 📝 Conventions de code »1. Nommage des directives
<!-- Préfixer les handlers avec 'on' ou 'handle' --><button @click="handleAddToCart(product)">Ajouter</button><form @submit.prevent="onSubmitForm">...</form>
<!-- Nommer les toggles avec 'is' ou 'show' --><div x-data="{ isOpen: false, showModal: false }">2. Organisation des fichiers
assets/├── main.js # Point d'entrée├── style.css # Styles globaux├── stores/ # États globaux│ ├── cart.js│ ├── wishlist.js│ └── filters.js└── components/ # Composants Alpine.data() ├── productCard.js ├── productGallery.js └── mobileMenu.js3. Commenter les composants complexes
/** * Product Gallery Component * * Gère l'affichage d'images produit avec thumbnails. * Supporte clavier (←/→) et swipe mobile. * * @usage <div x-data="productGallery" x-init="init({{ images|json_encode }})"> */Alpine.data('productGallery', () => ({ active: 0, images: [],
init(images) { this.images = images; this.setupKeyboard(); },
// ...}));♿ Accessibilité
Section intitulée « ♿ Accessibilité »1. Gérer le focus
Alpine.data('modal', () => ({ open: false,
show() { this.open = true; // Focus sur le premier élément this.$nextTick(() => { this.$refs.closeButton.focus(); }); },
hide() { this.open = false; // Retour au trigger this.$refs.trigger.focus(); }}));<div x-data="modal"> <button @click="show" x-ref="trigger">Ouvrir</button>
<div x-show="open" @keydown.escape="hide"> <button @click="hide" x-ref="closeButton">✕</button> </div></div>2. Attributs ARIA
<!-- Menu déroulant accessible --><div x-data="{ open: false }"> <button @click="open = !open" :aria-expanded="open" aria-controls="menu" aria-haspopup="true" > Menu </button>
<div x-show="open" id="menu" role="menu" @click.outside="open = false" > <a role="menuitem" href="/products">Produits</a> </div></div>3. Annoncer les changements
<div x-data="{ count: 0 }"> <button @click="count++; $dispatch('announce', `${count} produits`)">+</button>
<!-- Zone ARIA live --> <div @announce.window="$el.textContent = $event.detail" aria-live="polite" aria-atomic="true" class="sr-only" ></div></div>🚨 Erreurs courantes et solutions
Section intitulée « 🚨 Erreurs courantes et solutions »Erreur 1 : “Alpine is not defined”
Section intitulée « Erreur 1 : “Alpine is not defined” »Symptôme : Console JavaScript affiche l’erreur
Causes :
- Alpine.js non chargé
- Script chargé après l’utilisation
- Conflit de versions
Solution :
# tailstore.libraries.yml - Vérifier l'ordreglobal: js: dist/assets/main.js: {} # Alpine doit être dans main.js dependencies: - core/drupal// main.js - Vérifier l'initialisationimport Alpine from 'alpinejs';
window.Alpine = Alpine; // ✅ Exposer globalementAlpine.start(); // ✅ Démarrer AlpineErreur 2 : x-data ne fonctionne pas
Section intitulée « Erreur 2 : x-data ne fonctionne pas »Symptôme : Les directives Alpine sont ignorées
Cause : Alpine démarré avant le DOM
Solution :
// ❌ MauvaisAlpine.start();document.addEventListener('DOMContentLoaded', () => { // Trop tard});
// ✅ Bonimport Alpine from 'alpinejs';
// Enregistrer stores et componentsAlpine.store('cart', { /* ... */ });
// Démarrer (Alpine attend automatiquement le DOM)Alpine.start();Erreur 3 : x-for ne met pas à jour la liste
Section intitulée « Erreur 3 : x-for ne met pas à jour la liste »Symptôme : Items ajoutés mais pas affichés
Cause : Mutation d’array sans réactivité
Solution :
<!-- ❌ Mauvais - Mutation directe --><button @click="items[0] = newItem">Update</button>
<!-- ✅ Bon - Remplacement complet --><button @click="items = [...items.slice(0, -1), newItem]">Update</button>
<!-- ✅ Ou utiliser des méthodes réactives --><button @click="items.push(newItem)">Add</button><button @click="items.splice(index, 1)">Remove</button>Erreur 4 : Store non partagé entre composants
Section intitulée « Erreur 4 : Store non partagé entre composants »Symptôme : Modifications dans un composant n’apparaissent pas ailleurs
Cause : Store non enregistré globalement
Solution :
// ❌ Mauvais - Store localAlpine.data('cart', () => ({ items: [] // Chaque instance a sa propre copie}));
// ✅ Bon - Store globalAlpine.store('cart', { items: [] // Partagé entre tous les composants});Erreur 5 : Données perdues au refresh
Section intitulée « Erreur 5 : Données perdues au refresh »Symptôme : Panier vidé au rechargement de page
Cause : Pas de persistance
Solution :
// Option 1 : Plugin persist (recommandé)import persist from '@alpinejs/persist';Alpine.plugin(persist);
Alpine.store('cart', { items: Alpine.$persist([]).as('cart_items')});
// Option 2 : Manuel avec localStorageAlpine.store('cart', { items: JSON.parse(localStorage.getItem('cart') || '[]'),
add(item) { this.items.push(item); this.save(); },
save() { localStorage.setItem('cart', JSON.stringify(this.items)); }});Erreur 6 : Transitions saccadées
Section intitulée « Erreur 6 : Transitions saccadées »Symptôme : Animations ne sont pas fluides
Cause : Propriétés non optimisées pour GPU
Solution :
/* ❌ Mauvais - Pas optimisé GPU */.dropdown { transition: height 0.3s;}
/* ✅ Bon - GPU accelerated */.dropdown { transition: transform 0.3s, opacity 0.3s; will-change: transform, opacity;}<!-- Utiliser les transitions Alpine optimisées --><div x-show="open" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100">📚 Ressources complémentaires
Section intitulée « 📚 Ressources complémentaires »Documentation officielle
Section intitulée « Documentation officielle »- Alpine.js Official Docs - Documentation complète
- Alpine.js Essentials - Guide de démarrage
- Directives Reference - Toutes les directives
- Magics Reference -
$el,$refs,$store, etc. - Plugins - Persist, Focus, Collapse, etc.
Plugins utiles
Section intitulée « Plugins utiles »npm install @alpinejs/persist # Persistance localStoragenpm install @alpinejs/focus # Gestion focusnpm install @alpinejs/collapse # Animations collapsenpm install @alpinejs/intersect # Intersection ObserverComposants open-source
Section intitulée « Composants open-source »- Alpine Components - Collection officielle
- Alpine Toolbox - Composants communautaires
- Alpine UI - Composants e-commerce
Intégrations
Section intitulée « Intégrations »- Alpine + Tailwind - États dynamiques
- Alpine + htmx - Requêtes AJAX
- Alpine + Drupal - Module Drupal (optionnel)
✅ Checklist
Section intitulée « ✅ Checklist »- Alpine.js installé (CDN ou npm)
- Store panier implémenté
- Mini-cart avec dropdown
- Menu mobile avec drawer
- Galerie produit interactive
- Toasts notifications
- Debug configuré
🔜 Prochaine étape
Section intitulée « 🔜 Prochaine étape »L’interactivité est en place ! Passons aux exercices pour mettre tout en pratique.