🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Alpine.js
Exemples du framework Alpine.js incluant les composants réactifs, la liaison de données et les patterns Alpine.js modernes
💻 Alpine.js Hello World html
🟢 simple
⭐
Configuration de base Alpine.js et exemples Hello World avec des données réactives
⏱️ 15 min
🏷️ alpinejs, reactive, components
Prerequisites:
Basic HTML knowledge, JavaScript basics
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine.js Hello World</title>
<!-- Alpine.js -->
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
background: white;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0056b3;
}
.highlight {
background: #fff3cd;
padding: 1rem;
border-radius: 4px;
border-left: 4px solid #ffc107;
}
</style>
</head>
<body>
<h1>Alpine.js Hello World</h1>
<!-- Basic Reactive Data -->
<div class="card" x-data="{ message: 'Hello, Alpine.js!' }">
<h2>Basic Reactive Data</h2>
<p x-text="message"></p>
<button @click="message = 'Button clicked!'" class="btn">
Change Message
</button>
</div>
<!-- Counter Example -->
<div class="card" x-data="{ count: 0 }">
<h2>Counter Component</h2>
<p>Current count: <strong x-text="count"></strong></p>
<button @click="count--" class="btn">Decrement</button>
<button @click="count++" class="btn">Increment</button>
<button @click="count = 0" class="btn">Reset</button>
</div>
<!-- Input Binding -->
<div class="card" x-data="{ name: '' }">
<h2>Input Binding</h2>
<input
type="text"
x-model="name"
placeholder="Enter your name"
style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%; margin-bottom: 1rem;">
<p x-show="name">
Hello, <strong x-text="name"></strong>!
</p>
<p x-show="!name" style="color: #6c757d;">
Please enter your name above.
</p>
</div>
<!-- Toggle Visibility -->
<div class="card" x-data="{ showDetails: false }">
<h2>Toggle Visibility</h2>
<button @click="showDetails = !showDetails" class="btn">
<span x-text="showDetails ? 'Hide' : 'Show'"></span> Details
</button>
<div x-show="showDetails" x-transition class="highlight" style="margin-top: 1rem;">
<h3>Hidden Content</h3>
<p>This content is toggled using Alpine.js x-show and x-transition directives.</p>
<p>The transition effect is automatically applied when the element appears or disappears.</p>
</div>
</div>
<!-- Conditional Classes -->
<div class="card" x-data="{
isDark: false,
styles: {
light: { background: '#f8f9fa', color: '#212529', padding: '1rem', borderRadius: '4px' },
dark: { background: '#212529', color: '#f8f9fa', padding: '1rem', borderRadius: '4px' }
}
}">
<h2>Conditional Styling</h2>
<button @click="isDark = !isDark" class="btn">
Toggle Theme
</button>
<div
x-bind="isDark ? styles.dark : styles.light"
style="margin-top: 1rem;">
<p x-text="isDark ? 'Dark mode enabled' : 'Light mode enabled'"></p>
<p>Current theme: <strong x-text="isDark ? 'Dark' : 'Light'"></strong></p>
</div>
</div>
<!-- Array Iteration -->
<div class="card" x-data="{
items: [
{ id: 1, name: 'Item 1', completed: false },
{ id: 2, name: 'Item 2', completed: true },
{ id: 3, name: 'Item 3', completed: false }
],
newItem: ''
}">
<h2>Todo List</h2>
<!-- Add new item -->
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<input
type="text"
x-model="newItem"
@keyup.enter="if(newItem.trim()) { items.push({ id: Date.now(), name: newItem, completed: false }); newItem = '' }"
placeholder="Add new item"
style="flex: 1; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
<button @click="if(newItem.trim()) { items.push({ id: Date.now(), name: newItem, completed: false }); newItem = '' }" class="btn">
Add
</button>
</div>
<!-- Items list -->
<ul style="list-style: none; padding: 0;">
<template x-for="item in items" :key="item.id">
<li style="display: flex; align-items: center; padding: 0.5rem; border-bottom: 1px solid #eee;">
<input
type="checkbox"
x-model="item.completed"
style="margin-right: 0.5rem;">
<span
x-text="item.name"
:style="{ textDecoration: item.completed ? 'line-through' : 'none' }"
style="flex: 1;"></span>
<button
@click="items = items.filter(i => i.id !== item.id)"
style="background: #dc3545; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer;">
Delete
</button>
</li>
</template>
</ul>
<!-- Stats -->
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
<p>Total items: <strong x-text="items.length"></strong></p>
<p>Completed: <strong x-text="items.filter(i => i.completed).length"></strong></p>
<p>Pending: <strong x-text="items.filter(i => !i.completed).length"></strong></p>
</div>
</div>
<!-- Form Example -->
<div class="card" x-data="{
form: {
name: '',
email: '',
message: ''
},
submitted: false,
errors: {}
}">
<h2>Contact Form</h2>
<form @submit.prevent="
errors = {};
if(!form.name) errors.name = 'Name is required';
if(!form.email) errors.email = 'Email is required';
if(!form.message) errors.message = 'Message is required';
if(Object.keys(errors).length === 0) {
submitted = true;
setTimeout(() => submitted = false, 3000);
}
">
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Name:</label>
<input
type="text"
x-model="form.name"
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
<p x-show="errors.name" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.name"></p>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Email:</label>
<input
type="email"
x-model="form.email"
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
<p x-show="errors.email" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.email"></p>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Message:</label>
<textarea
x-model="form.message"
rows="4"
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;"></textarea>
<p x-show="errors.message" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.message"></p>
</div>
<button type="submit" class="btn">Submit</button>
</form>
<div x-show="submitted" x-transition class="highlight" style="margin-top: 1rem;">
<strong>Form submitted successfully!</strong>
<p>Name: <span x-text="form.name"></span></p>
<p>Email: <span x-text="form.email"></span></p>
<p>Message: <span x-text="form.message"></span></p>
</div>
</div>
<!-- Clock Example -->
<div class="card" x-data="{
time: new Date().toLocaleTimeString(),
init() {
setInterval(() => {
this.time = new Date().toLocaleTimeString();
}, 1000);
}
}">
<h2>Live Clock</h2>
<div style="font-size: 2rem; font-weight: bold; text-align: center; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
<span x-text="time"></span>
</div>
<p style="text-align: center; color: #6c757d; margin-top: 0.5rem;">
Updates every second using setInterval in the init() lifecycle hook.
</p>
</div>
<!-- Shopping Cart -->
<div class="card" x-data="{
products: [
{ id: 1, name: 'Laptop', price: 999, quantity: 0 },
{ id: 2, name: 'Mouse', price: 29, quantity: 0 },
{ id: 3, name: 'Keyboard', price: 79, quantity: 0 }
],
get total() {
return this.products.reduce((sum, product) => sum + (product.price * product.quantity), 0);
},
get itemCount() {
return this.products.reduce((sum, product) => sum + product.quantity, 0);
}
}">
<h2>Shopping Cart</h2>
<template x-for="product in products" :key="product.id">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 1rem; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 0.5rem;">
<div>
<strong x-text="product.name"></strong>
<span style="color: #6c757d;">($<span x-text="product.price"></span>)</span>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<button
@click="product.quantity = Math.max(0, product.quantity - 1)"
style="background: #6c757d; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer;">
-
</button>
<span x-text="product.quantity" style="min-width: 2rem; text-align: center;"></span>
<button
@click="product.quantity++"
style="background: #28a745; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer;">
+
</button>
</div>
</div>
</template>
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px; text-align: right;">
<p>Items: <strong x-text="itemCount"></strong></p>
<p>Total: <strong>$<span x-text="total.toFixed(2)"></span></strong></p>
<button
@click="products.forEach(p => p.quantity = 0)"
style="background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; margin-top: 0.5rem;">
Clear Cart
</button>
</div>
</div>
</body>
</html>
💻 Patterns de Composants Alpine.js html
🟡 intermediate
⭐⭐⭐
Patterns de composants avancés, flux de données et architecture avec Alpine.js
⏱️ 30 min
🏷️ alpinejs, components, patterns
Prerequisites:
Alpine.js basics, JavaScript ES6+, Component concepts
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine.js Component Patterns</title>
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
background: white;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
}
.tab {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab.active {
border-bottom-color: #007bff;
color: #007bff;
font-weight: bold;
}
.tab-content {
padding: 1rem 0;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
min-width: 200px;
z-index: 100;
}
.dropdown-item {
padding: 0.5rem 1rem;
cursor: pointer;
}
.dropdown-item:hover {
background: #f8f9fa;
}
.accordion-item {
border: 1px solid #ddd;
margin-bottom: 0.5rem;
border-radius: 4px;
}
.accordion-header {
padding: 1rem;
background: #f8f9fa;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.accordion-content {
padding: 1rem;
border-top: 1px solid #ddd;
}
.slide-item {
min-width: 100%;
padding: 2rem;
background: #f8f9fa;
text-align: center;
border-radius: 8px;
}
.carousel-container {
overflow: hidden;
border-radius: 8px;
}
.carousel-track {
display: flex;
transition: transform 0.3s ease;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 4px;
color: white;
font-weight: bold;
z-index: 1000;
}
.notification.success { background: #28a745; }
.notification.error { background: #dc3545; }
.notification.warning { background: #ffc107; color: #212529; }
.notification.info { background: #17a2b8; }
</style>
</head>
<body>
<h1>Alpine.js Component Patterns</h1>
<!-- Tabs Component -->
<div class="card" x-data="tabsComponent()">
<h2>Tabs Component</h2>
<div class="tabs">
<template x-for="tab in tabs" :key="tab.id">
<div
@click="activeTab = tab.id"
:class="{ 'active': activeTab === tab.id }"
class="tab"
x-text="tab.title">
</div>
</template>
</div>
<div class="tab-content">
<template x-for="tab in tabs" :key="tab.id">
<div x-show="activeTab === tab.id" x-transition>
<h3 x-text="tab.title"></h3>
<p x-text="tab.content"></p>
</div>
</template>
</div>
</div>
<!-- Modal Component -->
<div class="card" x-data="modalComponent()">
<h2>Modal Component</h2>
<button @click="open('basic')" class="btn" style="margin-right: 0.5rem;">Open Basic Modal</button>
<button @click="open('confirm')" class="btn" style="margin-right: 0.5rem;">Open Confirm Modal</button>
<button @click="open('form')" class="btn">Open Form Modal</button>
<!-- Basic Modal -->
<div x-show="currentModal === 'basic'" x-transition class="modal">
<div class="modal-content" @click.away="close()">
<h3>Basic Modal</h3>
<p>This is a basic modal dialog with some content.</p>
<button @click="close()" class="btn">Close</button>
</div>
</div>
<!-- Confirm Modal -->
<div x-show="currentModal === 'confirm'" x-transition class="modal">
<div class="modal-content" @click.away="close()">
<h3>Confirm Action</h3>
<p>Are you sure you want to perform this action?</p>
<div style="margin-top: 1rem;">
<button @click="confirm()" class="btn" style="margin-right: 0.5rem;">Confirm</button>
<button @click="close()">Cancel</button>
</div>
</div>
</div>
<!-- Form Modal -->
<div x-show="currentModal === 'form'" x-transition class="modal">
<div class="modal-content" @click.away="close()">
<h3>User Information</h3>
<form @submit.prevent="submitForm()">
<div style="margin-bottom: 1rem;">
<label>Name:</label>
<input type="text" x-model="formData.name" required style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 1rem;">
<label>Email:</label>
<input type="email" x-model="formData.email" required style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
</div>
<button type="submit" class="btn" style="margin-right: 0.5rem;">Submit</button>
<button type="button" @click="close()">Cancel</button>
</form>
</div>
</div>
</div>
<!-- Dropdown Component -->
<div class="card" x-data="dropdownComponent()">
<h2>Dropdown Component</h2>
<div class="dropdown">
<button @click="toggle()" class="btn">
Select Option <span x-text="selected ? '✓' : '▼'"></span>
</button>
<div x-show="isOpen" x-transition @click.away="close()" class="dropdown-menu">
<template x-for="option in options" :key="option.value">
<div
@click="select(option)"
class="dropdown-item"
:style="{ fontWeight: selected === option.value ? 'bold' : 'normal' }">
<span x-text="option.icon" style="margin-right: 0.5rem;"></span>
<span x-text="option.label"></span>
</div>
</template>
</div>
</div>
<p style="margin-top: 1rem;">Selected: <strong x-text="selectedLabel"></strong></p>
</div>
<!-- Accordion Component -->
<div class="card" x-data="accordionComponent()">
<h2>Accordion Component</h2>
<template x-for="(item, index) in items" :key="index">
<div class="accordion-item">
<div class="accordion-header" @click="toggle(index)">
<span x-text="item.title"></span>
<span x-text="openItems.includes(index) ? '▼' : '▶'"></span>
</div>
<div x-show="openItems.includes(index)" x-collapse>
<div class="accordion-content">
<p x-text="item.content"></p>
</div>
</div>
</div>
</template>
</div>
<!-- Carousel Component -->
<div class="card" x-data="carouselComponent()">
<h2>Carousel Component</h2>
<div class="carousel-container">
<div class="carousel-track" :style="{ transform: `translateX(-${currentIndex * 100}%)` }">
<template x-for="(slide, index) in slides" :key="index">
<div class="slide-item">
<h3 x-text="slide.title"></h3>
<p x-text="slide.content"></p>
</div>
</template>
</div>
</div>
<div style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;">
<button @click="prev()" class="btn" :disabled="currentIndex === 0">Previous</button>
<div>
<template x-for="(slide, index) in slides" :key="index">
<button
@click="goTo(index)"
:style="{ background: currentIndex === index ? '#007bff' : '#6c757d' }"
style="width: 10px; height: 10px; border-radius: 50%; margin: 0 2px; border: none; cursor: pointer;">
</button>
</template>
</div>
<button @click="next()" class="btn" :disabled="currentIndex === slides.length - 1">Next</button>
</div>
</div>
<!-- Search Filter Component -->
<div class="card" x-data="searchComponent()">
<h2>Search Filter Component</h2>
<div style="margin-bottom: 1rem;">
<input
type="text"
x-model="searchTerm"
placeholder="Search users..."
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 1rem;">
<label>
<input type="checkbox" x-model="showActive" style="margin-right: 0.5rem;">
Show only active users
</label>
</div>
<div style="margin-bottom: 1rem;">
<label>Role filter:</label>
<select x-model="selectedRole" style="margin-left: 0.5rem; padding: 0.25rem; border: 1px solid #ddd; border-radius: 4px;">
<option value="">All</option>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="guest">Guest</option>
</select>
</div>
<div>
<p><strong>Filtered results:</strong></p>
<template x-for="user in filteredUsers" :key="user.id">
<div style="padding: 0.5rem; border: 1px solid #eee; margin-bottom: 0.25rem; border-radius: 4px;">
<span x-text="user.name"></span>
<span style="margin-left: 0.5rem; color: #6c757d;" x-text="'(' + user.role + ')'"></span>
<span
:style="{
color: user.active ? '#28a745' : '#dc3545',
marginLeft: '0.5rem'
}"
x-text="user.active ? '✓ Active' : '✗ Inactive'">
</span>
</div>
</template>
<p x-show="filteredUsers.length === 0" style="color: #6c757d; font-style: italic;">
No users found matching the criteria.
</p>
</div>
</div>
<!-- Form Validation Component -->
<div class="card" x-data="formValidationComponent()">
<h2>Form Validation Component</h2>
<form @submit.prevent="validateAndSubmit()">
<div style="margin-bottom: 1rem;">
<label>Username (min 3 chars):</label>
<input
type="text"
x-model="form.username"
@blur="validateField('username')"
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;"
:style="{ borderColor: errors.username ? '#dc3545' : touched.username && !errors.username ? '#28a745' : '#ddd' }">
<p x-show="errors.username" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.username"></p>
</div>
<div style="margin-bottom: 1rem;">
<label>Email:</label>
<input
type="email"
x-model="form.email"
@blur="validateField('email')"
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;"
:style="{ borderColor: errors.email ? '#dc3545' : touched.email && !errors.email ? '#28a745' : '#ddd' }">
<p x-show="errors.email" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.email"></p>
</div>
<div style="margin-bottom: 1rem;">
<label>Password (min 8 chars):</label>
<input
type="password"
x-model="form.password"
@blur="validateField('password')"
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;"
:style="{ borderColor: errors.password ? '#dc3545' : touched.password && !errors.password ? '#28a745' : '#ddd' }">
<p x-show="errors.password" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.password"></p>
</div>
<div style="margin-bottom: 1rem;">
<label>Confirm Password:</label>
<input
type="password"
x-model="form.confirmPassword"
@blur="validateField('confirmPassword')"
style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;"
:style="{ borderColor: errors.confirmPassword ? '#dc3545' : touched.confirmPassword && !errors.confirmPassword ? '#28a745' : '#ddd' }">
<p x-show="errors.confirmPassword" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.confirmPassword"></p>
</div>
<div style="margin-bottom: 1rem;">
<label>
<input type="checkbox" x-model="form.terms" style="margin-right: 0.5rem;">
I accept the terms and conditions
</label>
<p x-show="errors.terms" style="color: #dc3545; font-size: 0.875rem;" x-text="errors.terms"></p>
</div>
<button type="submit" class="btn" :disabled="!isValid">Register</button>
</form>
<div x-show="submitted" x-transition style="margin-top: 1rem; padding: 1rem; background: #d4edda; border-radius: 4px;">
<strong>Form submitted successfully!</strong>
</div>
</div>
<!-- Notification System -->
<div class="card" x-data="notificationSystem()">
<h2>Notification System</h2>
<div>
<button @click="show('success', 'Operation completed successfully!')" class="btn" style="margin-right: 0.5rem; background: #28a745;">Success</button>
<button @click="show('error', 'Something went wrong!')" class="btn" style="margin-right: 0.5rem; background: #dc3545;">Error</button>
<button @click="show('warning', 'Please be careful!')" class="btn" style="margin-right: 0.5rem; background: #ffc107; color: #212529;">Warning</button>
<button @click="show('info', 'Here is some information')" class="btn" style="background: #17a2b8;">Info</button>
</div>
</div>
<script>
// Component initialization functions
function tabsComponent() {
return {
activeTab: 'tab1',
tabs: [
{ id: 'tab1', title: 'Home', content: 'Welcome to the home tab. This is where you can find general information and updates.' },
{ id: 'tab2', title: 'Profile', content: 'Manage your profile information, settings, and preferences here.' },
{ id: 'tab3', title: 'Settings', content: 'Configure your application settings and customize your experience.' }
]
}
}
function modalComponent() {
return {
currentModal: null,
formData: { name: '', email: '' },
open(type) {
this.currentModal = type;
},
close() {
this.currentModal = null;
},
confirm() {
alert('Action confirmed!');
this.close();
},
submitForm() {
alert('Form submitted: ' + JSON.stringify(this.formData));
this.close();
}
}
}
function dropdownComponent() {
return {
isOpen: false,
selected: null,
options: [
{ value: 'edit', label: 'Edit', icon: '✏️' },
{ value: 'copy', label: 'Copy', icon: '📋' },
{ value: 'delete', label: 'Delete', icon: '🗑️' },
{ value: 'share', label: 'Share', icon: '🔗' }
],
toggle() {
this.isOpen = !this.isOpen;
},
close() {
this.isOpen = false;
},
select(option) {
this.selected = option.value;
this.close();
},
get selectedLabel() {
const option = this.options.find(o => o.value === this.selected);
return option ? option.label : 'None selected';
}
}
}
function accordionComponent() {
return {
openItems: [0],
items: [
{ title: 'What is Alpine.js?', content: 'Alpine.js is a rugged, minimal JavaScript framework for adding client-side interactivity to your server-rendered HTML.' },
{ title: 'Why use Alpine.js?', content: 'Alpine.js provides the reactive and declarative nature of big frameworks like Vue or React at a much lower cost and with much less complexity.' },
{ title: 'How does it work?', content: 'You include Alpine.js on your page and use x-data, x-bind, x-on, and other directives to add interactivity to your HTML.' }
],
toggle(index) {
const position = this.openItems.indexOf(index);
if (position === -1) {
this.openItems.push(index);
} else {
this.openItems.splice(position, 1);
}
}
}
}
function carouselComponent() {
return {
currentIndex: 0,
slides: [
{ title: 'Slide 1', content: 'This is the first slide with some example content.' },
{ title: 'Slide 2', content: 'This is the second slide with different content.' },
{ title: 'Slide 3', content: 'This is the third slide with yet more content.' }
],
next() {
if (this.currentIndex < this.slides.length - 1) {
this.currentIndex++;
}
},
prev() {
if (this.currentIndex > 0) {
this.currentIndex--;
}
},
goTo(index) {
this.currentIndex = index;
}
}
}
function searchComponent() {
return {
searchTerm: '',
showActive: false,
selectedRole: '',
users: [
{ id: 1, name: 'John Doe', role: 'admin', active: true },
{ id: 2, name: 'Jane Smith', role: 'user', active: true },
{ id: 3, name: 'Bob Johnson', role: 'guest', active: false },
{ id: 4, name: 'Alice Brown', role: 'user', active: true },
{ id: 5, name: 'Charlie Wilson', role: 'admin', active: false }
],
get filteredUsers() {
return this.users.filter(user => {
const matchesSearch = user.name.toLowerCase().includes(this.searchTerm.toLowerCase());
const matchesActive = !this.showActive || user.active;
const matchesRole = !this.selectedRole || user.role === this.selectedRole;
return matchesSearch && matchesActive && matchesRole;
});
}
}
}
function formValidationComponent() {
return {
form: {
username: '',
email: '',
password: '',
confirmPassword: '',
terms: false
},
errors: {},
touched: {},
submitted: false,
validateField(field) {
this.touched[field] = true;
switch(field) {
case 'username':
if (!this.form.username || this.form.username.length < 3) {
this.errors[field] = 'Username must be at least 3 characters';
} else {
delete this.errors[field];
}
break;
case 'email':
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!this.form.email || !emailRegex.test(this.form.email)) {
this.errors[field] = 'Please enter a valid email';
} else {
delete this.errors[field];
}
break;
case 'password':
if (!this.form.password || this.form.password.length < 8) {
this.errors[field] = 'Password must be at least 8 characters';
} else {
delete this.errors[field];
}
break;
case 'confirmPassword':
if (this.form.password !== this.form.confirmPassword) {
this.errors[field] = 'Passwords do not match';
} else {
delete this.errors[field];
}
break;
case 'terms':
if (!this.form.terms) {
this.errors[field] = 'You must accept the terms';
} else {
delete this.errors[field];
}
break;
}
},
get isValid() {
return Object.keys(this.errors).length === 0 &&
this.form.username &&
this.form.email &&
this.form.password &&
this.form.confirmPassword &&
this.form.terms;
},
validateAndSubmit() {
// Validate all fields
['username', 'email', 'password', 'confirmPassword', 'terms'].forEach(field => {
this.validateField(field);
});
if (this.isValid) {
this.submitted = true;
setTimeout(() => this.submitted = false, 3000);
}
}
}
}
function notificationSystem() {
return {
show(type, message) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.zIndex = '1000';
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
}
}
</script>
</body>
</html>
💻 Gestion d'État Alpine.js html
🟡 intermediate
⭐⭐⭐⭐
Gestion d'état avancée, stores et patterns de flux de données avec Alpine.js
⏱️ 35 min
🏷️ alpinejs, state, stores
Prerequisites:
Alpine.js basics, State management concepts, JavaScript ES6+
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine.js State Management</title>
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
background: white;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0056b3;
}
.btn.success {
background: #28a745;
}
.btn.warning {
background: #ffc107;
color: #212529;
}
.btn.danger {
background: #dc3545;
}
.store-monitor {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.875rem;
margin: 1rem 0;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
border: 1px solid #eee;
margin-bottom: 0.5rem;
border-radius: 4px;
}
.theme-light {
background: white;
color: #212529;
}
.theme-dark {
background: #212529;
color: #f8f9fa;
}
.todo-item {
display: flex;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item.completed span {
text-decoration: line-through;
}
</style>
</head>
<body x-data="mainApp()">
<h1>Alpine.js State Management</h1>
<!-- Global Store Monitor -->
<div class="card">
<h2>Global Store Monitor</h2>
<div class="store-monitor">
<div>Cart Items: <span x-text="cart.items.length"></span></div>
<div>Cart Total: $<span x-text="cart.total.toFixed(2)"></span></div>
<div>User Theme: <span x-text="user.theme"></span></div>
<div>Todos: <span x-text="todos.items.filter(t => !t.completed).length"></span> pending, <span x-text="todos.items.filter(t => t.completed).length"></span> completed</div>
<div>Current Time: <span x-text="clock.time"></span></div>
</div>
</div>
<!-- Shopping Cart Store -->
<div class="card" x-data="cartStore()">
<h2>Shopping Cart Store</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<!-- Products -->
<div>
<h3>Available Products</h3>
<template x-for="product in availableProducts" :key="product.id">
<div class="cart-item">
<div>
<strong x-text="product.name"></strong>
<span style="color: #6c757d;"> - $<span x-text="product.price"></span></span>
</div>
<button @click="addToCart(product)" class="btn">Add to Cart</button>
</div>
</template>
</div>
<!-- Cart -->
<div>
<h3>Shopping Cart</h3>
<template x-for="item in cart.items" :key="item.id">
<div class="cart-item">
<div>
<strong x-text="item.name"></strong>
<span style="color: #6c757d;"> ($<span x-text="item.price"></span> × <span x-text="item.quantity"></span>)</span>
</div>
<div>
<button @click="decrementQuantity(item.id)" class="btn" style="margin-right: 0.25rem;">-</button>
<span x-text="item.quantity" style="margin: 0 0.5rem;"></span>
<button @click="incrementQuantity(item.id)" class="btn" style="margin-right: 0.25rem;">+</button>
<button @click="removeFromCart(item.id)" class="btn danger">×</button>
</div>
</div>
</template>
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
<strong>Total: $<span x-text="cart.total.toFixed(2)"></span></strong>
</div>
<button @click="clearCart()" class="btn warning" style="margin-top: 0.5rem;">Clear Cart</button>
</div>
</div>
</div>
<!-- User Store -->
<div class="card" x-data="userStore()">
<h2>User Store</h2>
<div>
<h3>User Profile</h3>
<p>Name: <strong x-text="user.name"></strong></p>
<p>Email: <strong x-text="user.email"></strong></p>
<p>Theme: <strong x-text="user.theme"></strong></p>
<p>Login Count: <strong x-text="user.loginCount"></strong></p>
<div style="margin-top: 1rem;">
<button @click="toggleTheme()" class="btn">Toggle Theme</button>
<button @click="incrementLoginCount()" class="btn" style="margin-left: 0.5rem;">Increment Login</button>
</div>
</div>
<div style="margin-top: 2rem;">
<h3>Preferences</h3>
<label>
<input type="checkbox" x-model="user.preferences.notifications" style="margin-right: 0.5rem;">
Enable Notifications
</label><br>
<label style="margin-top: 0.5rem;">
<input type="checkbox" x-model="user.preferences.darkMode" style="margin-right: 0.5rem;">
Dark Mode (overrides theme)
</label><br>
<label style="margin-top: 0.5rem;">
Language:
<select x-model="user.preferences.language" style="margin-left: 0.5rem;">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
</label>
</div>
</div>
<!-- Todo Store -->
<div class="card" x-data="todoStore()">
<h2>Todo Store</h2>
<div>
<!-- Add Todo -->
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<input
type="text"
x-model="newTodo"
@keyup.enter="addTodo()"
placeholder="Add new todo..."
style="flex: 1; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
<button @click="addTodo()" class="btn">Add</button>
</div>
<!-- Filters -->
<div style="margin-bottom: 1rem;">
<button
@click="todos.filter = 'all'"
:class="{ 'btn': true, 'success': todos.filter === 'all' }"
style="margin-right: 0.25rem;">
All (<span x-text="todos.items.length"></span>)
</button>
<button
@click="todos.filter = 'active'"
:class="{ 'btn': true, 'success': todos.filter === 'active' }"
style="margin-right: 0.25rem;">
Active (<span x-text="todos.items.filter(t => !t.completed).length"></span>)
</button>
<button
@click="todos.filter = 'completed'"
:class="{ 'btn': true, 'success': todos.filter === 'completed' }"
style="margin-right: 0.25rem;">
Completed (<span x-text="todos.items.filter(t => t.completed).length"></span>)
</button>
</div>
<!-- Todo List -->
<template x-for="todo in filteredTodos" :key="todo.id">
<div class="todo-item" :class="{ completed: todo.completed }">
<input
type="checkbox"
x-model="todo.completed"
@change="toggleTodo(todo.id)"
style="margin-right: 0.5rem;">
<span x-text="todo.text" style="flex: 1;"></span>
<button
@click="removeTodo(todo.id)"
class="btn danger"
style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">
Delete
</button>
</div>
</template>
<!-- Stats -->
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
<div>Completion Rate: <strong x-text="Math.round((todos.items.filter(t => t.completed).length / todos.items.length) * 100) || 0">%</strong></div>
</div>
</div>
</div>
<!-- Clock Store -->
<div class="card" x-data="clockStore()">
<h2>Clock Store</h2>
<div style="text-align: center;">
<div style="font-size: 3rem; font-weight: bold; margin-bottom: 1rem;">
<span x-text="clock.time"></span>
</div>
<div style="margin-bottom: 1rem;">
<button @click="toggleClock()" class="btn">
<span x-text="clock.isRunning ? 'Stop' : 'Start'"></span> Clock
</button>
<button @click="resetClock()" class="btn" style="margin-left: 0.5rem;">Reset</button>
</div>
<div>
<label>
<input type="checkbox" x-model="clock.showSeconds" style="margin-right: 0.5rem;">
Show Seconds
</label>
</div>
</div>
</div>
<!-- Cross-Store Communication -->
<div class="card" x-data="crossStoreDemo()">
<h2>Cross-Store Communication</h2>
<div>
<p>This demonstrates how different stores can communicate and affect each other.</p>
<div style="margin-top: 1rem;">
<button @click="performComplexAction()" class="btn">
Perform Complex Action (Add Random Product + Todo)
</button>
<button @click="resetAllStores()" class="btn warning" style="margin-left: 0.5rem;">
Reset All Stores
</button>
</div>
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
<strong>Action Log:</strong>
<ul style="margin: 0.5rem 0 0 0; padding-left: 1rem;">
<template x-for="log in actionLogs" :key="log.timestamp">
<li style="font-size: 0.875rem; color: #6c757d;">
<span x-text="log.timestamp"></span> - <span x-text="log.message"></span>
</li>
</template>
</ul>
</div>
</div>
</div>
<script>
// Alpine Store Implementation
document.addEventListener('alpine:init', () => {
// Shopping Cart Store
Alpine.store('cart', {
items: [],
get total() {
return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
add(product) {
const existingItem = this.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity++;
} else {
this.items.push({ ...product, quantity: 1 });
}
},
remove(productId) {
this.items = this.items.filter(item => item.id !== productId);
},
incrementQuantity(productId) {
const item = this.items.find(item => item.id === productId);
if (item) {
item.quantity++;
}
},
decrementQuantity(productId) {
const item = this.items.find(item => item.id === productId);
if (item && item.quantity > 1) {
item.quantity--;
} else {
this.remove(productId);
}
},
clear() {
this.items = [];
}
});
// User Store
Alpine.store('user', {
name: 'John Doe',
email: '[email protected]',
theme: 'light',
loginCount: 1,
preferences: {
notifications: true,
darkMode: false,
language: 'en'
},
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
},
incrementLoginCount() {
this.loginCount++;
}
});
// Todo Store
Alpine.store('todos', {
items: [],
filter: 'all',
add(text) {
this.items.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
});
},
toggle(id) {
const todo = this.items.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
},
remove(id) {
this.items = this.items.filter(t => t.id !== id);
},
get filtered() {
switch(this.filter) {
case 'active':
return this.items.filter(t => !t.completed);
case 'completed':
return this.items.filter(t => t.completed);
default:
return this.items;
}
}
});
// Clock Store
Alpine.store('clock', {
time: new Date().toLocaleTimeString(),
isRunning: true,
showSeconds: true,
interval: null,
init() {
this.start();
},
start() {
this.isRunning = true;
this.interval = setInterval(() => {
this.updateTime();
}, 1000);
},
stop() {
this.isRunning = false;
if (this.interval) {
clearInterval(this.interval);
}
},
toggle() {
if (this.isRunning) {
this.stop();
} else {
this.start();
}
},
updateTime() {
const now = new Date();
if (this.showSeconds) {
this.time = now.toLocaleTimeString();
} else {
this.time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
},
reset() {
this.time = '00:00:00';
}
});
});
// Component data functions
function mainApp() {
return {
cart: Alpine.store('cart'),
user: Alpine.store('user'),
todos: Alpine.store('todos'),
clock: Alpine.store('clock')
}
}
function cartStore() {
return {
cart: Alpine.store('cart'),
availableProducts: [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Keyboard', price: 79 },
{ id: 4, name: 'Monitor', price: 299 },
{ id: 5, name: 'Headphones', price: 149 }
],
addToCart(product) {
this.cart.add(product);
},
removeFromCart(productId) {
this.cart.remove(productId);
},
incrementQuantity(productId) {
this.cart.incrementQuantity(productId);
},
decrementQuantity(productId) {
this.cart.decrementQuantity(productId);
},
clearCart() {
this.cart.clear();
}
}
}
function userStore() {
return {
user: Alpine.store('user'),
toggleTheme() {
this.user.toggleTheme();
},
incrementLoginCount() {
this.user.incrementLoginCount();
}
}
}
function todoStore() {
return {
newTodo: '',
todos: Alpine.store('todos'),
filteredTodos: [],
init() {
this.$watch('todos.filter', () => {
this.filteredTodos = this.todos.filtered;
});
this.filteredTodos = this.todos.filtered;
},
addTodo() {
if (this.newTodo.trim()) {
this.todos.add(this.newTodo);
this.newTodo = '';
}
},
toggleTodo(id) {
this.todos.toggle(id);
},
removeTodo(id) {
this.todos.remove(id);
}
}
}
function clockStore() {
return {
clock: Alpine.store('clock'),
toggleClock() {
this.clock.toggle();
},
resetClock() {
this.clock.reset();
}
}
}
function crossStoreDemo() {
return {
actionLogs: [],
cart: Alpine.store('cart'),
todos: Alpine.store('todos'),
user: Alpine.store('user'),
performComplexAction() {
// Add random product to cart
const products = [
{ id: Date.now(), name: 'Random Product ' + Math.floor(Math.random() * 100), price: Math.floor(Math.random() * 100) + 10 }
];
this.cart.add(products[0]);
this.logAction('Added random product to cart');
// Add todo
this.todos.add('Complex action performed - check cart');
this.logAction('Added todo about complex action');
// Increment user login count
this.user.incrementLoginCount();
this.logAction('Incremented user login count');
},
resetAllStores() {
this.cart.clear();
this.todos.items = [];
this.todos.filter = 'all';
this.user.loginCount = 0;
this.logAction('Reset all stores');
},
logAction(message) {
this.actionLogs.unshift({
timestamp: new Date().toLocaleTimeString(),
message
});
// Keep only last 10 logs
if (this.actionLogs.length > 10) {
this.actionLogs.pop();
}
}
}
}
</script>
</body>
</html>