Skip to main content

Ajax Event Handling Process

Created: July 12, 2015 Larry Qu 8 min read

Ajax Event Handling Process

Ajax (Asynchronous JavaScript and XML) allows web pages to update content dynamically without reloading. Below is a step-by-step process for handling Ajax events.

1. XMLHttpRequest (Classic Approach)

1. Write the Request Object Creation Function

Create a function that returns an XMLHttpRequest object when called. This object should be a global variable since the callback function needs to access it. Consider browser compatibility (e.g., use ActiveXObject for older IE).

2. Establish the Event Handling Function

  • Call the request object creation function to create the request object.
  • Set the request object properties (e.g., method, URL).
  • Set the callback function.
  • Initiate the request.

3. Establish the Callback Function

  • Write the callback logic, such as updating CSS properties or setting innerHTML based on the response.

4. Set Up Event Listening

  • After the page loads, attach an event listener to a DOM element, setting it to the event handling function from step 2.

Example Code

Here’s a basic example of Ajax event handling:

// Step 1: Create XMLHttpRequest object (with browser compatibility)
function createXHR() {
    if (window.XMLHttpRequest) {
        return new XMLHttpRequest();
    } else {
        return new ActiveXObject("Microsoft.XMLHTTP");
    }
}

// Global request object
var xhr = createXHR();

// Step 2: Event handling function
function handleAjaxRequest() {
    xhr.open("GET", "https://api.example.com/data", true);
    xhr.onreadystatechange = callbackFunction;
    xhr.send();
}

// Step 3: Callback function
function callbackFunction() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        document.getElementById("result").innerHTML = xhr.responseText;
    }
}

// Step 4: Set up event listener
window.onload = function() {
    document.getElementById("myButton").addEventListener("click", handleAjaxRequest);
};

Modern Fetch API

The Fetch API provides a more powerful and flexible alternative to XMLHttpRequest. It uses Promises, making it easier to write asynchronous code.

Basic Fetch Example

fetch("https://api.example.com/data")
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        document.getElementById("result").innerHTML = JSON.stringify(data);
    })
    .catch(error => {
        console.error("Fetch error:", error);
    });

Fetch with POST and Headers

fetch("https://api.example.com/users", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer your-token-here"
    },
    body: JSON.stringify({
        name: "John Doe",
        email: "[email protected]"
    })
})
.then(response => response.json())
.then(data => console.log("User created:", data))
.catch(error => console.error("Error:", error));

Async/Await Patterns

Async/await makes asynchronous code read like synchronous code, greatly improving readability.

Basic Async/Await

async function fetchUserData(userId) {
    try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
            throw new Error(`Failed to fetch user: ${response.status}`);
        }
        const user = await response.json();
        return user;
    } catch (error) {
        console.error("Error fetching user:", error);
        throw error;
    }
}

// Usage
async function displayUser() {
    try {
        const user = await fetchUserData(42);
        document.getElementById("userName").textContent = user.name;
    } catch (error) {
        document.getElementById("errorMsg").textContent = "Failed to load user";
    }
}

Parallel Requests with Promise.all

async function loadDashboard() {
    try {
        const [users, posts, stats] = await Promise.all([
            fetch("/api/users").then(r => r.json()),
            fetch("/api/posts").then(r => r.json()),
            fetch("/api/stats").then(r => r.json())
        ]);
        return { users, posts, stats };
    } catch (error) {
        console.error("Dashboard load failed:", error);
        throw error;
    }
}

Sequential Requests (Dependent Data)

async function loadUserWithPosts(userId) {
    const user = await fetch(`/api/users/${userId}`).then(r => r.json());
    const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
    const comments = await Promise.all(
        posts.map(post => fetch(`/api/posts/${post.id}/comments`).then(r => r.json()))
    );
    return { user, posts, comments };
}

Error Handling Patterns

Proper error handling distinguishes robust applications from fragile ones.

Centralized Error Handler

class ApiError extends Error {
    constructor(message, status, data) {
        super(message);
        this.name = "ApiError";
        this.status = status;
        this.data = data;
    }
}

async function apiRequest(url, options = {}) {
    try {
        const response = await fetch(url, {
            headers: {
                "Content-Type": "application/json",
                ...options.headers
            },
            ...options
        });

        if (!response.ok) {
            const errorData = await response.json().catch(() => null);
            throw new ApiError(
                errorData?.message || `Request failed with status ${response.status}`,
                response.status,
                errorData
            );
        }

        return await response.json();
    } catch (error) {
        if (error instanceof ApiError) throw error;
        throw new ApiError("Network error: unable to reach server", 0, null);
    }
}

// Usage
async function saveUser(userData) {
    try {
        const result = await apiRequest("/api/users", {
            method: "POST",
            body: JSON.stringify(userData)
        });
        showSuccess("User saved successfully");
        return result;
    } catch (error) {
        if (error.status === 409) {
            showWarning("User already exists");
        } else if (error.status >= 500) {
            showError("Server error, please try again later");
        } else {
            showError(error.message);
        }
    }
}

Retry Pattern

async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
    for (let attempt = 1; attempt <= retries; attempt++) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) throw new Error(`Status ${response.status}`);
            return await response.json();
        } catch (error) {
            if (attempt === retries) throw error;
            console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
            await new Promise(resolve => setTimeout(resolve, delay * attempt));
        }
    }
}

CORS Handling

Cross-Origin Resource Sharing (CORS) is a security mechanism that restricts requests from one origin to another.

Understanding CORS

// This request will fail if the server doesn't allow your origin
fetch("https://api.another-domain.com/data")
    .then(response => response.json())
    .catch(error => console.error("CORS error:", error));

Server-Side CORS Configuration (Express.js Example)

const express = require("express");
const app = express();

// Allow specific origins
app.use((req, res, next) => {
    const allowedOrigins = ["https://myapp.com", "https://admin.myapp.com"];
    const origin = req.headers.origin;

    if (allowedOrigins.includes(origin)) {
        res.setHeader("Access-Control-Allow-Origin", origin);
    }

    res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
    res.setHeader("Access-Control-Max-Age", "86400");

    if (req.method === "OPTIONS") {
        return res.sendStatus(204);
    }

    next();
});

CORS with Credentials

// Client-side: include cookies/auth headers
fetch("https://api.example.com/data", {
    credentials: "include",  // Send cookies
    mode: "cors"
});

// Server-side: must set specific origin (not *)
res.setHeader("Access-Control-Allow-Origin", "https://myapp.com");
res.setHeader("Access-Control-Allow-Credentials", "true");

Proxy Solution for Development

// vite.config.js
export default {
    server: {
        proxy: {
            "/api": {
                target: "https://api.example.com",
                changeOrigin: true,
                rewrite: path => path.replace(/^\/api/, "")
            }
        }
    }
};

Request Cancellation with AbortController

AbortController allows cancelling in-flight requests, essential for cleanup in Single Page Applications.

Basic Cancellation

const controller = new AbortController();
const signal = controller.signal;

// Start the request
fetch("https://api.example.com/large-data", { signal })
    .then(response => response.json())
    .then(data => console.log("Data:", data))
    .catch(error => {
        if (error.name === "AbortError") {
            console.log("Request was cancelled");
        } else {
            console.error("Request failed:", error);
        }
    });

// Cancel the request
setTimeout(() => {
    controller.abort();
    console.log("Request cancelled due to timeout");
}, 5000);

React/Svelte Component Cleanup

function SearchComponent() {
    let currentController = null;

    async function search(query) {
        // Cancel previous request
        if (currentController) {
            currentController.abort();
        }

        currentController = new AbortController();

        try {
            const results = await fetch(`/api/search?q=${query}`, {
                signal: currentController.signal
            }).then(r => r.json());

            displayResults(results);
        } catch (error) {
            if (error.name !== "AbortError") {
                showError("Search failed");
            }
        }
    }

    // Cleanup on component destroy
    return () => {
        if (currentController) {
            currentController.abort();
        }
    };
}

Timeout Wrapper

async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });
        return await response.json();
    } finally {
        clearTimeout(timeoutId);
    }
}

// Usage
fetchWithTimeout("https://api.example.com/data", {}, 5000)
    .then(data => console.log(data))
    .catch(error => {
        if (error.name === "AbortError") {
            console.error("Request timed out");
        }
    });

File Uploads

Single File Upload with FormData

async function uploadFile(fileInput) {
    const formData = new FormData();
    formData.append("file", fileInput.files[0]);

    try {
        const response = await fetch("/api/upload", {
            method: "POST",
            body: formData  // Don't set Content-Type — browser sets it with boundary
        });

        if (!response.ok) throw new Error("Upload failed");
        return await response.json();
    } catch (error) {
        console.error("Upload error:", error);
        throw error;
    }
}

Multiple File Upload with Progress (XMLHttpRequest)

function uploadMultipleFiles(files) {
    const formData = new FormData();
    files.forEach((file, index) => {
        formData.append(`file_${index}`, file);
    });

    const xhr = new XMLHttpRequest();

    xhr.upload.addEventListener("progress", (event) => {
        if (event.lengthComputable) {
            const percent = (event.loaded / event.total) * 100;
            updateProgressBar(percent);
        }
    });

    return new Promise((resolve, reject) => {
        xhr.addEventListener("load", () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error(`Upload failed: ${xhr.status}`));
            }
        });

        xhr.addEventListener("error", () => reject(new Error("Network error")));
        xhr.open("POST", "/api/upload");
        xhr.send(formData);
    });
}

File Upload with Drag and Drop

const dropZone = document.getElementById("dropZone");

dropZone.addEventListener("dragover", (event) => {
    event.preventDefault();
    dropZone.classList.add("drag-over");
});

dropZone.addEventListener("dragleave", () => {
    dropZone.classList.remove("drag-over");
});

dropZone.addEventListener("drop", async (event) => {
    event.preventDefault();
    dropZone.classList.remove("drag-over");

    const files = Array.from(event.dataTransfer.files);
    const results = await Promise.all(
        files.map(file => uploadSingleFile(file))
    );
    console.log("Uploaded files:", results);
});

JSONP (JSON with Padding)

JSONP is a technique for making cross-origin requests in older browsers that don’t support CORS. It works by injecting a <script> tag.

function jsonpRequest(url, callbackName) {
    return new Promise((resolve, reject) => {
        // Create a unique callback name
        const uniqueCallback = `jsonp_${Date.now()}_${Math.random().toString(36).slice(2)}`;

        // Define the callback function globally
        window[uniqueCallback] = (data) => {
            cleanup();
            resolve(data);
        };

        // Create script element
        const script = document.createElement("script");
        const separator = url.includes("?") ? "&" : "?";
        script.src = `${url}${separator}callback=${uniqueCallback}`;

        // Handle errors
        script.onerror = () => {
            cleanup();
            reject(new Error("JSONP request failed"));
        };

        // Timeout fallback
        const timeoutId = setTimeout(() => {
            cleanup();
            reject(new Error("JSONP request timed out"));
        }, 10000);

        function cleanup() {
            delete window[uniqueCallback];
            document.body.removeChild(script);
            clearTimeout(timeoutId);
        }

        document.body.appendChild(script);
    });
}

// Usage
jsonpRequest("https://api.example.com/data", "handleResponse")
    .then(data => console.log("JSONP data:", data))
    .catch(error => console.error("JSONP error:", error));

Comparison: XMLHttpRequest vs Fetch vs Axios

Feature XMLHttpRequest Fetch API Axios
Promise-based No (callbacks) Yes Yes
Request cancellation xhr.abort() AbortController CancelToken
Upload progress Native support Not built-in Built-in
Download progress Native support Not built-in Built-in
JSON auto-parse Manual Manual Automatic
CORS support Yes Yes Yes
Browser support All Modern browsers All (with polyfill)
Bundle size Built-in Built-in ~14KB gzipped
Interceptors Manual Manual Built-in
Timeout xhr.timeout Manual Built-in

Important Notes

  • Modern Alternatives: For new projects, consider using fetch() API or libraries like Axios for simpler and more powerful Ajax handling.
  • Error Handling: Always check xhr.status and handle errors in the callback.
  • Security: Be cautious with CORS (Cross-Origin Resource Sharing) and validate inputs.
  • Async vs Sync: Use asynchronous requests (true in open()) to avoid blocking the UI.
  • JSON Handling: If the response is JSON, parse it with JSON.parse(xhr.responseText).

Resources

Comments

Share this article

Scan to read on mobile