WordPress and AJAX: An Introduction

WordPress theme developers looking to take their projects to the next level often turn to AJAX for improved UX. Of course, there are a lot of ways to go about implementing AJAX in WordPress, and some ways are better than others.

Let’s look at some common mistakes and then dive into some better approaches.

Techniques to avoid

I’ve seen two common design patterns from self-taught developers or those less familiar with WordPress that should be avoided:

  1. directly accessing PHP files, and
  2. generating JavaScript with PHP.

Directly accessing PHP files tends to happen in theming. In short, the developer creates a PHP file in the theme that’s designed to handle the AJAX calls, and then sends requests directly to that file.

To get the file to work like WordPress, typically something like this goes right up top:

require_once( "../../../../wp-load.php" );

And then in the JavaScript (we’ll use jQuery here for simplicity’s sake), the calls look something like this:

jQuery.post( '/wp-content/themes/mytheme/myAjax.php', function() { ... });

There are a couple of problems with this approach: it’s brittle (making assumptions about file locations that may not be true), and it could pose a security risk (as it exposes direct URLs of PHP files that may not be sufficiently protected).

The other technique that I sometimes see is using PHP to output a JavaScript file containing some information that needs to come from WordPress. These files go by names like “myScript.js.php” and contain code something vaguely like this:


myThing = {
  post_id : ,
  post_title : ;
}

These sort of situations where you need a JavaScript file but you’re relying on PHP to generate it are confusing and weird and should be avoided, too.

So what are some good ways to implement AJAX in WordPress without resorting to these sorts of approaches? Let’s look at the options.

Better approach: admin-ajax.php

Using admin-ajax.php is the most common and preferred way of doing most AJAX calls in WordPress.

Its origin, as the name implies, rests in the use of AJAX by WordPress core in the administrative area. It’s great for the small user interactions where caching should be avoided (such as upvoting a post). Leveraging it for end-user-facing AJAX can be thought of as a four-step process:

  1. Writing your JavaScript file (the JS that handles the user events and AJAX calls)
  2. Enqueuing your JavaScript file
  3. Passing the URL of admin-ajax.php to the front-end so your JS knows where to send requests
  4. Writing the PHP that uses WordPress’s action hook(s) to process the requests

Let’s go over each in detail:

Writing your JS file

First things first, you’ll need to have some JavaScript that performs your AJAX requests. If you were creating a primitive upvoting plugin, your plugin’s javascript file — let’s call it upvote.js — might look something like this:

jQuery(document).ready( function($) {
  $(".upvote").click( function() {
    var post_id = jQuery(this).attr("data-post_id");
    $.post({
      url : upvoteObj.ajaxurl,
      data : { action: "my_upvote", post_id : post_id },
      success: function( response ) {
        alert( "Vote " + response.result + "! Post now has " + response.votes + " votes." );
      }
    });
  });
});

If you’re familiar with using jQuery to make AJAX requests, that should look pretty straightforward. Except: What’s that “upvoteObj.ajaxurl” bit, you ask? Or the ‘my_upvote’ value for ‘action’?

Good questions! The first object/property is a reference to WordPress’s admin-ajax.php. The second tells WordPress which hook to execute.

To see how the upvoteObj.ajaxurl gets there, let’s jump over to the PHP side.

Enqueueing your JS file

Our minimalist plugin’s index.php file needs to tell WordPress to load our upvote.js file. We do this using the wp_enqueue_scripts() hook:

/*
  Plugin Name: Upvote Sample
  Plugin URI: https://cornershopcreative.com
  Description: An AJAX demo
  Version: 0.1
  Author: Ben Byrne, Cornershop Creative
  Author URI: https://cornershopcreative.com
*/

function upvote_scripts() {
  wp_enqueue_script(
    'upvote-script',
    plugin_dir_url(__FILE__) . "upvote.js",
    array('jquery'),
    '1.0',
    true
  );
}
add_action( 'wp_enqueue_scripts', 'upvote_scripts' );

So now our bit of jQuery that triggers the AJAX post is getting loaded in the theme. Pretty straightforward… but we still need to resolve that whole “myUpvote.ajaxurl” thing. Next up:

Passing the URL of admin-ajax.php

Instead of using the myUpvote.ajaxurl property, we could have just hard-coded something like “/wp-admin/admin-ajax.php” — but that would be horribly dangerous, because we can’t assume WordPress isn’t installed in a subdirectory or something.

So, instead of hard-coding it, we create an object with a property that references the URL. We do this by using a WordPress function called wp_localize_script(). Ostensibly, this function exists to facilitate localizing strings, but it’s a convenient way to instantiate a javascript object of any kind.

Here, we use it to set the admin-ajax.php URL, which we can do in the upvote_scripts() function we already defined, like so:

/*
  Plugin Name: Upvote Sample
  Plugin URI: https://cornershopcreative.com
  Description: An AJAX demo
  Version: 0.1
  Author: Ben Byrne, Cornershop Creative
  Author URI: https://cornershopcreative.com
*/

function upvote_scripts() {
  wp_enqueue_script(
    'upvote-script',
    plugin_dir_url(__FILE__) . "upvote.js",
    array('jquery'),
    '1.0',
    true
  );

  wp_localize_script(
    'upvote-script', // this needs to match the name of our enqueued script
    'myUpvote',      // the name of the object
    array('ajaxurl' => admin_url('admin-ajax.php')) // the property/value
  );
}
add_action( 'wp_enqueue_scripts', 'upvote_scripts' );

Process the AJAX requests

Okay, we’ve got our JavaScript trigger hitting our AJAX endpoint. Now it’s time for the good stuff: Processing the requests!

WordPress offers two different action hooks for handling AJAX requests, which take these forms:

add_action( 'wp_ajax_MYACTION', 'callback' );
add_action( 'wp_ajax_nopriv_MYACTION', 'callback' );

The first one is the one that gets run when admin-ajax.php gets a request from an authenticated user (i.e. someone using the admin). The second gets run when the user is anonymous, meaning when someone is visiting your site but isn’t logged in.

The “MYACTION” bit in these hooks should be replaced with whatever value you’re passing as the ‘action’ property in the data being sent. In our example, that’d be “my_upvote.” So if we call our function handling the upvotings ‘upvote_handle_ajax’, we’d add the actions like so:

add_action( 'wp_ajax_my_upvote', 'upvote_handle_ajax' );
add_action( 'wp_ajax_nopriv_my_upvote', 'upvote_handle_ajax' );

Now, we just need to write our upvote_handle_ajax function. This function takes no arguments, but we can get at our data using PHP’s $_POST superglobal. It might looks something like this:

function upvote_handle_ajax() {
  // get the ID of the post we're upvoting
  $post_id = intval( $POST['post_id'] );
 
  // fetch the number of upvotes
  $num_upvotes = get_post_meta( $post_id, 'upvote_count', true );
 
  // bump it up by 1 and save it
  $num_upvotes = intval( $num_upvotes ) + 1;
  $success = update_post_meta( $post_id, 'upvote_count', $num_upvotes );
 
  // prepare our response
  if ( $success ) {
    $response = array(
      'result' => 'successful',
      'votes' => $num_upvotes
    );
  } else {
    $response = array(
      'result' => 'unsuccessful',
      'votes' => $num_upvotes - 1 // because it didn't get incremented
    );
  }
  wp_send_json( $response );
}

There. Now we’re processing the data sent by our javascript. We use wp_send_json() to handle output: it takes care of of setting the content-type header, json encoding our data, and exiting the script so nothing else happens. It’s a handy shortcut.

A Word About Best Practices

This exercise is intended to demonstrate the basic approach to using admin-ajax.php and doesn’t reflect all the best practices involved.

You should be sanitizing the incoming data, as well as using nonces (if possible — on the front end if you’ve got a page caching system going, then nonces won’t work for anonymous users). And of course, you’d want to set a cookie or something to prevent users from infinitely upvoting the same post. But hopefully this gives you a better sense of what’s involved.

(Note: This post is based on a presentation/workshop done at 2015 WordCamp Columbus)