Skip to main content

Course Progress

Loading...

⚡ 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
admin-ajax.php: Traditional WordPress AJAX handler
REST API: Modern, RESTful approach with better performance

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();
            }
        });
    }
});

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

Create a comprehensive AJAX implementation that includes:

  1. Implement load more posts button
  2. Add infinite scroll functionality
  3. Create live search with debouncing
  4. Build category filter system
  5. Add like/favorite functionality
  6. Implement AJAX comment submission
  7. Create REST API endpoint
  8. Add loading animations
  9. Implement error handling
  10. Add proper security measures

Additional Resources