Exemples HTMX

Exemples de la librairie HTMX pour les applications web modernes avec interactions HTML dynamiques

💻 HTMX Hello World html

🟢 simple

Configuration de base HTMX et exemple d'interactions 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
            hx-get="/api/content/{value}"
            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 = url.split('/').pop();
                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>

💻 Opérations CRUD HTMX html

🟡 intermediate ⭐⭐⭐

Opérations CRUD complètes (Créer, Lire, Mettre à jour, Supprimer) avec 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>

💻 Intégration HTMX WebSockets html

🟡 intermediate ⭐⭐⭐⭐

Mises à jour en temps réel en utilisant HTMX avec des connexions 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>

💻 Patterns Avancés HTMX html

🔴 complex ⭐⭐⭐⭐

Patterns avancés HTMX incluant le chargement paresseux, le défilement infini et le téléchargement de fichiers

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