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.statusand handle errors in the callback. - Security: Be cautious with CORS (Cross-Origin Resource Sharing) and validate inputs.
- Async vs Sync: Use asynchronous requests (
trueinopen()) to avoid blocking the UI. - JSON Handling: If the response is JSON, parse it with
JSON.parse(xhr.responseText).
Comments