Ejemplos HTMX

Ejemplos de la librería HTMX para aplicaciones web modernas con interacciones HTML dinámicas

Key Facts

Category
JavaScript Libraries
Items
4
Format Families
sample

Sample Overview

Ejemplos de la librería HTMX para aplicaciones web modernas con interacciones HTML dinámicas This sample set belongs to JavaScript Libraries and can be used to test related workflows inside Elysia Tools.

💻 HTMX Hola Mundo html

🟢 simple

Configuración básica de HTMX y ejemplo de interacciones AJAX

⏱️ 10 min 🏷️ htmx, javascript, ajax
Prerequisites: Basic HTML knowledge
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX Hello World</title>
    <!-- HTMX Library -->
    <script src="https://unpkg.com/htmx.org@latest"></script>
    <!-- Basic styling -->
    <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;
        }
        .loading {
            opacity: 0.7;
            transition: opacity 300ms;
        }
    </style>
</head>
<body>
    <h1>HTMX Hello World</h1>

    <!-- Basic Click Example -->
    <div class="card">
        <h2>Click to Load Content</h2>
        <p>This example demonstrates HTMX's ability to load content with a simple click.</p>

        <button
            hx-get="/api/hello-world-message"
            hx-target="#message-content"
            hx-swap="innerHTML"
            class="btn">
            Load Message
        </button>

        <div id="message-content" class="mt-4">
            <em>Click the button above to load a message from the server...</em>
        </div>
    </div>

    <!-- Button State Example -->
    <div class="card">
        <h2>Button with Loading State</h2>
        <p>Example showing HTMX request indicators and disabled state during loading.</p>

        <button
            hx-get="/api/delayed-response"
            hx-target="#delayed-content"
            hx-swap="innerHTML"
            hx-indicator="#loading-indicator"
            hx-disabled-elt="this"
            class="btn">
            Load Data (2 second delay)
        </button>

        <span id="loading-indicator" class="htmx-indicator">
            ⏳ Loading...
        </span>

        <div id="delayed-content" class="mt-4">
            <em>Click to load data with artificial delay...</em>
        </div>
    </div>

    <!-- Form Submission Example -->
    <div class="card">
        <h2>AJAX Form Submission</h2>
        <p>Submit forms without page refresh using HTMX.</p>

        <form
            hx-post="/api/submit-form"
            hx-target="#form-result"
            hx-swap="innerHTML">

            <div style="margin-bottom: 1rem;">
                <label for="name">Name:</label><br>
                <input type="text" id="name" name="name" required style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%;">
            </div>

            <div style="margin-bottom: 1rem;">
                <label for="email">Email:</label><br>
                <input type="email" id="email" name="email" required style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%;">
            </div>

            <button type="submit" class="btn">Submit Form</button>
        </form>

        <div id="form-result" class="mt-4"></div>
    </div>

    <!-- Real-time Updates Example -->
    <div class="card">
        <h2>Real-time Counter</h2>
        <p>Update content dynamically using HTMX polling.</p>

        <div
            hx-get="/api/counter"
            hx-trigger="every 1s"
            hx-swap="innerHTML">
            Counter: <strong id="counter">0</strong>
        </div>

        <p><small>This counter updates automatically every second.</small></p>
    </div>

    <!-- Dynamic Content Example -->
    <div class="card">
        <h2>Dynamic Content Loading</h2>
        <p>Load different content based on user selection.</p>

        <select
            name="value"
            hx-get="/api/content"
            hx-target="#dynamic-content"
            hx-swap="innerHTML"
            hx-trigger="change">
            <option value="">Select an option...</option>
            <option value="users">Users</option>
            <option value="products">Products</option>
            <option value="orders">Orders</option>
        </select>

        <div id="dynamic-content" class="mt-4">
            <em>Select an option to load content...</em>
        </div>
    </div>

    <!-- Keyboard Shortcuts Example -->
    <div class="card">
        <h2>Keyboard Shortcuts</h2>
        <p>Trigger actions with keyboard events using HTMX.</p>

        <div>
            <input
                type="text"
                placeholder="Type 'hello' and press Enter"
                hx-get="/api/search?q={value}"
                hx-target="#search-results"
                hx-swap="innerHTML"
                hx-trigger="keyup[keyCode==13]"
                style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%;">
        </div>

        <div id="search-results" class="mt-4"></div>

        <p><small>Press Enter when typing "hello" to trigger search.</small></p>
    </div>

    <script>
        // Mock server responses for demonstration
        // In a real application, these would be actual server endpoints

        // Intercept htmx requests for demonstration
        document.addEventListener('htmx:beforeRequest', function(event) {
            const url = event.detail.requestConfig.url;

            // Mock response for hello-world-message
            if (url.includes('/api/hello-world-message')) {
                event.preventDefault();
                document.getElementById('message-content').innerHTML =
                    '<div style="background: #e7f3fe; padding: 1rem; border-radius: 4px;">' +
                    '<h3>Hello, HTMX! 🎉</h3>' +
                    '<p>This message was loaded dynamically without page refresh!</p>' +
                    '<p>Current time: ' + new Date().toLocaleTimeString() + '</p>' +
                    '</div>';
                return;
            }

            // Mock response for delayed-response
            if (url.includes('/api/delayed-response')) {
                event.preventDefault();
                document.getElementById('delayed-content').classList.add('loading');
                setTimeout(() => {
                    document.getElementById('delayed-content').classList.remove('loading');
                    document.getElementById('delayed-content').innerHTML =
                        '<div style="background: #d4edda; padding: 1rem; border-radius: 4px;">' +
                        '<h3>Data Loaded Successfully! ✅</h3>' +
                        '<p>This content was loaded after a 2-second delay.</p>' +
                        '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>' +
                        '</div>';
                }, 2000);
                return;
            }

            // Mock response for form submission
            if (url.includes('/api/submit-form')) {
                event.preventDefault();
                const formData = new URLSearchParams(event.detail.requestConfig.parameters);
                const name = formData.get('name');
                const email = formData.get('email');

                document.getElementById('form-result').innerHTML =
                    '<div style="background: #d1ecf1; padding: 1rem; border-radius: 4px;">' +
                    '<h3>Form Submitted! 📝</h3>' +
                    '<p><strong>Name:</strong> ' + name + '</p>' +
                    '<p><strong>Email:</strong> ' + email + '</p>' +
                    '<p>Form submitted at: ' + new Date().toLocaleTimeString() + '</p>' +
                    '</div>';
                return;
            }

            // Mock response for counter
            if (url.includes('/api/counter')) {
                event.preventDefault();
                const counterElement = document.querySelector('#counter');
                const currentCount = parseInt(counterElement.textContent);
                counterElement.textContent = currentCount + 1;
                return;
            }

            // Mock response for dynamic content
            if (url.includes('/api/content')) {
                event.preventDefault();
                const contentType = new URL(url, window.location.origin).searchParams.get('value');
                let content = '';

                switch(contentType) {
                    case 'users':
                        content =
                            '<h3>Users 👥</h3>' +
                            '<ul><li>John Doe - [email protected]</li>' +
                            '<li>Jane Smith - [email protected]</li>' +
                            '<li>Bob Johnson - [email protected]</li></ul>';
                        break;
                    case 'products':
                        content =
                            '<h3>Products 🛍️</h3>' +
                            '<ul><li>Laptop - $999</li>' +
                            '<li>Mouse - $29</li>' +
                            '<li>Keyboard - $79</li></ul>';
                        break;
                    case 'orders':
                        content =
                            '<h3>Orders 📦</h3>' +
                            '<ul><li>Order #1234 - Pending</li>' +
                            '<li>Order #1235 - Shipped</li>' +
                            '<li>Order #1236 - Delivered</li></ul>';
                        break;
                }

                document.getElementById('dynamic-content').innerHTML = content;
                return;
            }

            // Mock response for search
            if (url.includes('/api/search')) {
                event.preventDefault();
                const searchParams = new URLSearchParams(url.split('?')[1]);
                const query = searchParams.get('q');

                if (query && query.toLowerCase().includes('hello')) {
                    document.getElementById('search-results').innerHTML =
                        '<div style="background: #fff3cd; padding: 1rem; border-radius: 4px;">' +
                        '<h3>Search Results for "' + query + '"</h3>' +
                        '<ul><li>Hello World Tutorial</li>' +
                        '<li>Hello HTMX Guide</li>' +
                        '<li>Hello JavaScript Book</li></ul>' +
                        '</div>';
                } else {
                    document.getElementById('search-results').innerHTML =
                        '<p style="color: #dc3545;">Try typing "hello" and press Enter!</p>';
                }
                return;
            }
        });
    </script>
</body>
</html>

💻 Operaciones CRUD HTMX html

🟡 intermediate ⭐⭐⭐

Operaciones CRUD completas (Crear, Leer, Actualizar, Eliminar) con HTMX

⏱️ 25 min 🏷️ htmx, crud, database, table
Prerequisites: HTMX basics, understanding of CRUD operations
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX CRUD Operations</title>
    <script src="https://unpkg.com/htmx.org@latest"></script>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 1200px;
            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;
            margin-right: 0.5rem;
        }
        .btn:hover { background: #0056b3; }
        .btn-success { background: #28a745; }
        .btn-success:hover { background: #1e7e34; }
        .btn-danger { background: #dc3545; }
        .btn-danger:hover { background: #c82333; }
        .btn-warning { background: #ffc107; color: #000; }
        .btn-warning:hover { background: #e0a800; }
        .table { width: 100%; border-collapse: collapse; }
        .table th, .table td { padding: 0.75rem; border: 1px solid #ddd; text-align: left; }
        .table th { background: #f8f9fa; }
        .form-group { margin-bottom: 1rem; }
        .form-group label { display: block; margin-bottom: 0.5rem; font-weight: bold; }
        .form-control { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; }
        .modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.4);
        }
        .modal-content {
            background-color: white;
            margin: 15% auto;
            padding: 2rem;
            border-radius: 8px;
            width: 80%;
            max-width: 500px;
        }
        .close { float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
        .toast {
            position: fixed;
            top: 20px;
            right: 20px;
            background: #28a745;
            color: white;
            padding: 1rem;
            border-radius: 4px;
            display: none;
            z-index: 2000;
        }
        .htmx-indicator { opacity: 0; transition: opacity 300ms; }
        .htmx-indicator.htmx-request { opacity: 1; }
        .highlight { animation: highlight 2s; }
        @keyframes highlight { 0% { background: yellow; } 100% { background: transparent; } }
    </style>
</head>
<body>
    <h1>HTMX CRUD Operations</h1>

    <!-- Create User Button -->
    <div class="card">
        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
            <h2>User Management</h2>
            <button class="btn btn-success" onclick="openCreateModal()">
                ➕ Add New User
            </button>
        </div>

        <!-- Users Table -->
        <div id="users-container">
            <table class="table">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Name</th>
                        <th>Email</th>
                        <th>Role</th>
                        <th>Status</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody id="users-tbody">
                    <!-- Users will be loaded here -->
                </tbody>
            </table>
        </div>

        <div class="htmx-indicator">
            ⏳ Loading...
        </div>
    </div>

    <!-- Create/Edit Modal -->
    <div id="userModal" class="modal">
        <div class="modal-content">
            <span class="close" onclick="closeModal()">&times;</span>
            <h2 id="modalTitle">Add New User</h2>

            <form id="userForm">
                <input type="hidden" id="userId" name="id">

                <div class="form-group">
                    <label for="userName">Name:</label>
                    <input type="text" id="userName" name="name" class="form-control" required>
                </div>

                <div class="form-group">
                    <label for="userEmail">Email:</label>
                    <input type="email" id="userEmail" name="email" class="form-control" required>
                </div>

                <div class="form-group">
                    <label for="userRole">Role:</label>
                    <select id="userRole" name="role" class="form-control">
                        <option value="User">User</option>
                        <option value="Admin">Admin</option>
                        <option value="Moderator">Moderator</option>
                    </select>
                </div>

                <div class="form-group">
                    <label for="userStatus">Status:</label>
                    <select id="userStatus" name="status" class="form-control">
                        <option value="Active">Active</option>
                        <option value="Inactive">Inactive</option>
                        <option value="Suspended">Suspended</option>
                    </select>
                </div>

                <div>
                    <button type="submit" class="btn btn-success">Save User</button>
                    <button type="button" class="btn" onclick="closeModal()">Cancel</button>
                </div>
            </form>
        </div>
    </div>

    <!-- Toast Notification -->
    <div id="toast" class="toast"></div>

    <script>
        // Sample data
        let users = [
            { id: 1, name: 'John Doe', email: '[email protected]', role: 'Admin', status: 'Active' },
            { id: 2, name: 'Jane Smith', email: '[email protected]', role: 'User', status: 'Active' },
            { id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'Moderator', status: 'Inactive' }
        ];
        let nextId = 4;
        let editingUserId = null;

        // Load users on page load
        document.addEventListener('DOMContentLoaded', loadUsers);

        function loadUsers() {
            const tbody = document.getElementById('users-tbody');
            tbody.innerHTML = '';

            users.forEach(user => {
                const row = document.createElement('tr');
                row.id = 'user-row-' + user.id;
                row.innerHTML = `
                    <td>${user.id}</td>
                    <td>${user.name}</td>
                    <td>${user.email}</td>
                    <td><span class="badge badge-primary">${user.role}</span></td>
                    <td>
                        <span class="badge badge-${user.status === 'Active' ? 'success' : user.status === 'Inactive' ? 'secondary' : 'danger'}">
                            ${user.status}
                        </span>
                    </td>
                    <td>
                        <button class="btn btn-warning" onclick="editUser(${user.id})">Edit</button>
                        <button class="btn btn-danger" onclick="deleteUser(${user.id})">Delete</button>
                    </td>
                `;
                tbody.appendChild(row);
            });
        }

        function openCreateModal() {
            editingUserId = null;
            document.getElementById('modalTitle').textContent = 'Add New User';
            document.getElementById('userForm').reset();
            document.getElementById('userModal').style.display = 'block';
        }

        function editUser(id) {
            editingUserId = id;
            const user = users.find(u => u.id === id);

            document.getElementById('modalTitle').textContent = 'Edit User';
            document.getElementById('userId').value = user.id;
            document.getElementById('userName').value = user.name;
            document.getElementById('userEmail').value = user.email;
            document.getElementById('userRole').value = user.role;
            document.getElementById('userStatus').value = user.status;
            document.getElementById('userModal').style.display = 'block';
        }

        function closeModal() {
            document.getElementById('userModal').style.display = 'none';
            document.getElementById('userForm').reset();
            editingUserId = null;
        }

        function deleteUser(id) {
            if (confirm('Are you sure you want to delete this user?')) {
                // Simulate HTMX delete request
                const row = document.getElementById('user-row-' + id);
                row.style.transition = 'opacity 300ms';
                row.style.opacity = '0';

                setTimeout(() => {
                    users = users.filter(u => u.id !== id);
                    loadUsers();
                    showToast('User deleted successfully!', 'success');
                }, 300);
            }
        }

        // Form submission with HTMX simulation
        document.getElementById('userForm').addEventListener('submit', function(e) {
            e.preventDefault();

            const formData = new FormData(this);
            const userData = Object.fromEntries(formData.entries());

            if (editingUserId) {
                // Update existing user
                const index = users.findIndex(u => u.id === parseInt(userData.id));
                users[index] = { ...userData, id: parseInt(userData.id) };
                showToast('User updated successfully!', 'success');
            } else {
                // Create new user
                users.push({ ...userData, id: nextId++ });
                showToast('User created successfully!', 'success');
            }

            loadUsers();
            closeModal();
        });

        function showToast(message, type = 'success') {
            const toast = document.getElementById('toast');
            toast.textContent = message;
            toast.style.background = type === 'success' ? '#28a745' : '#dc3545';
            toast.style.display = 'block';

            setTimeout(() => {
                toast.style.display = 'none';
            }, 3000);
        }

        // HTMX-like event handling for demonstration
        document.body.addEventListener('htmx:beforeRequest', function(event) {
            console.log('HTMX request intercepted:', event.detail);
            // Here you would make actual AJAX requests in a real app
        });

        // Close modal when clicking outside
        window.onclick = function(event) {
            const modal = document.getElementById('userModal');
            if (event.target === modal) {
                closeModal();
            }
        }
    </script>
</body>
</html>

💻 Integración HTMX WebSockets html

🟡 intermediate ⭐⭐⭐⭐

Actualizaciones en tiempo real usando HTMX con conexiones WebSocket

⏱️ 30 min 🏷️ htmx, websocket, real-time
Prerequisites: HTMX basics, understanding of WebSockets
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX WebSockets Integration</title>
    <script src="https://unpkg.com/htmx.org@latest"></script>
    <script src="https://unpkg.com/htmx.org@latest/dist/ext/ws.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;
            margin-right: 0.5rem;
        }
        .btn:hover { background: #0056b3; }
        .btn-success { background: #28a745; }
        .btn-success:hover { background: #1e7e34; }
        .btn-danger { background: #dc3545; }
        .btn-danger:hover { background: #c82333; }
        .btn-warning { background: #ffc107; color: #000; }
        .btn-warning:hover { background: #e0a800; }
        .chat-container {
            height: 400px;
            overflow-y: auto;
            border: 1px solid #ddd;
            padding: 1rem;
            background: #f8f9fa;
            border-radius: 4px;
        }
        .message {
            margin-bottom: 1rem;
            padding: 0.5rem;
            border-radius: 4px;
        }
        .message-sent {
            background: #007bff;
            color: white;
            margin-left: 20%;
            text-align: right;
        }
        .message-received {
            background: #e9ecef;
            margin-right: 20%;
        }
        .message-system {
            background: #fff3cd;
            text-align: center;
            font-style: italic;
            margin: 0 20%;
        }
        .typing-indicator {
            color: #6c757d;
            font-style: italic;
        }
        .status-indicator {
            display: inline-block;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            margin-right: 0.5rem;
        }
        .status-connected { background: #28a745; }
        .status-disconnected { background: #dc3545; }
        .status-connecting { background: #ffc107; }
        .online-users {
            list-style: none;
            padding: 0;
        }
        .online-users li {
            padding: 0.5rem;
            border-bottom: 1px solid #eee;
        }
        .stock-ticker {
            font-family: monospace;
            font-size: 1.2rem;
            padding: 1rem;
            background: #f8f9fa;
            border-radius: 4px;
            margin-bottom: 1rem;
        }
        .stock-up { color: #28a745; }
        .stock-down { color: #dc3545; }
        .notification {
            position: fixed;
            top: 20px;
            right: 20px;
            background: #17a2b8;
            color: white;
            padding: 1rem;
            border-radius: 4px;
            max-width: 300px;
            display: none;
            z-index: 1000;
        }
    </style>
</head>
<body>
    <h1>HTMX WebSockets Integration</h1>

    <!-- Connection Status -->
    <div class="card">
        <h2>WebSocket Connection</h2>
        <div>
            <span class="status-indicator status-connecting" id="statusIndicator"></span>
            <span id="connectionStatus">Connecting...</span>
        </div>
        <button class="btn" onclick="connectWebSocket()">Connect</button>
        <button class="btn btn-danger" onclick="disconnectWebSocket()">Disconnect</button>
    </div>

    <!-- Real-time Chat -->
    <div class="card">
        <h2>Real-time Chat</h2>
        <p>Send and receive messages in real-time using WebSockets.</p>

        <div class="chat-container" id="chatMessages">
            <div class="message message-system">
                Welcome to the chat! Messages will appear here.
            </div>
        </div>

        <form onsubmit="sendMessage(event)">
            <div style="display: flex; margin-top: 1rem;">
                <input type="text" id="messageInput"
                       placeholder="Type a message..."
                       style="flex: 1; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; margin-right: 0.5rem;">
                <button type="submit" class="btn btn-success">Send</button>
            </div>
        </form>

        <div id="typingIndicator" class="typing-indicator" style="display: none; margin-top: 0.5rem;">
            Someone is typing...
        </div>
    </div>

    <!-- Online Users -->
    <div class="card">
        <h2>Online Users</h2>
        <div
            hx-ext="ws"
            ws-connect="/ws/users"
            ws-send="get_users"
            hx-trigger="every 5s"
            hx-swap="innerHTML">

            <ul class="online-users" id="onlineUsers">
                <li>Loading users...</li>
            </ul>
        </div>

        <small class="text-muted">Updates every 5 seconds</small>
    </div>

    <!-- Live Stock Ticker -->
    <div class="card">
        <h2>Live Stock Ticker</h2>
        <p>Real-time stock price updates using WebSocket streaming.</p>

        <div
            hx-ext="ws"
            ws-connect="/ws/stocks"
            hx-trigger="receivedMessage from:body"
            hx-swap="innerHTML"
            id="stockTicker">
            <div class="stock-ticker">
                <div>AAPL: $150.25 <span class="stock-up">↑+2.15%</span></div>
                <div>GOOGL: $2,845.30 <span class="stock-down">↓-0.85%</span></div>
                <div>MSFT: $305.80 <span class="stock-up">↑+1.42%</span></div>
                <div>TSLA: $245.60 <span class="stock-down">↓-3.21%</span></div>
            </div>
        </div>

        <button class="btn btn-warning" onclick="toggleStockUpdates()">
            Toggle Updates
        </button>
    </div>

    <!-- Live Notifications -->
    <div class="card">
        <h2>Live Notifications</h2>
        <p>Receive real-time notifications and system alerts.</p>

        <div
            hx-ext="ws"
            ws-connect="/ws/notifications"
            hx-trigger="receivedMessage from:body"
            hx-swap="beforebegin"
            id="notificationsList">
            <!-- Notifications will be added here -->
        </div>

        <button class="btn btn-success" onclick="sendTestNotification()">
            Send Test Notification
        </button>
    </div>

    <!-- Real-time Collaboration -->
    <div class="card">
        <h2>Collaborative Document</h2>
        <p>See live updates as users type in the shared document.</p>

        <div
            hx-ext="ws"
            ws-connect="/ws/document"
            ws-send="get_document"
            hx-trigger="load"
            id="documentContainer">

            <textarea
                id="sharedDocument"
                placeholder="Start typing to see real-time collaboration..."
                style="width: 100%; height: 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 4px;"
                onkeyup="syncDocument()"></textarea>
        </div>

        <div id="activeUsers" style="margin-top: 1rem; color: #6c757d;">
            <small>Active editors: None</small>
        </div>
    </div>

    <!-- Notification Toast -->
    <div id="notification" class="notification"></div>

    <script>
        // WebSocket connection management
        let ws = null;
        let wsUrl = 'ws://localhost:8080'; // Mock WebSocket URL
        let stockUpdateInterval = null;
        let notificationInterval = null;

        // Simulate WebSocket connection
        function connectWebSocket() {
            updateConnectionStatus('connecting');

            // Simulate connection delay
            setTimeout(() => {
                updateConnectionStatus('connected');
                startMockDataStream();
                showToast('Connected to WebSocket!', 'success');
            }, 1000);
        }

        function disconnectWebSocket() {
            updateConnectionStatus('disconnected');
            stopMockDataStream();
            showToast('Disconnected from WebSocket', 'warning');
        }

        function updateConnectionStatus(status) {
            const indicator = document.getElementById('statusIndicator');
            const statusText = document.getElementById('connectionStatus');

            indicator.className = 'status-indicator status-' + status;

            switch(status) {
                case 'connected':
                    statusText.textContent = 'Connected';
                    break;
                case 'disconnected':
                    statusText.textContent = 'Disconnected';
                    break;
                case 'connecting':
                    statusText.textContent = 'Connecting...';
                    break;
            }
        }

        // Chat functionality
        function sendMessage(event) {
            event.preventDefault();
            const input = document.getElementById('messageInput');
            const message = input.value.trim();

            if (message) {
                addChatMessage(message, 'sent');
                input.value = '';

                // Simulate receiving a response
                setTimeout(() => {
                    const responses = [
                        'That's interesting!',
                        'Tell me more.',
                        'I agree with you.',
                        'Great point!',
                        'Thanks for sharing!'
                    ];
                    const randomResponse = responses[Math.floor(Math.random() * responses.length)];
                    addChatMessage(randomResponse, 'received');
                }, 1000 + Math.random() * 2000);

                // Show typing indicator
                showTypingIndicator();
            }
        }

        function addChatMessage(message, type) {
            const container = document.getElementById('chatMessages');
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message message-' + type;
            messageDiv.textContent = message;
            container.appendChild(messageDiv);
            container.scrollTop = container.scrollHeight;
        }

        function showTypingIndicator() {
            const indicator = document.getElementById('typingIndicator');
            indicator.style.display = 'block';

            setTimeout(() => {
                indicator.style.display = 'none';
            }, 2000);
        }

        // Mock online users
        function updateOnlineUsers() {
            const users = [
                'John Doe',
                'Jane Smith',
                'Alice Johnson',
                'Bob Wilson',
                'Emma Davis'
            ];

            const userList = document.getElementById('onlineUsers');
            const randomCount = Math.floor(Math.random() * users.length) + 1;
            const selectedUsers = users.slice(0, randomCount);

            userList.innerHTML = selectedUsers.map(user =>
                '<li>🟢 ' + user + '</li>'
            ).join('');
        }

        // Mock stock updates
        function updateStocks() {
            const stocks = [
                { symbol: 'AAPL', price: 150.25 },
                { symbol: 'GOOGL', price: 2845.30 },
                { symbol: 'MSFT', price: 305.80 },
                { symbol: 'TSLA', price: 245.60 }
            ];

            const tickerHtml = stocks.map(stock => {
                const change = (Math.random() - 0.5) * 10;
                const changePercent = (change / stock.price * 100).toFixed(2);
                const newPrice = (stock.price + change).toFixed(2);
                const changeClass = change > 0 ? 'stock-up' : 'stock-down';
                const changeSymbol = change > 0 ? '↑' : '↓';

                return `
                    <div>${stock.symbol}: $${newPrice}
                    <span class="${changeClass}">${changeSymbol}${changePercent}%</span></div>
                `;
            }).join('');

            document.getElementById('stockTicker').innerHTML =
                '<div class="stock-ticker">' + tickerHtml + '</div>';
        }

        // Mock notifications
        function generateNotification() {
            const notifications = [
                'New user registered',
                'Server maintenance scheduled',
                'Database backup completed',
                'Security update available',
                'Performance optimization done'
            ];

            const notification = notifications[Math.floor(Math.random() * notifications.length)];
            showNotification(notification);
        }

        // Document collaboration
        function syncDocument() {
            const content = document.getElementById('sharedDocument').value;

            // Update active users indicator
            const activeCount = Math.floor(Math.random() * 3) + 1;
            const activeUsersText = activeCount > 0
                ? `Active editors: ${activeCount} user(s)`
                : 'Active editors: None';

            document.getElementById('activeUsers').innerHTML =
                '<small>' + activeUsersText + '</small>';
        }

        // Utility functions
        function startMockDataStream() {
            // Update online users every 5 seconds
            setInterval(updateOnlineUsers, 5000);
            updateOnlineUsers();

            // Update stocks every 2 seconds
            stockUpdateInterval = setInterval(updateStocks, 2000);
            updateStocks();

            // Generate notifications every 10-30 seconds
            notificationInterval = setInterval(generateNotification, 10000 + Math.random() * 20000);
        }

        function stopMockDataStream() {
            if (stockUpdateInterval) {
                clearInterval(stockUpdateInterval);
            }
            if (notificationInterval) {
                clearInterval(notificationInterval);
            }
        }

        function toggleStockUpdates() {
            if (stockUpdateInterval) {
                clearInterval(stockUpdateInterval);
                stockUpdateInterval = null;
                showToast('Stock updates paused', 'warning');
            } else {
                stockUpdateInterval = setInterval(updateStocks, 2000);
                showToast('Stock updates resumed', 'success');
            }
        }

        function sendTestNotification() {
            showNotification('This is a test notification from the user!');
        }

        function showNotification(message) {
            const notification = document.getElementById('notification');
            notification.textContent = message;
            notification.style.display = 'block';

            setTimeout(() => {
                notification.style.display = 'none';
            }, 5000);
        }

        function showToast(message, type) {
            showNotification(message);
        }

        // Auto-connect on page load
        document.addEventListener('DOMContentLoaded', () => {
            connectWebSocket();
        });

        // Simulate WebSocket events
        document.body.addEventListener('htmx:wsConnecting', function(event) {
            console.log('WebSocket connecting...');
        });

        document.body.addEventListener('htmx:wsOpen', function(event) {
            console.log('WebSocket connected');
        });

        document.body.addEventListener('htmx:wsClose', function(event) {
            console.log('WebSocket disconnected');
        });

        document.body.addEventListener('htmx:wsMessage', function(event) {
            console.log('WebSocket message received:', event.detail);
        });
    </script>
</body>
</html>

💻 Patrones Avanzados HTMX html

🔴 complex ⭐⭐⭐⭐

Patrones avanzados HTMX incluyendo carga perezosa, scroll infinito y subida de archivos

⏱️ 40 min 🏷️ htmx, advanced, patterns
Prerequisites: Advanced HTMX knowledge, JavaScript concepts
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX Advanced Patterns</title>
    <script src="https://unpkg.com/htmx.org@latest"></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;
            margin-right: 0.5rem;
        }
        .btn:hover { background: #0056b3; }
        .btn-success { background: #28a745; }
        .btn-success:hover { background: #1e7e34; }
        .btn-warning { background: #ffc107; color: #000; }
        .btn-warning:hover { background: #e0a800; }
        .loading {
            opacity: 0.7;
            transition: opacity 300ms;
        }
        .skeleton {
            background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
            background-size: 200% 100%;
            animation: loading 1.5s infinite;
        }
        @keyframes loading {
            0% { background-position: 200% 0; }
            100% { background-position: -200% 0; }
        }
        .image-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 1rem;
            margin-top: 1rem;
        }
        .image-card {
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: transform 0.2s;
        }
        .image-card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.15);
        }
        .image-card img {
            width: 100%;
            height: 150px;
            object-fit: cover;
        }
        .image-card .caption {
            padding: 0.5rem;
            background: #f8f9fa;
            font-size: 0.875rem;
        }
        .file-upload-area {
            border: 2px dashed #ddd;
            border-radius: 8px;
            padding: 2rem;
            text-align: center;
            transition: border-color 0.3s;
            cursor: pointer;
        }
        .file-upload-area.dragover {
            border-color: #007bff;
            background: #f8f9ff;
        }
        .progress-bar {
            width: 100%;
            height: 20px;
            background: #e9ecef;
            border-radius: 10px;
            overflow: hidden;
            margin: 1rem 0;
        }
        .progress-fill {
            height: 100%;
            background: #007bff;
            transition: width 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 0.875rem;
        }
        .autocomplete {
            position: relative;
        }
        .autocomplete-results {
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: white;
            border: 1px solid #ddd;
            border-top: none;
            border-radius: 0 0 4px 4px;
            max-height: 200px;
            overflow-y: auto;
            z-index: 1000;
        }
        .autocomplete-item {
            padding: 0.5rem;
            cursor: pointer;
        }
        .autocomplete-item:hover, .autocomplete-item.selected {
            background: #f8f9fa;
        }
        .debounce-indicator {
            font-size: 0.875rem;
            color: #6c757d;
            margin-left: 0.5rem;
        }
        .infinite-scroll-container {
            height: 400px;
            overflow-y: auto;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .infinite-scroll-item {
            padding: 1rem;
            border-bottom: 1px solid #eee;
        }
        .infinite-scroll-item:last-child {
            border-bottom: none;
        }
        .lazy-image {
            min-height: 200px;
            background: #f0f0f0;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #6c757d;
        }
    </style>
</head>
<body>
    <h1>HTMX Advanced Patterns</h1>

    <!-- Lazy Loading with Intersection Observer -->
    <div class="card">
        <h2>Lazy Loading with Intersection Observer</h2>
        <p>Content is loaded only when it becomes visible in the viewport.</p>

        <div id="lazy-content">
            <!-- Initially visible content -->
            <p>This content is visible immediately.</p>

            <!-- Lazy loaded content -->
            <div
                hx-get="/api/lazy-content"
                hx-trigger="revealed"
                hx-swap="innerHTML"
                hx-indicator=".loading-indicator">
                <div class="loading-indicator">
                    <div class="skeleton" style="height: 100px; margin: 1rem 0; border-radius: 4px;"></div>
                </div>
            </div>
        </div>
    </div>

    <!-- Infinite Scroll -->
    <div class="card">
        <h2>Infinite Scroll</h2>
        <p>Load more content as you scroll down.</p>

        <div
            class="infinite-scroll-container"
            hx-get="/api/more-items?page=1"
            hx-trigger="scroll(bottom)"
            hx-swap="innerHTML"
            hx-target="#items-container">

            <div id="items-container">
                <!-- Initial items -->
                <div class="infinite-scroll-item">Item 1</div>
                <div class="infinite-scroll-item">Item 2</div>
                <div class="infinite-scroll-item">Item 3</div>
            </div>

            <div class="htmx-indicator">
                <div style="text-align: center; padding: 1rem;">
                    <div class="spinner"></div> Loading more items...
                </div>
            </div>
        </div>
    </div>

    <!-- Image Gallery with Lazy Loading -->
    <div class="card">
        <h2>Image Gallery with Lazy Loading</h2>
        <p>Images are loaded as they come into view.</p>

        <div class="image-grid">
            <!-- Lazy loaded images -->
            <div class="image-card">
                <div
                    hx-get="/api/image/1"
                    hx-trigger="revealed"
                    hx-swap="innerHTML">
                    <div class="lazy-image">Loading image...</div>
                </div>
                <div class="caption">Mountain Landscape</div>
            </div>

            <div class="image-card">
                <div
                    hx-get="/api/image/2"
                    hx-trigger="revealed"
                    hx-swap="innerHTML">
                    <div class="lazy-image">Loading image...</div>
                </div>
                <div class="caption">Ocean Sunset</div>
            </div>

            <div class="image-card">
                <div
                    hx-get="/api/image/3"
                    hx-trigger="revealed"
                    hx-swap="innerHTML">
                    <div class="lazy-image">Loading image...</div>
                </div>
                <div class="caption">Forest Path</div>
            </div>

            <div class="image-card">
                <div
                    hx-get="/api/image/4"
                    hx-trigger="revealed"
                    hx-swap="innerHTML">
                    <div class="lazy-image">Loading image...</div>
                </div>
                <div class="caption">City Skyline</div>
            </div>
        </div>
    </div>

    <!-- Debounced Search -->
    <div class="card">
        <h2>Debounced Search with Autocomplete</h2>
        <p>Search with autocomplete and debounced requests.</p>

        <div class="autocomplete">
            <input
                type="text"
                id="searchInput"
                placeholder="Search for products..."
                hx-get="/api/search?q={value}"
                hx-trigger="keyup changed delay:500ms"
                hx-target="#search-results"
                hx-swap="innerHTML"
                class="form-control"
                style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%;">
            <span class="debounce-indicator" id="debounceIndicator"></span>

            <div id="search-results" class="autocomplete-results"></div>
        </div>
    </div>

    <!-- File Upload with Progress -->
    <div class="card">
        <h2>File Upload with Progress</h2>
        <p>Upload files with progress indicators.</p>

        <form
            hx-post="/api/upload"
            hx-encoding="multipart/form-data"
            hx-target="#upload-result"
            hx-swap="innerHTML"
            hx-on::before-request="showUploadProgress()">

            <div
                class="file-upload-area"
                id="dropZone"
                ondrop="handleDrop(event)"
                ondragover="handleDragOver(event)"
                ondragleave="handleDragLeave(event)">

                <input
                    type="file"
                    id="fileInput"
                    name="file"
                    multiple
                    onchange="handleFileSelect(event)"
                    style="display: none;">

                <div id="upload-prompt">
                    <div style="font-size: 3rem; margin-bottom: 1rem;">📁</div>
                    <p>Drag and drop files here or click to browse</p>
                    <button type="button" class="btn" onclick="document.getElementById('fileInput').click()">
                        Choose Files
                    </button>
                </div>

                <div id="upload-progress" style="display: none;">
                    <div class="progress-bar">
                        <div class="progress-fill" id="progressFill">0%</div>
                    </div>
                    <p id="upload-status">Uploading...</p>
                </div>
            </div>

            <button type="submit" class="btn btn-success" style="margin-top: 1rem;">
                Upload Files
            </button>
        </form>

        <div id="upload-result" style="margin-top: 1rem;"></div>
    </div>

    <!-- Request Caching -->
    <div class="card">
        <h2>Request Caching</h2>
        <p>Demonstrates HTMX caching capabilities.</p>

        <button
            hx-get="/api/cached-data"
            hx-trigger="click"
            hx-target="#cached-content"
            hx-swap="innerHTML"
            hx-cache="true">
            Load Cached Data
        </button>

        <button
            onclick="clearCache()"
            class="btn btn-warning">
            Clear Cache
        </button>

        <div id="cached-content" style="margin-top: 1rem;">
            <em>Click to load data (will be cached for subsequent requests)</em>
        </div>

        <div id="cache-status" style="margin-top: 0.5rem; font-size: 0.875rem; color: #6c757d;"></div>
    </div>

    <!-- Request Validation -->
    <div class="card">
        <h2>Client-Side Validation</h2>
        <p>Validate input before sending requests.</p>

        <form
            hx-post="/api/validate-form"
            hx-target="#validation-result"
            hx-swap="innerHTML"
            hx-on::before-request="validateForm(event)">

            <div class="form-group">
                <label>Email:</label>
                <input
                    type="email"
                    name="email"
                    required
                    class="form-control"
                    style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%;">
            </div>

            <div class="form-group">
                <label>Password (min 8 chars):</label>
                <input
                    type="password"
                    name="password"
                    minlength="8"
                    required
                    class="form-control"
                    style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%;">
            </div>

            <button type="submit" class="btn btn-success">Submit</button>
        </form>

        <div id="validation-result" style="margin-top: 1rem;"></div>
    </div>

    <!-- Custom Events -->
    <div class="card">
        <h2>Custom Events and Triggers</h2>
        <p>Using custom events to trigger HTMX requests.</p>

        <div id="custom-event-source">
            <button onclick="triggerCustomEvent()" class="btn">
                Trigger Custom Event
            </button>

            <div
                hx-get="/api/custom-event-data"
                hx-trigger="customLoad from:#custom-event-source"
                hx-target="#custom-event-content"
                hx-swap="innerHTML">

                <div id="custom-event-content" style="margin-top: 1rem;">
                    <em>Waiting for custom event...</em>
                </div>
            </div>
        </div>
    </div>

    <script>
        // Mock API responses
        document.addEventListener('htmx:beforeRequest', function(event) {
            const url = event.detail.requestConfig.url;

            // Mock lazy content
            if (url.includes('/api/lazy-content')) {
                event.preventDefault();
                setTimeout(() => {
                    document.querySelector('[hx-get*="lazy-content"]').innerHTML =
                        '<div style="background: #e7f3fe; padding: 1rem; border-radius: 4px; margin-top: 1rem;">' +
                        '<h3>Lazy Loaded Content! 🎉</h3>' +
                        '<p>This content was loaded when it came into view.</p>' +
                        '<p>Current time: ' + new Date().toLocaleTimeString() + '</p>' +
                        '</div>';
                }, 1000);
                return;
            }

            // Mock infinite scroll
            if (url.includes('/api/more-items')) {
                event.preventDefault();
                setTimeout(() => {
                    const pageMatch = url.match(/page=(\d+)/);
                    const page = pageMatch ? parseInt(pageMatch[1]) : 1;

                    if (page < 5) {
                        const container = document.getElementById('items-container');
                        for (let i = 1; i <= 3; i++) {
                            const itemNum = (page - 1) * 3 + i + 3;
                            const newItem = document.createElement('div');
                            newItem.className = 'infinite-scroll-item';
                            newItem.textContent = `Item ${itemNum}`;
                            container.appendChild(newItem);
                        }

                        // Update the trigger for next page
                        const trigger = document.querySelector('[hx-get*="more-items"]');
                        trigger.setAttribute('hx-get', `/api/more-items?page=${page + 1}`);
                    } else {
                        // No more items
                        document.querySelector('.htmx-indicator').innerHTML =
                            '<div style="text-align: center; padding: 1rem; color: #6c757d;">No more items to load</div>';
                    }
                }, 500);
                return;
            }

            // Mock image loading
            if (url.includes('/api/image/')) {
                event.preventDefault();
                setTimeout(() => {
                    const imageId = url.split('/').pop();
                    const colors = ['#e3f2fd', '#f3e5f5', '#e8f5e8', '#fff3e0'];
                    const color = colors[parseInt(imageId) - 1];

                    event.target.closest('[hx-get*="image"]').innerHTML =
                        `<img src="https://picsum.photos/200/150?random=${imageId}" alt="Image ${imageId}"> `;
                }, 800);
                return;
            }

            // Mock search
            if (url.includes('/api/search')) {
                event.preventDefault();

                // Show debounce indicator
                document.getElementById('debounceIndicator').textContent = '⏳ Searching...';

                setTimeout(() => {
                    const searchParams = new URLSearchParams(url.split('?')[1]);
                    const query = searchParams.get('q');

                    document.getElementById('debounceIndicator').textContent = '';

                    if (query && query.length > 0) {
                        const products = [
                            'Laptop Pro 15"',
                            'Wireless Mouse',
                            'Mechanical Keyboard',
                            'USB-C Hub',
                            'Monitor 4K',
                            'Webcam HD',
                            'Headphones Pro',
                            'Phone Stand'
                        ];

                        const filtered = products.filter(p =>
                            p.toLowerCase().includes(query.toLowerCase())
                        );

                        const resultsHtml = filtered.length > 0
                            ? filtered.map(p => `<div class="autocomplete-item">${p}</div>`).join('')
                            : '<div class="autocomplete-item">No results found</div>';

                        document.getElementById('search-results').innerHTML = resultsHtml;
                    } else {
                        document.getElementById('search-results').innerHTML = '';
                    }
                }, 300);
                return;
            }

            // Mock file upload
            if (url.includes('/api/upload')) {
                event.preventDefault();
                simulateFileUpload();
                return;
            }

            // Mock cached data
            if (url.includes('/api/cached-data')) {
                event.preventDefault();
                const isCached = htmx.cache ? Object.keys(htmx.cache).includes(url) : false;

                setTimeout(() => {
                    const timestamp = new Date().toLocaleTimeString();
                    document.getElementById('cached-content').innerHTML =
                        '<div style="background: #d4edda; padding: 1rem; border-radius: 4px;">' +
                        '<h3>Cached Data</h3>' +
                        '<p>Loaded at: ' + timestamp + '</p>' +
                        '<p>' + (isCached ? '📦 Served from cache' : '🌐 Fresh from server') + '</p>' +
                        '</div>';

                    document.getElementById('cache-status').textContent =
                        isCached ? '✅ This response was cached' : '📦 Response cached for future requests';
                }, 500);
                return;
            }

            // Mock custom event data
            if (url.includes('/api/custom-event-data')) {
                event.preventDefault();
                setTimeout(() => {
                    document.getElementById('custom-event-content').innerHTML =
                        '<div style="background: #fff3cd; padding: 1rem; border-radius: 4px;">' +
                        '<h3>Custom Event Triggered! 🎯</h3>' +
                        '<p>Data loaded via custom event trigger.</p>' +
                        '<p>Event time: ' + new Date().toLocaleTimeString() + '</p>' +
                        '</div>';
                }, 300);
                return;
            }
        });

        // File upload handlers
        function handleFileSelect(event) {
            const files = event.target.files;
            if (files.length > 0) {
                updateFileList(files);
            }
        }

        function handleDrop(event) {
            event.preventDefault();
            event.stopPropagation();

            const dropZone = document.getElementById('dropZone');
            dropZone.classList.remove('dragover');

            const files = event.dataTransfer.files;
            if (files.length > 0) {
                updateFileList(files);
            }
        }

        function handleDragOver(event) {
            event.preventDefault();
            document.getElementById('dropZone').classList.add('dragover');
        }

        function handleDragLeave(event) {
            event.preventDefault();
            document.getElementById('dropZone').classList.remove('dragover');
        }

        function updateFileList(files) {
            const fileList = Array.from(files).map(f => f.name).join(', ');
            document.getElementById('upload-prompt').innerHTML =
                '<div style="font-size: 1.5rem; margin-bottom: 1rem;">📄</div>' +
                '<p><strong>Selected files:</strong></p>' +
                '<p>' + fileList + '</p>';
        }

        function showUploadProgress() {
            document.getElementById('upload-prompt').style.display = 'none';
            document.getElementById('upload-progress').style.display = 'block';
        }

        function simulateFileUpload() {
            let progress = 0;
            const interval = setInterval(() => {
                progress += Math.random() * 15;
                if (progress > 100) progress = 100;

                document.getElementById('progressFill').style.width = progress + '%';
                document.getElementById('progressFill').textContent = Math.round(progress) + '%';

                if (progress >= 100) {
                    clearInterval(interval);
                    document.getElementById('upload-status').textContent = 'Upload complete!';
                    document.getElementById('upload-result').innerHTML =
                        '<div style="background: #d4edda; padding: 1rem; border-radius: 4px;">' +
                        '<h4>✅ Upload Successful!</h4>' +
                        '<p>Files have been uploaded successfully.</p>' +
                        '</div>';

                    // Reset after 2 seconds
                    setTimeout(() => {
                        document.getElementById('upload-prompt').style.display = 'block';
                        document.getElementById('upload-progress').style.display = 'none';
                        document.getElementById('progressFill').style.width = '0%';
                        document.getElementById('fileInput').value = '';
                    }, 2000);
                }
            }, 100);
        }

        // Validation
        function validateForm(event) {
            const form = event.target;
            const email = form.querySelector('input[name="email"]').value;
            const password = form.querySelector('input[name="password"]').value;

            if (!email || !password) {
                event.preventDefault();
                document.getElementById('validation-result').innerHTML =
                    '<div style="background: #f8d7da; padding: 1rem; border-radius: 4px;">' +
                    '<p style="color: #721c24;">Please fill in all required fields.</p>' +
                    '</div>';
                return false;
            }

            if (password.length < 8) {
                event.preventDefault();
                document.getElementById('validation-result').innerHTML =
                    '<div style="background: #f8d7da; padding: 1rem; border-radius: 4px;">' +
                    '<p style="color: #721c24;">Password must be at least 8 characters long.</p>' +
                    '</div>';
                return false;
            }

            return true;
        }

        // Custom event
        function triggerCustomEvent() {
            const source = document.getElementById('custom-event-source');
            const event = new CustomEvent('customLoad', {
                bubbles: true,
                detail: { timestamp: Date.now() }
            });
            source.dispatchEvent(event);
        }

        // Cache management
        function clearCache() {
            if (htmx && htmx.cache) {
                htmx.cache = {};
                document.getElementById('cached-content').innerHTML =
                    '<em>Cache cleared. Click to load fresh data.</em>';
                document.getElementById('cache-status').textContent = '';
            }
        }

        // Initialize
        document.addEventListener('DOMContentLoaded', function() {
            // Setup intersection observer for lazy loading
            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        entry.target.dispatchEvent(new Event('revealed'));
                        observer.unobserve(entry.target);
                    }
                });
            }, {
                rootMargin: '50px'
            });

            // Observe lazy loading elements
            document.querySelectorAll('[hx-trigger="revealed"]').forEach(el => {
                observer.observe(el);
            });
        });
    </script>
</body>
</html>