⚡ AJAX & Dynamic Content
Create interactive, dynamic experiences without page reloads
Master AJAX in WordPress for modern user interfaces
Learning Objectives
- Understand AJAX in WordPress context
- Implement wp_ajax actions properly
- Create load more functionality
- Build infinite scroll systems
- Implement real-time search
- Handle AJAX form submissions
- Use WordPress REST API for AJAX
- Implement proper security with nonces
Understanding AJAX in WordPress
AJAX (Asynchronous JavaScript and XML) allows you to update parts of a web page without reloading the entire page. WordPress provides built-in support for AJAX through admin-ajax.php and the REST API.
Two Approaches
AJAX Request Flow
1. User Action
→
Click, scroll, input
2. JavaScript
→
Send AJAX request
3. WordPress
→
Process request
4. Response
→
Return data
5. Update DOM
Display results
Basic AJAX Setup
Step 1: Enqueue Scripts and Localize
<?php
/**
* Enqueue AJAX scripts
*/
function mytheme_enqueue_ajax_scripts() {
wp_enqueue_script(
'mytheme-ajax',
get_template_directory_uri() . '/js/ajax.js',
array( 'jquery' ),
'1.0.0',
true
);
// Localize script with AJAX URL and nonce
wp_localize_script( 'mytheme-ajax', 'mytheme_ajax', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mytheme_ajax_nonce' ),
'loading' => __( 'Loading...', 'mytheme' ),
'error' => __( 'An error occurred. Please try again.', 'mytheme' ),
) );
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_ajax_scripts' );
Step 2: Create AJAX Handler in PHP
<?php
/**
* AJAX Handler for Loading Posts
*/
function mytheme_load_more_posts() {
// Verify nonce
if ( ! wp_verify_nonce( $_POST['nonce'], 'mytheme_ajax_nonce' ) ) {
wp_die( 'Security check failed' );
}
// Get parameters
$page = isset( $_POST['page'] ) ? intval( $_POST['page'] ) : 1;
$posts_per_page = isset( $_POST['posts_per_page'] ) ? intval( $_POST['posts_per_page'] ) : 6;
// Query posts
$args = array(
'post_type' => 'post',
'posts_per_page' => $posts_per_page,
'paged' => $page,
'post_status' => 'publish',
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
ob_start();
while ( $query->have_posts() ) {
$query->the_post();
// Use your template part
get_template_part( 'template-parts/content', 'ajax' );
}
$content = ob_get_clean();
wp_send_json_success( array(
'content' => $content,
'found_posts' => $query->found_posts,
'max_pages' => $query->max_num_pages,
) );
} else {
wp_send_json_error( 'No posts found' );
}
wp_die();
}
// Hook for logged-in users
add_action( 'wp_ajax_load_more_posts', 'mytheme_load_more_posts' );
// Hook for non-logged-in users
add_action( 'wp_ajax_nopriv_load_more_posts', 'mytheme_load_more_posts' );
Step 3: JavaScript AJAX Request
// ajax.js
(function($) {
'use strict';
$(document).ready(function() {
let currentPage = 1;
let loading = false;
// Load More Button Click
$('#load-more-btn').on('click', function(e) {
e.preventDefault();
if (loading) return;
loading = true;
const $button = $(this);
const originalText = $button.text();
// Update button state
$button.text(mytheme_ajax.loading).prop('disabled', true);
// AJAX request
$.ajax({
url: mytheme_ajax.ajax_url,
type: 'POST',
data: {
action: 'load_more_posts',
nonce: mytheme_ajax.nonce,
page: currentPage + 1,
posts_per_page: 6
},
success: function(response) {
if (response.success) {
// Append new posts
$('#posts-container').append(response.data.content);
// Update page number
currentPage++;
// Check if more posts exist
if (currentPage >= response.data.max_pages) {
$button.text('No More Posts').prop('disabled', true);
} else {
$button.text(originalText).prop('disabled', false);
}
} else {
console.error('Error:', response.data);
alert(mytheme_ajax.error);
}
},
error: function(xhr, status, error) {
console.error('AJAX Error:', error);
alert(mytheme_ajax.error);
},
complete: function() {
loading = false;
if (currentPage < maxPages) {
$button.prop('disabled', false);
}
}
});
});
});
})(jQuery);
Common AJAX Implementations
🔄 Load More Posts
Load additional posts on button click
♾️ Infinite Scroll
Auto-load content on scroll
🔍 Live Search
Real-time search results
🗳️ Post Filters
Filter posts by category/tag
❤️ Like System
Save likes without reload
💬 Comments
Submit comments via AJAX
Infinite Scroll Implementation
// Infinite Scroll
let isLoading = false;
let currentPage = 1;
let maxPages = parseInt($('#posts-container').data('max-pages'));
$(window).on('scroll', function() {
if (isLoading) return;
const scrollHeight = $(document).height();
const scrollPosition = $(window).height() + $(window).scrollTop();
const scrollPercentage = (scrollPosition / scrollHeight) * 100;
// Load more when user scrolls to 80% of page
if (scrollPercentage > 80 && currentPage < maxPages) {
isLoading = true;
// Show loading spinner
$('#loading-spinner').show();
$.ajax({
url: mytheme_ajax.ajax_url,
type: 'POST',
data: {
action: 'load_more_posts',
nonce: mytheme_ajax.nonce,
page: currentPage + 1
},
success: function(response) {
if (response.success) {
// Append posts with fade-in effect
const $newPosts = $(response.data.content).hide();
$('#posts-container').append($newPosts);
$newPosts.fadeIn();
currentPage++;
// Update max pages if needed
if (currentPage >= response.data.max_pages) {
$(window).off('scroll');
$('#end-message').show();
}
}
},
complete: function() {
isLoading = false;
$('#loading-spinner').hide();
}
});
}
});
Live Search Implementation
// Live Search with Debounce
let searchTimer;
const searchDelay = 500; // milliseconds
$('#search-input').on('keyup', function() {
const searchTerm = $(this).val();
// Clear previous timer
clearTimeout(searchTimer);
// Minimum 3 characters
if (searchTerm.length < 3) {
$('#search-results').empty();
return;
}
// Show loading
$('#search-results').html('<div class="loading">Searching...</div>');
// Debounce search
searchTimer = setTimeout(function() {
$.ajax({
url: mytheme_ajax.ajax_url,
type: 'POST',
data: {
action: 'live_search',
nonce: mytheme_ajax.nonce,
search: searchTerm
},
success: function(response) {
if (response.success && response.data.results.length > 0) {
let html = '<ul class="search-results-list">';
response.data.results.forEach(function(post) {
html += `<li>
<a href="${post.url}">
<h4>${post.title}</h4>
<p>${post.excerpt}</p>
</a>
</li>`;
});
html += '</ul>';
$('#search-results').html(html);
} else {
$('#search-results').html('<p>No results found</p>');
}
}
});
}, searchDelay);
});
// PHP Handler
function mytheme_live_search() {
check_ajax_referer( 'mytheme_ajax_nonce', 'nonce' );
$search_term = sanitize_text_field( $_POST['search'] );
$args = array(
's' => $search_term,
'post_type' => 'post',
'posts_per_page' => 5,
'post_status' => 'publish',
);
$query = new WP_Query( $args );
$results = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$results[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'excerpt' => wp_trim_words( get_the_excerpt(), 15 ),
'url' => get_permalink(),
);
}
}
wp_send_json_success( array( 'results' => $results ) );
}
Filter Posts by Category
// Filter Posts
$('.filter-button').on('click', function() {
const $button = $(this);
const category = $button.data('category');
// Update active state
$('.filter-button').removeClass('active');
$button.addClass('active');
// Show loading
$('#posts-container').html('<div class="loading">Loading...</div>');
$.ajax({
url: mytheme_ajax.ajax_url,
type: 'POST',
data: {
action: 'filter_posts',
nonce: mytheme_ajax.nonce,
category: category
},
success: function(response) {
if (response.success) {
$('#posts-container').html(response.data.content);
// Animate posts appearance
$('.post-card').each(function(index) {
$(this).css('opacity', 0).delay(index * 100).animate({
opacity: 1
}, 500);
});
}
}
});
});
// PHP Handler
function mytheme_filter_posts() {
check_ajax_referer( 'mytheme_ajax_nonce', 'nonce' );
$category = sanitize_text_field( $_POST['category'] );
$args = array(
'post_type' => 'post',
'posts_per_page' => 12,
);
if ( $category !== 'all' ) {
$args['category_name'] = $category;
}
$query = new WP_Query( $args );
ob_start();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
get_template_part( 'template-parts/content', 'card' );
}
} else {
echo '<p>No posts found.</p>';
}
$content = ob_get_clean();
wp_send_json_success( array( 'content' => $content ) );
}
Using REST API for AJAX
REST API Endpoint
<?php
/**
* Register REST API Routes
*/
function mytheme_register_rest_routes() {
register_rest_route( 'mytheme/v1', '/posts', array(
'methods' => 'GET',
'callback' => 'mytheme_get_posts',
'permission_callback' => '__return_true',
'args' => array(
'page' => array(
'default' => 1,
'sanitize_callback' => 'absint',
),
'per_page' => array(
'default' => 10,
'sanitize_callback' => 'absint',
),
),
) );
register_rest_route( 'mytheme/v1', '/like', array(
'methods' => 'POST',
'callback' => 'mytheme_handle_like',
'permission_callback' => function() {
return is_user_logged_in();
},
) );
}
add_action( 'rest_api_init', 'mytheme_register_rest_routes' );
/**
* REST API Callback
*/
function mytheme_get_posts( $request ) {
$page = $request->get_param( 'page' );
$per_page = $request->get_param( 'per_page' );
$args = array(
'post_type' => 'post',
'posts_per_page' => $per_page,
'paged' => $page,
);
$query = new WP_Query( $args );
$posts = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$posts[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'excerpt' => get_the_excerpt(),
'permalink' => get_permalink(),
'thumbnail' => get_the_post_thumbnail_url( get_the_ID(), 'medium' ),
);
}
}
return new WP_REST_Response( array(
'posts' => $posts,
'total' => $query->found_posts,
'pages' => $query->max_num_pages,
), 200 );
}
JavaScript Fetch API
// Using Fetch API with REST
async function loadPosts(page = 1) {
try {
const response = await fetch(`${wpApiSettings.root}mytheme/v1/posts?page=${page}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpApiSettings.nonce
}
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
// Process posts
if (data.posts.length > 0) {
displayPosts(data.posts);
}
return data;
} catch (error) {
console.error('Error:', error);
}
}
// Display posts
function displayPosts(posts) {
const container = document.getElementById('posts-container');
posts.forEach(post => {
const postElement = document.createElement('article');
postElement.className = 'post-card';
postElement.innerHTML = `
<h3><a href="${post.permalink}">${post.title}</a></h3>
${post.thumbnail ? `<img src="${post.thumbnail}" alt="${post.title}">` : ''}
<p>${post.excerpt}</p>
`;
container.appendChild(postElement);
});
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadPosts(1);
});
AJAX Form Submission
Contact Form with AJAX
// Form submission
$('#contact-form').on('submit', function(e) {
e.preventDefault();
const $form = $(this);
const $submitBtn = $form.find('button[type="submit"]');
const $message = $('#form-message');
// Disable submit button
$submitBtn.prop('disabled', true).text('Sending...');
// Get form data
const formData = new FormData(this);
formData.append('action', 'submit_contact_form');
formData.append('nonce', mytheme_ajax.nonce);
$.ajax({
url: mytheme_ajax.ajax_url,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$message.html('<div class="success">' + response.data.message + '</div>');
$form[0].reset();
} else {
$message.html('<div class="error">' + response.data + '</div>');
}
},
error: function() {
$message.html('<div class="error">An error occurred. Please try again.</div>');
},
complete: function() {
$submitBtn.prop('disabled', false).text('Send Message');
}
});
});
AJAX Methods Comparison
| Method | Pros | Cons | Best For |
|---|---|---|---|
| admin-ajax.php | Traditional, well-documented, works everywhere | Loads full WordPress, slower | Forms, authenticated requests |
| REST API | Modern, faster, cleaner URLs | Requires more setup | Public data, modern apps |
| Custom Endpoints | Full control, optimized | More complex | Specific use cases |
AJAX Security Best Practices
Security Measures
- Always use nonces: Verify with wp_verify_nonce()
- Check capabilities: Ensure user has permission
- Sanitize input: Never trust user data
- Validate data: Check data types and ranges
- Escape output: Prevent XSS attacks
- Rate limiting: Prevent abuse
- Use HTTPS: Encrypt data in transit
Secure AJAX Handler
<?php
function mytheme_secure_ajax_handler() {
// Check nonce
if ( ! wp_verify_nonce( $_POST['nonce'], 'mytheme_ajax_nonce' ) ) {
wp_send_json_error( 'Invalid nonce' );
}
// Check user capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Insufficient permissions' );
}
// Sanitize input
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
$content = isset( $_POST['content'] ) ? wp_kses_post( $_POST['content'] ) : '';
// Validate
if ( ! $post_id || ! get_post( $post_id ) ) {
wp_send_json_error( 'Invalid post ID' );
}
// Process request
// ... your code here ...
wp_send_json_success( array(
'message' => 'Success!'
) );
}
Never skip nonce verification in AJAX handlers. This is your primary defense against CSRF attacks.
Practice Exercise
Build Complete AJAX System