Skip to content

AJAX

SØAD provides seamless AJAX support through JSON responses and form handling. This recipe demonstrates how to create interactive web applications using different JavaScript approaches - from vanilla JavaScript to modern libraries like jQuery and Axios.


Basic AJAX with Vanilla JavaScript

user_search.py
from utils import render
from models import User
import json

class User_search(object):
    def view(self, ctx):
        # Load the initial HTML page
        ctx.go_to = render.as_view(ctx, "user_search")

    def search(self, ctx):
        # Handle AJAX search request
        request = ctx.getRequest()
        query = request.getParameter("q") or ""

        if len(query) < 2:
            result = {"users": [], "message": "Please enter at least 2 characters"}
        else:
            # Search users by name or email
            users = User.where("name LIKE ? OR email LIKE ?", 
                             "%%%s%%" % query, "%%%s%%" % query).limit(10)

            user_list = []
            for user in users:
                user_list.append({
                    "id": user.get("id"),
                    "name": user.get("name"),
                    "email": user.get("email"),
                    "department": user.get("department")
                })

            result = {
                "users": user_list,
                "total": len(user_list),
                "query": query
            }

        ctx.go_to = render.as_json(ctx, result)

    def details(self, ctx):
        # Get user details by ID
        request = ctx.getRequest()
        user_id = request.getParameter("id")

        if not user_id:
            result = {"error": "User ID is required"}
        else:
            user = User.findById(user_id)
            if user:
                result = {
                    "user": {
                        "id": user.get("id"),
                        "name": user.get("name"),
                        "email": user.get("email"),
                        "department": user.get("department"),
                        "phone": user.get("phone"),
                        "created_at": str(user.get("created_at"))
                    }
                }
            else:
                result = {"error": "User not found"}

        ctx.go_to = render.as_json(ctx, result)
user_search.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Search - Vanilla JavaScript</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .loading { display: none; }
        .user-card { cursor: pointer; transition: background-color 0.2s; }
        .user-card:hover { background-color: #f8f9fa; }
    </style>
</head>
<body>
    <div class="container mt-4">
        <h2>User Search</h2>

        <!-- Search Form -->
        <div class="row mb-4">
            <div class="col-md-6">
                <div class="input-group">
                    <input type="text" id="searchInput" class="form-control" 
                           placeholder="Search users by name or email...">
                    <button class="btn btn-primary" type="button" onclick="searchUsers()">
                        <span class="loading spinner-border spinner-border-sm me-2" role="status"></span>
                        Search
                    </button>
                </div>
                <div id="message" class="mt-2"></div>
            </div>
        </div>

        <!-- Search Results -->
        <div id="results" class="row"></div>

        <!-- User Details Modal -->
        <div class="modal fade" id="userModal" tabindex="-1">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">User Details</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                    </div>
                    <div class="modal-body" id="userDetails">
                        <!-- User details will be loaded here -->
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
    <script>
        // Search users using vanilla JavaScript
        function searchUsers() {
            const query = document.getElementById('searchInput').value;
            const loadingSpinner = document.querySelector('.loading');
            const messageDiv = document.getElementById('message');
            const resultsDiv = document.getElementById('results');

            // Show loading state
            loadingSpinner.style.display = 'inline-block';
            messageDiv.innerHTML = '';
            resultsDiv.innerHTML = '';

            // Create XMLHttpRequest
            const xhr = new XMLHttpRequest();
            xhr.open('POST', '/t/example/user_search/search', true);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    loadingSpinner.style.display = 'none';

                    if (xhr.status === 200) {
                        try {
                            const response = JSON.parse(xhr.responseText);
                            displayResults(response);
                        } catch (e) {
                            messageDiv.innerHTML = '<div class="alert alert-danger">Error parsing response</div>';
                        }
                    } else {
                        messageDiv.innerHTML = '<div class="alert alert-danger">Request failed</div>';
                    }
                }
            };

            xhr.send('q=' + encodeURIComponent(query));
        }

        // Display search results
        function displayResults(response) {
            const messageDiv = document.getElementById('message');
            const resultsDiv = document.getElementById('results');

            if (response.message) {
                messageDiv.innerHTML = '<div class="alert alert-info">' + response.message + '</div>';
                return;
            }

            if (response.users.length === 0) {
                messageDiv.innerHTML = '<div class="alert alert-info">No users found</div>';
                return;
            }

            messageDiv.innerHTML = '<div class="alert alert-success">Found ' + response.total + ' user(s)</div>';

            let html = '';
            response.users.forEach(function(user) {
                html += `
                    <div class="col-md-4 mb-3">
                        <div class="card user-card" onclick="showUserDetails(${user.id})">
                            <div class="card-body">
                                <h5 class="card-title">${user.name}</h5>
                                <p class="card-text">
                                    <small class="text-muted">${user.email}</small><br>
                                    <span class="badge bg-secondary">${user.department}</span>
                                </p>
                            </div>
                        </div>
                    </div>
                `;
            });
            resultsDiv.innerHTML = html;
        }

        // Show user details in modal
        function showUserDetails(userId) {
            const userDetailsDiv = document.getElementById('userDetails');
            userDetailsDiv.innerHTML = '<div class="text-center"><div class="spinner-border" role="status"></div></div>';

            const xhr = new XMLHttpRequest();
            xhr.open('POST', '/t/example/user_search/details', true);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    const response = JSON.parse(xhr.responseText);
                    if (response.error) {
                        userDetailsDiv.innerHTML = '<div class="alert alert-danger">' + response.error + '</div>';
                    } else {
                        const user = response.user;
                        userDetailsDiv.innerHTML = `
                            <table class="table">
                                <tr><td><strong>Name:</strong></td><td>${user.name}</td></tr>
                                <tr><td><strong>Email:</strong></td><td>${user.email}</td></tr>
                                <tr><td><strong>Department:</strong></td><td>${user.department}</td></tr>
                                <tr><td><strong>Phone:</strong></td><td>${user.phone || 'N/A'}</td></tr>
                                <tr><td><strong>Created:</strong></td><td>${user.created_at}</td></tr>
                            </table>
                        `;
                    }
                }
            };

            xhr.send('id=' + userId);

            // Show modal
            new bootstrap.Modal(document.getElementById('userModal')).show();
        }

        // Search on Enter key
        document.getElementById('searchInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                searchUsers();
            }
        });
    </script>
</body>
</html>
CREATE TABLE user (
    id int NOT NULL AUTO_INCREMENT,
    name varchar(100) NOT NULL,
    email varchar(150) NOT NULL,
    department varchar(50),
    phone varchar(20),
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY email (email)
);

INSERT INTO user (name, email, department, phone) VALUES
('Alice Johnson', '[email protected]', 'Engineering', '+1-555-0101'),
('Bob Smith', '[email protected]', 'Marketing', '+1-555-0102'),
('Carol Davis', '[email protected]', 'Engineering', '+1-555-0103'),
('David Wilson', '[email protected]', 'Sales', '+1-555-0104'),
('Eva Brown', '[email protected]', 'HR', '+1-555-0105'),
('Frank Miller', '[email protected]', 'Engineering', '+1-555-0106'),
('Grace Lee', '[email protected]', 'Finance', '+1-555-0107'),
('Henry Taylor', '[email protected]', 'Marketing', '+1-555-0108'),
('Iris Chen', '[email protected]', 'Engineering', '+1-555-0109'),
('Jack Anderson', '[email protected]', 'Sales', '+1-555-0110');

How It Works

  • Search as you type: Real-time user search with vanilla JavaScript
  • Modal details: Click any user card to view detailed information
  • Error handling: Proper error messages and loading states
  • Responsive design: Bootstrap-powered responsive layout

AJAX Form Submission with jQuery

contact_form.py
from utils import render, Log
from models import Contact
import json

class Contact_form(object):
    def view(self, ctx):
        # Load the contact form page
        ctx.go_to = render.as_view(ctx, "contact_form")

    def submit(self, ctx):
        # Handle form submission via AJAX
        request = ctx.getRequest()

        # Extract form data
        name = request.getParameter("name") or ""
        email = request.getParameter("email") or ""
        subject = request.getParameter("subject") or ""
        message = request.getParameter("message") or ""

        # Validation
        errors = []
        if len(name.strip()) < 2:
            errors.append("Name must be at least 2 characters")

        if "@" not in email or len(email) < 5:
            errors.append("Please enter a valid email address")

        if len(subject.strip()) < 5:
            errors.append("Subject must be at least 5 characters")

        if len(message.strip()) < 10:
            errors.append("Message must be at least 10 characters")

        if errors:
            result = {
                "success": False,
                "errors": errors
            }
        else:
            try:
                # Save to database
                contact = Contact()
                contact.set("name", name.strip())
                contact.set("email", email.strip())
                contact.set("subject", subject.strip())
                contact.set("message", message.strip())
                contact.set("status", "new")
                contact.save()

                Log.info(ctx, "New contact form submission from: %s" % email)

                result = {
                    "success": True,
                    "message": "Thank you for your message! We'll get back to you soon.",
                    "contact_id": contact.get("id")
                }
            except Exception as e:
                Log.error(ctx, "Error saving contact: %s" % str(e))
                result = {
                    "success": False,
                    "errors": ["Sorry, there was an error processing your request"]
                }

        ctx.go_to = render.as_json(ctx, result)

    def validate_email(self, ctx):
        # Real-time email validation endpoint
        request = ctx.getRequest()
        email = request.getParameter("email") or ""

        if not email:
            result = {"valid": False, "message": "Email is required"}
        elif "@" not in email or len(email) < 5:
            result = {"valid": False, "message": "Please enter a valid email format"}
        else:
            # Check if email already exists
            existing = Contact.count("email = ?", email)
            if existing > 0:
                result = {"valid": False, "message": "This email has already submitted a contact form"}
            else:
                result = {"valid": True, "message": "Email looks good!"}

        ctx.go_to = render.as_json(ctx, result)
contact_form.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contact Form - jQuery AJAX</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .form-feedback { font-size: 0.875em; margin-top: 0.25rem; }
        .is-validating { position: relative; }
        .validation-spinner {
            position: absolute;
            right: 10px;
            top: 50%;
            transform: translateY(-50%);
            width: 16px;
            height: 16px;
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <h2>Contact Us</h2>
                <p class="text-muted">Send us a message and we'll get back to you as soon as possible.</p>

                <!-- Alert Messages -->
                <div id="alertContainer"></div>

                <!-- Contact Form -->
                <form id="contactForm" novalidate>
                    <div class="mb-3">
                        <label for="name" class="form-label">Full Name <span class="text-danger">*</span></label>
                        <input type="text" class="form-control" id="name" name="name" required>
                        <div class="form-feedback"></div>
                    </div>

                    <div class="mb-3">
                        <label for="email" class="form-label">Email Address <span class="text-danger">*</span></label>
                        <div class="position-relative">
                            <input type="email" class="form-control" id="email" name="email" required>
                            <div class="validation-spinner d-none">
                                <div class="spinner-border spinner-border-sm" role="status"></div>
                            </div>
                        </div>
                        <div class="form-feedback"></div>
                    </div>

                    <div class="mb-3">
                        <label for="subject" class="form-label">Subject <span class="text-danger">*</span></label>
                        <input type="text" class="form-control" id="subject" name="subject" required>
                        <div class="form-feedback"></div>
                    </div>

                    <div class="mb-3">
                        <label for="message" class="form-label">Message <span class="text-danger">*</span></label>
                        <textarea class="form-control" id="message" name="message" rows="5" required></textarea>
                        <div class="form-feedback"></div>
                        <div class="form-text">Minimum 10 characters</div>
                    </div>

                    <button type="submit" class="btn btn-primary" id="submitBtn">
                        <span class="spinner-border spinner-border-sm d-none me-2" role="status"></span>
                        Send Message
                    </button>
                </form>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
    <script>
        $(document).ready(function() {
            // Real-time email validation
            let emailTimeout;
            $('#email').on('input', function() {
                const email = $(this).val();
                const $input = $(this);
                const $feedback = $input.next().next('.form-feedback');
                const $spinner = $input.next('.validation-spinner');

                // Clear previous timeout
                clearTimeout(emailTimeout);

                // Reset states
                $input.removeClass('is-valid is-invalid');
                $feedback.removeClass('text-success text-danger').empty();
                $spinner.addClass('d-none');

                if (email.length < 3) return;

                // Show spinner and validate after delay
                emailTimeout = setTimeout(function() {
                    $spinner.removeClass('d-none');

                    $.ajax({
                        url: '/t/example/contact_form/validate_email',
                        method: 'POST',
                        data: { email: email },
                        success: function(response) {
                            $spinner.addClass('d-none');
                            if (response.valid) {
                                $input.addClass('is-valid');
                                $feedback.addClass('text-success').text(response.message);
                            } else {
                                $input.addClass('is-invalid');
                                $feedback.addClass('text-danger').text(response.message);
                            }
                        },
                        error: function() {
                            $spinner.addClass('d-none');
                            $input.addClass('is-invalid');
                            $feedback.addClass('text-danger').text('Error validating email');
                        }
                    });
                }, 500);
            });

            // Form submission
            $('#contactForm').on('submit', function(e) {
                e.preventDefault();

                const $form = $(this);
                const $submitBtn = $('#submitBtn');
                const $spinner = $submitBtn.find('.spinner-border');
                const $alertContainer = $('#alertContainer');

                // Show loading state
                $submitBtn.prop('disabled', true);
                $spinner.removeClass('d-none');
                $alertContainer.empty();

                // Clear previous validation states
                $form.find('.form-control').removeClass('is-valid is-invalid');
                $form.find('.form-feedback').empty();

                // Submit form
                $.ajax({
                    url: '/t/example/contact_form/submit',
                    method: 'POST',
                    data: $form.serialize(),
                    success: function(response) {
                        if (response.success) {
                            // Success message
                            $alertContainer.html(
                                '<div class="alert alert-success alert-dismissible fade show" role="alert">' +
                                response.message +
                                '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' +
                                '</div>'
                            );

                            // Reset form
                            $form[0].reset();

                            // Scroll to top
                            $('html, body').animate({ scrollTop: 0 }, 300);
                        } else {
                            // Show field-specific errors
                            $alertContainer.html(
                                '<div class="alert alert-danger alert-dismissible fade show" role="alert">' +
                                '<strong>Please fix the following errors:</strong><ul class="mb-0 mt-2">' +
                                response.errors.map(error => '<li>' + error + '</li>').join('') +
                                '</ul>' +
                                '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' +
                                '</div>'
                            );
                        }
                    },
                    error: function() {
                        $alertContainer.html(
                            '<div class="alert alert-danger alert-dismissible fade show" role="alert">' +
                            'Sorry, there was an error processing your request. Please try again.' +
                            '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' +
                            '</div>'
                        );
                    },
                    complete: function() {
                        // Reset loading state
                        $submitBtn.prop('disabled', false);
                        $spinner.addClass('d-none');
                    }
                });
            });
        });
    </script>
</body>
</html>
CREATE TABLE contact (
    id int NOT NULL AUTO_INCREMENT,
    name varchar(100) NOT NULL,
    email varchar(150) NOT NULL,
    subject varchar(200) NOT NULL,
    message text NOT NULL,
    status enum('new','read','replied') DEFAULT 'new',
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_email (email),
    KEY idx_status (status)
);

Key Features

  • Real-time validation: Email validation as you type
  • Form serialization: jQuery handles form data automatically
  • Loading states: Visual feedback during submission
  • Error handling: Field-specific error messages

Modern AJAX with Fetch API

task_manager.py
from utils import render
from models import Task
import json
from java.util import Date

class Task_manager(object):
    def view(self, ctx):
        # Load task manager interface
        ctx.go_to = render.as_view(ctx, "task_manager")

    def list(self, ctx):
        # Get all tasks
        request = ctx.getRequest()
        status = request.getParameter("status") or "all"

        if status == "all":
            tasks = Task.findAll().orderBy("created_at DESC")
        else:
            tasks = Task.where("status = ?", status).orderBy("created_at DESC")

        task_list = []
        for task in tasks:
            task_list.append({
                "id": task.get("id"),
                "title": task.get("title"),
                "description": task.get("description"),
                "status": task.get("status"),
                "priority": task.get("priority"),
                "created_at": str(task.get("created_at")),
                "due_date": str(task.get("due_date")) if task.get("due_date") else None
            })

        result = {"tasks": task_list, "total": len(task_list)}
        ctx.go_to = render.as_json(ctx, result)

    def create(self, ctx):
        # Create new task
        request = ctx.getRequest()
        title = request.getParameter("title") or ""
        description = request.getParameter("description") or ""
        priority = request.getParameter("priority") or "medium"
        due_date = request.getParameter("due_date")

        if len(title.strip()) < 3:
            result = {"success": False, "error": "Title must be at least 3 characters"}
        else:
            try:
                task = Task()
                task.set("title", title.strip())
                task.set("description", description.strip())
                task.set("status", "pending")
                task.set("priority", priority)
                if due_date:
                    # Simple date format handling
                    task.set("due_date", due_date)
                task.save()

                result = {
                    "success": True,
                    "task": {
                        "id": task.get("id"),
                        "title": task.get("title"),
                        "description": task.get("description"),
                        "status": task.get("status"),
                        "priority": task.get("priority"),
                        "created_at": str(task.get("created_at"))
                    }
                }
            except Exception as e:
                result = {"success": False, "error": "Error creating task: %s" % str(e)}

        ctx.go_to = render.as_json(ctx, result)

    def update_status(self, ctx):
        # Update task status
        request = ctx.getRequest()
        task_id = request.getParameter("id")
        new_status = request.getParameter("status")

        if not task_id or not new_status:
            result = {"success": False, "error": "Task ID and status are required"}
        else:
            try:
                task = Task.findById(task_id)
                if task:
                    task.set("status", new_status)
                    task.save()
                    result = {"success": True, "message": "Task status updated"}
                else:
                    result = {"success": False, "error": "Task not found"}
            except Exception as e:
                result = {"success": False, "error": "Error updating task: %s" % str(e)}

        ctx.go_to = render.as_json(ctx, result)

    def delete(self, ctx):
        # Delete task
        request = ctx.getRequest()
        task_id = request.getParameter("id")

        if not task_id:
            result = {"success": False, "error": "Task ID is required"}
        else:
            try:
                task = Task.findById(task_id)
                if task:
                    task.delete()
                    result = {"success": True, "message": "Task deleted"}
                else:
                    result = {"success": False, "error": "Task not found"}
            except Exception as e:
                result = {"success": False, "error": "Error deleting task: %s" % str(e)}

        ctx.go_to = render.as_json(ctx, result)
task_manager.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager - Fetch API</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <style>
        .task-card { transition: transform 0.2s; }
        .task-card:hover { transform: translateY(-2px); }
        .priority-high { border-left: 4px solid #dc3545; }
        .priority-medium { border-left: 4px solid #ffc107; }
        .priority-low { border-left: 4px solid #28a745; }
        .status-completed { opacity: 0.7; text-decoration: line-through; }
    </style>
</head>
<body>
    <div class="container mt-4">
        <div class="d-flex justify-content-between align-items-center mb-4">
            <h2><i class="fas fa-tasks me-2"></i>Task Manager</h2>
            <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#taskModal">
                <i class="fas fa-plus me-2"></i>New Task
            </button>
        </div>

        <!-- Filter Tabs -->
        <ul class="nav nav-tabs mb-4" id="statusTabs">
            <li class="nav-item">
                <a class="nav-link active" data-status="all" href="#">All Tasks</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" data-status="pending" href="#">Pending</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" data-status="in_progress" href="#">In Progress</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" data-status="completed" href="#">Completed</a>
            </li>
        </ul>

        <!-- Alert Container -->
        <div id="alertContainer"></div>

        <!-- Tasks Container -->
        <div id="tasksContainer" class="row">
            <!-- Tasks will be loaded here -->
        </div>

        <!-- Loading Indicator -->
        <div id="loadingIndicator" class="text-center py-4 d-none">
            <div class="spinner-border" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
        </div>
    </div>

    <!-- Task Modal -->
    <div class="modal fade" id="taskModal" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Create New Task</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <form id="taskForm">
                        <div class="mb-3">
                            <label for="title" class="form-label">Title *</label>
                            <input type="text" class="form-control" id="title" name="title" required>
                        </div>
                        <div class="mb-3">
                            <label for="description" class="form-label">Description</label>
                            <textarea class="form-control" id="description" name="description" rows="3"></textarea>
                        </div>
                        <div class="mb-3">
                            <label for="priority" class="form-label">Priority</label>
                            <select class="form-select" id="priority" name="priority">
                                <option value="low">Low</option>
                                <option value="medium" selected>Medium</option>
                                <option value="high">High</option>
                            </select>
                        </div>
                        <div class="mb-3">
                            <label for="due_date" class="form-label">Due Date</label>
                            <input type="date" class="form-control" id="due_date" name="due_date">
                        </div>
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-primary" id="saveTaskBtn">
                        <span class="spinner-border spinner-border-sm d-none me-2"></span>
                        Create Task
                    </button>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
    <script>
        class TaskManager {
            constructor() {
                this.currentStatus = 'all';
                this.init();
            }

            init() {
                this.loadTasks();
                this.attachEventListeners();
            }

            attachEventListeners() {
                // Status filter tabs
                document.querySelectorAll('#statusTabs .nav-link').forEach(tab => {
                    tab.addEventListener('click', (e) => {
                        e.preventDefault();
                        this.filterTasks(e.target.dataset.status);
                    });
                });

                // Create task button
                document.getElementById('saveTaskBtn').addEventListener('click', () => {
                    this.createTask();
                });
            }

            async loadTasks(status = 'all') {
                this.showLoading(true);

                try {
                    const formData = new FormData();
                    formData.append('status', status);

                    const response = await fetch('/t/example/task_manager/list', {
                        method: 'POST',
                        body: formData
                    });

                    if (!response.ok) throw new Error('Network response was not ok');

                    const data = await response.json();
                    this.renderTasks(data.tasks);
                } catch (error) {
                    this.showAlert('Error loading tasks: ' + error.message, 'danger');
                } finally {
                    this.showLoading(false);
                }
            }

            async createTask() {
                const form = document.getElementById('taskForm');
                const formData = new FormData(form);
                const saveBtn = document.getElementById('saveTaskBtn');
                const spinner = saveBtn.querySelector('.spinner-border');

                saveBtn.disabled = true;
                spinner.classList.remove('d-none');

                try {
                    const response = await fetch('/t/example/task_manager/create', {
                        method: 'POST',
                        body: formData
                    });

                    const data = await response.json();

                    if (data.success) {
                        this.showAlert('Task created successfully!', 'success');
                        form.reset();
                        bootstrap.Modal.getInstance(document.getElementById('taskModal')).hide();
                        this.loadTasks(this.currentStatus);
                    } else {
                        this.showAlert(data.error, 'danger');
                    }
                } catch (error) {
                    this.showAlert('Error creating task: ' + error.message, 'danger');
                } finally {
                    saveBtn.disabled = false;
                    spinner.classList.add('d-none');
                }
            }

            async updateTaskStatus(taskId, newStatus) {
                try {
                    const formData = new FormData();
                    formData.append('id', taskId);
                    formData.append('status', newStatus);

                    const response = await fetch('/t/example/task_manager/update_status', {
                        method: 'POST',
                        body: formData
                    });

                    const data = await response.json();

                    if (data.success) {
                        this.showAlert('Task status updated!', 'success');
                        this.loadTasks(this.currentStatus);
                    } else {
                        this.showAlert(data.error, 'danger');
                    }
                } catch (error) {
                    this.showAlert('Error updating task: ' + error.message, 'danger');
                }
            }

            async deleteTask(taskId) {
                if (!confirm('Are you sure you want to delete this task?')) return;

                try {
                    const formData = new FormData();
                    formData.append('id', taskId);

                    const response = await fetch('/t/example/task_manager/delete', {
                        method: 'POST',
                        body: formData
                    });

                    const data = await response.json();

                    if (data.success) {
                        this.showAlert('Task deleted!', 'success');
                        this.loadTasks(this.currentStatus);
                    } else {
                        this.showAlert(data.error, 'danger');
                    }
                } catch (error) {
                    this.showAlert('Error deleting task: ' + error.message, 'danger');
                }
            }

            filterTasks(status) {
                this.currentStatus = status;

                // Update active tab
                document.querySelectorAll('#statusTabs .nav-link').forEach(tab => {
                    tab.classList.remove('active');
                });
                document.querySelector(`[data-status="${status}"]`).classList.add('active');

                this.loadTasks(status);
            }

            renderTasks(tasks) {
                const container = document.getElementById('tasksContainer');

                if (tasks.length === 0) {
                    container.innerHTML = '<div class="col-12 text-center py-4"><p class="text-muted">No tasks found</p></div>';
                    return;
                }

                const html = tasks.map(task => `
                    <div class="col-md-6 col-lg-4 mb-3">
                        <div class="card task-card priority-${task.priority} ${task.status === 'completed' ? 'status-completed' : ''}">
                            <div class="card-body">
                                <div class="d-flex justify-content-between align-items-start mb-2">
                                    <h6 class="card-title mb-0">${task.title}</h6>
                                    <div class="dropdown">
                                        <button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
                                            <i class="fas fa-ellipsis-v"></i>
                                        </button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" href="#" onclick="taskManager.updateTaskStatus(${task.id}, 'pending')">Mark Pending</a></li>
                                            <li><a class="dropdown-item" href="#" onclick="taskManager.updateTaskStatus(${task.id}, 'in_progress')">Mark In Progress</a></li>
                                            <li><a class="dropdown-item" href="#" onclick="taskManager.updateTaskStatus(${task.id}, 'completed')">Mark Completed</a></li>
                                            <li><hr class="dropdown-divider"></li>
                                            <li><a class="dropdown-item text-danger" href="#" onclick="taskManager.deleteTask(${task.id})">Delete</a></li>
                                        </ul>
                                    </div>
                                </div>

                                ${task.description ? `<p class="card-text small text-muted">${task.description}</p>` : ''}

                                <div class="d-flex justify-content-between align-items-end">
                                    <div>
                                        <span class="badge bg-${this.getStatusColor(task.status)}">${task.status.replace('_', ' ')}</span>
                                        <span class="badge bg-${this.getPriorityColor(task.priority)} ms-1">${task.priority}</span>
                                    </div>
                                    ${task.due_date ? `<small class="text-muted">${new Date(task.due_date).toLocaleDateString()}</small>` : ''}
                                </div>
                            </div>
                        </div>
                    </div>
                `).join('');

                container.innerHTML = html;
            }

            showLoading(show) {
                const indicator = document.getElementById('loadingIndicator');
                const container = document.getElementById('tasksContainer');

                if (show) {
                    indicator.classList.remove('d-none');
                    container.innerHTML = '';
                } else {
                    indicator.classList.add('d-none');
                }
            }

            showAlert(message, type = 'info') {
                const container = document.getElementById('alertContainer');
                const alert = `
                    <div class="alert alert-${type} alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                    </div>
                `;
                container.innerHTML = alert;

                // Auto dismiss after 5 seconds
                setTimeout(() => {
                    const alertElement = container.querySelector('.alert');
                    if (alertElement) {
                        bootstrap.Alert.getInstance(alertElement).close();
                    }
                }, 5000);
            }

            getStatusColor(status) {
                const colors = {
                    'pending': 'warning',
                    'in_progress': 'info',
                    'completed': 'success'
                };
                return colors[status] || 'secondary';
            }

            getPriorityColor(priority) {
                const colors = {
                    'low': 'success',
                    'medium': 'warning',
                    'high': 'danger'
                };
                return colors[priority] || 'secondary';
            }
        }

        // Initialize task manager when DOM is loaded
        document.addEventListener('DOMContentLoaded', () => {
            window.taskManager = new TaskManager();
        });
    </script>
</body>
</html>
CREATE TABLE task (
    id int NOT NULL AUTO_INCREMENT,
    title varchar(200) NOT NULL,
    description text,
    status enum('pending','in_progress','completed') DEFAULT 'pending',
    priority enum('low','medium','high') DEFAULT 'medium',
    due_date date,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_status (status),
    KEY idx_priority (priority)
);

INSERT INTO task (title, description, priority, due_date) VALUES
('Complete project proposal', 'Write and submit the Q1 project proposal document', 'high', '2024-02-15'),
('Code review session', 'Review pull requests from team members', 'medium', '2024-02-10'),
('Update documentation', 'Update API documentation with new endpoints', 'low', '2024-02-20'),
('Team meeting preparation', 'Prepare slides for weekly team meeting', 'medium', '2024-02-08'),
('Bug fixes', 'Fix reported bugs in the user dashboard', 'high', '2024-02-12');

Modern Features

  • ES6 Classes: Object-oriented JavaScript architecture
  • Fetch API: Modern promise-based HTTP requests
  • Async/Await: Clean asynchronous code handling
  • CRUD Operations: Complete task management functionality

Do you know?

  • Fetch API is modern and built into all browsers, no library needed
  • FormData automatically handles form serialization and file uploads
  • Async/await makes code more readable than traditional callbacks
  • Error boundaries ensure graceful degradation when requests fail