Ejemplos de Alpine.js

Ejemplos del framework Alpine.js incluyendo componentes reactivos, enlace de datos y patrones modernos de Alpine.js

💻 Alpine.js Hello World html

🟢 simple

Configuración básica de Alpine.js y ejemplos de Hello World con datos reactivos

⏱️ 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>

💻 Patrones de Componentes Alpine.js html

🟡 intermediate ⭐⭐⭐

Patrones de componentes avanzados, flujo de datos y arquitectura con 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>

💻 Gestión de Estado Alpine.js html

🟡 intermediate ⭐⭐⭐⭐

Gestión de estado avanzada, stores y patrones de flujo de datos con 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>