An in-depth guide on using Ajax with WordPress

Introduction to Admin Ajax

Admin Ajax is the oldest technique for sending Ajax requests using WordPress. All you need to do is register an action, point to https://yourdomain.com/wp-admin/admin-ajax.php, and decide how you want to return the data. You can return JSON, HTML, or even XML (if you like pain).

Let’s get started with the benefits and drawbacks of using Admin Ajax.

Benefits of Admin Ajax

Admin Ajax is very simple to use. As stated before, all you have to do is register an action, point your script to the correct endpoint, and you’re done.

Another benefit is that it is (from my experience) more reliable than using a REST API. See, a lot of installs actually disable the REST API because of security precautions. As a result, Admin Ajax is a safe bet to use. I once converted a plugin with 200,000 installs to use the REST API instead of Admin Ajax. As a result, support requests flowed in, mostly from people who have disabled the REST API. We ended up going back to using Admin Ajax.

Having said that, the REST API is more powerful and controlled, which is why people like using it. I’ll be going over REST in the next chapter as an alternative to Admin Ajax.

The last benefit is that you can control who has access to your Ajax action. You can choose no privileges (a logged-out user) or only logged in users. You can then do a nonce and capabilities check to make sure the user is accessing your endpoint legitimately.

Drawbacks of Admin Ajax

The main drawback of using Admin Ajax is that the results are generally unpredictable. You can use Admin Ajax to populate an iframe, for example. You can use it to return HTML, JSON, XML, or anything you can imagine.

Another drawback is that some users block access to anything after /wp-admin/. As a result, since admin-ajax.php is considered admin territory, your Ajax call will not work.

Setting Up Your Actions

There are two Ajax actions for using Admin Ajax. The two are:

  1. wp_ajax_your_action – wp_ajax will only be available for logged-in users. It’s a prefix to your action. In this example, the action name is your_action.
  2. wp_ajax_no_priv_your_action – wp_ajax_no_priv will be available for logged-out users. Again, it’s a prefix to your action, with your action being your_action.

Here’s an example assuming a callback function of ajax_your_action.

add_action( 'wp_ajax_your_action', 'ajax_your_action' );
add_action( 'wp_ajax_no_priv_your_action', 'ajax_your_action' );
/**
 * Ajax callback.
 */
function ajax_your_action() {
	wp_send_json_success(
		array(
			'key' => 'value',
		)
	);
}

Let’s build a plugin together

One of the key tenants of Ajax is to make things easier on the user. In this case, let’s create a plugin that ranks posts. Call it a “thumbs up” button, or whatever. This vote up and vote down button will be tied to a post id after a calculation is performed on which posts are actually ranking more.

Let’s do some brainstorming here. What features of this plugin are we expecting?

Let’s start with the rank logic. It’s a simple thumbs up and thumbs down scenario. We’d definitely need a way to store that data, and saving it as post meta would just be an expensive and not very elegant way to retrieve that data. So we’d need a custom table built since post meta and user meta are just not very useful in retrieving this type of data.

In this database, we’ll need a post type, post id, vote position (a 1 or a 0), ranking, and a primary key of just id.

Let’s concentrate on the plugin’s origin. The first and hard step is naming your plugin. How about… “Voting Tally.” Silly name isn’t it? But let’s go with it.

First, we’ll have to set up the architecture of this plugin. It’ll, of course, have a main plugin file, a folder for JavaScript, a folder for CSS, and an includes file for anything we might need PHP wise.

Voting Tally Folder Structure
Voting Tally Folder Structure

Let’s start with votingtally.php and build up the plugin’s header.

<?php
/**
 * Voting Tally Plugin
 *
 * @package   votingtally
 * @copyright Copyright(c) 2020, MediaRon LLC
 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
 *
 * Plugin Name: Voting Tally
 * Plugin URI: https://github.com/wpajax/votingtally
 * Description: A way to up-and-down-vote posts including post types.
 * Version: 1.0.0
 * Author: MediaRon LLC
 * Author URI: https://wpandajax.com
 * License: GPL2
 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: votingtally
 * Domain Path: languages
 */

Now that the header is built, let’s define some variables we’ll need later and get the autoloader.

define( 'VOTINGTALLY_VERSION', '1.0.0' );
define( 'VOTINGTALLY_TABLE_VERSION', '1.0.0' );
define( 'VOTINGTALLY_PLUGIN_NAME', 'Voting Tally' );
define( 'VOTINGTALLY_DIR', plugin_dir_path( __FILE__ ) );
define( 'VOTINGTALLY_URL', plugins_url( '/', __FILE__ ) );
define( 'VOTINGTALLY_SLUG', plugin_basename( __FILE__ ) );
define( 'VOTINGTALLY_FILE', __FILE__ );

// Setup the plugin auto loader.
require_once 'autoloader.php';

Finally, let’s load up the class for Voting Tally and instantiate it.

/**
 * The Voting Tally base class.
 */
class Voting_Tally {

	/**
	 * Voting_Tally instance.
	 *
	 * @var Voting_Tally $instance
	 */
	private static $instance = null;

	/**
	 * Return a class instance.
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Class Constructor
	 */
	private function __construct() {
		add_action( 'plugins_loaded', array( $this, 'plugin_loaded' ), 20 );
		add_action( 'init', array( $this, 'add_i18n' ) );
	}

	/**
	 * Fired when the init action for WordPress is triggered.
	 */
	public function init() {
		load_plugin_textdomain( 'votingtally', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
	}

	/**
	 * Fired when the plugins for WordPress have finished loading.
	 */
	public function plugins_loaded() {

	}
}
Voting_Tally::get_instance();

We have an empty plugins_loaded method. First, let’s create the table creation class. Let’s begin with the header:

<?php
/**
 * Creates and drops the table.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Create_Table
 */
class Create_Table {

	/**
	 * Holds the tablename for Voting Tally
	 *
	 * @var string $tablename
	 */
	private static $tablename = 'votingtally';

	/**
	 * Class Constructor.
	 */
	public function __construct() {
		$this->create_table();
	}

So we have a tablename named votingtally and our constructor creates the table upon instantiation. Here’s the rest of the create table class.

        /**
	 * Create Voting Tally table
	 *
	 * @since 1.0.0
	 * @access private
	 * @return void
	 */
	private function create_table() {
		global $wpdb;
		$tablename = $wpdb->base_prefix . self::$tablename;

		$version = get_site_option( 'votingtallytable_version', '0' );
		if ( version_compare( $version, VOTINGTALLY_TABLE_VERSION ) < 0 ) {
			$charset_collate = '';
			if ( ! empty( $wpdb->charset ) ) {
				$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
			}
			if ( ! empty( $wpdb->collate ) ) {
				$charset_collate .= " COLLATE $wpdb->collate";
			}
			$sql = "CREATE TABLE {$tablename} (
				id INT(20) NOT NULL AUTO_INCREMENT,
				site_id INT (20) NOT NULL,
				blog_id INT (20) NOT NULL,
				content_id INT(20) NOT NULL,
				post_type VARCHAR(20) NOT NULL,
				up_votes INT (20) NOT NULL,
				down_votes INT (20) NOT NULL,
				rating FLOAT (20) NOT NULL,
				PRIMARY KEY  (id)
				) {$charset_collate};";
			require_once ABSPATH . 'wp-admin/includes/upgrade.php';
			dbDelta( $sql );

			update_site_option( 'votingtallytable_version', VOTINGTALLY_TABLE_VERSION );
		}
	}
}

We’re creating the following fields:

  1. id – The unique key for each entry.
  2. site_id – Multisite site ID.
  3. blog_id – Multisite blog ID.
  4. content_id – The content ID (or Post ID).
  5. post_type – The post type of the post where voting takes place.
  6. up_votes – Number of upvotes.
  7. down_votes – Number of downvotes.
  8. rating – Rating based on average of other posts factored in with up and down votes.

We also store an option named votingtallytable_version with the table version. If we were to change our table, we’d simply increase the constant VOTINGTALLY_TABLE_VERSION by a point or two.

Now that the table is out of the way, let’s come up with our interface. We need a simple vote up and vote down button to show at the end of a post for logged-in users only. We could do a shortcode. But that would mean someone would have to manually place the voting shortcode per-post. Let’s use a filter instead to automatically place the voting button at the end of a post without the user having to do anything except activate the plugin.

After exploring my numerous design skills, I came up with this for the interface. It’s a simple vote up/vote down interface.

Thumbs Up and Down Interface
Thumbs Up and Down Interface

Let’s go ahead and work on the output interface. We’ll be creating a file called class-output.php in our includes folder. Here’s the class header:

<?php
/**
 * Outputs the Voting Tally Interface.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Output
 */
class Output {
	/**
	 * Class Constructor.
	 */
	public function __construct() {
		add_action( 'the_content', array( $this, 'maybe_output_interface' ) );
	}

We’re using the filter the_content in order to add our voting interface to the front-end of a post, page, and even custom post type.

Here’s the rest of the class:

        /**
	 * Output the Voting Tallery button interface.
	 *
	 * @param string $content The Post content.
	 *
	 * @return string Modified content.
	 */
	public function maybe_output_interface( $content ) {
		if ( ! is_singular() || ! is_user_logged_in() ) {
			return $content;
		}
		ob_start();
		?>	
		<div class="voting-tally">
			<h5><?php esc_html_e( 'Rank This Post', 'votingtally' ); ?></h5>
			<button class="vote-upwards tally-button" aria-label="<?php esc_attr_e( 'Vote this item up', 'votingtallery' ); ?>" data-nonce="<?php echo esc_html( wp_create_nonce( 'votingtallery-record-vote' ) ); ?>" data-id="<?php echo absint( get_the_ID() ); ?>" data-action="1">
				<img src="<?php echo esc_url( VOTINGTALLY_URL . 'images/thumbs-up.png' ); ?>" alt="Thumbs Up Button" />
			</button>
			<button class="vote-downwards  tally-button" aria-label="<?php esc_attr_e( 'Vote this item down', 'votingtallery' ); ?>" data-nonce="<?php echo esc_html( wp_create_nonce( 'votingtallery-record-vote' ) ); ?>" data-id="<?php echo absint( get_the_ID() ); ?>" data-action="0">
				<img src="<?php echo esc_url( VOTINGTALLY_URL . 'images/thumbs-down.png' ); ?>" alt="Thumbs Down Button" />
			</button>
		</div>
		<?php
		return $content . ob_get_clean();
	}
}

We make sure we’re on a post, page, or post type item by using the is_singular check. We then make sure that the user is logged in before outputting the interface. The HTML should be semantic, which is why I used buttons instead of a div or an anchor. I’ve loaded some data attributes with items we’ll need for our JavaScript Ajax handler.

Once we’re done with the class, it’s safe to include it in our main plugins_loaded method.

        /**
	 * Fired when the plugins for WordPress have finished loading.
	 */
	public function plugins_loaded() {
		// Create the table.
		new VotingTally\Includes\Create_Table();

		// Output the Voting Talley interface.
		new VotingTally\Includes\Output();

I’m using the Twenty Twenty theme to check the output, and I admit it’s pretty ugly.

Voting Interface for Twenty Twenty Theme
Voting Interface for Twenty Twenty Theme

Let’s create a stylesheet and override the button styles. We’ll just create a votingtally.css file in our css folder.

.voting-tally {
	background: #FFF;
	border: 1px solid #DDD;
	text-align: center;
	display: block;
	width: auto !important;
	margin: 0 auto;
	padding: 20px;
}
.voting-tally img {
	display: inline-block;
}
.voting-tally h5 {
	padding: 0 !important;
	margin: 0 !important;
}
.voting-tally button img {
	max-width: 50px;
	margin: 0;
	padding: 0;
}
body .voting-tally button {
	display: inline-block;
	border: none;
	padding: 1rem 2rem;
	margin: 0;
	text-decoration: none;
	background: #FFF;
	color: #ffffff;
	font-family: sans-serif;
	font-size: 1rem;
	cursor: pointer;
	text-align: center;
	transition: background 250ms ease-in-out, 
	transform 150ms ease;
	-webkit-appearance: none;
	-moz-appearance: none;
}

body .voting-tally button:hover,
body .voting-tally button:focus {
	background: rgb(250, 250, 250);
	border: 1px solid #EEE;
}

body .voting-tally button:focus {
	outline: 1px solid #fff;
	outline-offset: -4px;
}

body .voting-tally button:active {
	transform: scale(0.99);
}

I hate having to declare “!important” in CSS rules, but Twenty Twenty made it very difficult to override certain styles.

Here’s what the output looks like styled:

Styled Voting Tally Interface
Styled Voting Tally Interface

But wait! We skipped a step. How did we get our stylesheet into the plugin? Let’s create a new class called class-enqueue.php in our includes folder. In this class, we’ll enqueue our CSS and JavaScript.

<?php
/**
 * Outputs the scripts necessary to initialize the interface.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Enqueue
 */
class Enqueue {
	/**
	 * Class Constructor.
	 */
	public function __construct() {
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
	}

	/**
	 * Output the Voting Tallery scripts/styles.
	 */
	public function enqueue_scripts() {
		if ( ! is_singular() || ! is_user_logged_in() ) {
			return;
		}
		wp_enqueue_script(
			'votingtally',
			VOTINGTALLY_URL . 'js/votingtally.js',
			array( 'jquery' ),
			VOTINGTALLY_VERSION,
			true
		);
		wp_localize_script(
			'votingtally',
			'votingtally',
			array(
				'ajaxurl'       => admin_url( 'admin-ajax.php' ),
				'loading'       => VOTINGTALLY_URL . 'images/loading.svg',
				'vote_recorded' => __( 'Thanks! Your vote has been recorded.', 'votingtally' ),
				'vote_error'    => __( 'There was a problem recording your vote.', 'votingtally' ),
			)
		);
		wp_enqueue_style(
			'votingtally',
			VOTINGTALLY_URL . 'css/votingtally.css',
			array(),
			VOTINGTALLY_VERSION,
			'all'
		);
	}
}

In the above example, we’re enqueueing a script that doesn’t exist yet. We do know from the code that this file will reside in the js folder and be named votingtally.js. We’re also localizing a few variables:

  • The Ajax URL (path to admin-ajax.php).
  • A loading SVG
  • A translatable string for when a vote has been recorded.
  • A translatable string for when an error occurs.

And finally, we instantiate the class in our plugins_loaded function.

// Enqueue the necessary scripts/styles.
new VotingTally\Includes\Enqueue();

Let’s move onto the JavaScript file.

jQuery(function($) {
	$( '.tally-button' ).on( 'click', function( e ) {
		e.preventDefault();
		var html = '<img src="' + votingtally.loading + '" alt="Loading Animation" />';
		$( '.voting-tally' ).html( html );
		$.post(
			votingtally.ajaxurl,
			{
				action: 'votingtally_record_vote',
				nonce: $( this ).data( 'nonce' ),
				post_id: $( this ).data( 'id' ),
				vote: $( this ).data('action')
			},
			function() {

			} )
			.done(function() {
				$( '.voting-tally' ).html( '<h5>' + votingtally.vote_recorded + '</h5>' );
			})
			.fail(function() {
				$( '.voting-tally' ).html( '<h5>' + votingtally.vote_error + '</h5>' );
			})
			.always(function() {
				
			})
	} );
});

The code above does the following:

  1. On a button click, show a loading animation.
  2. Perform a POST Ajax request to admin-ajax.php.
  3. Pass an action, and get the data attributes in our button output.
  4. Output a message on success or failure.

Since our Ajax action doesn’t exist yet, we need to initialize it using PHP. Let’s create a new class called class-ajax.php. In this class, we’ll initialize the wp_ajax_ action and calculate and store a rating. Let’s step through the code bit-by-bit.

<?php
/**
 * Captures the Ajax Calls.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Enqueue
 */
class Ajax {
	/**
	 * Class Constructor.
	 */
	public function __construct() {
		add_action( 'wp_ajax_votingtally_record_vote', array( $this, 'ajax_record_vote' ) );
	}

The constructor initializes the wp_ajax_ action. Note that we’re using the same action name suffix as in our JavaScript file. These need to match. We have a callback method of ajax_record_vote, so let’s dive into that method.

        /**
	 * Capture the Recorded Vote.
	 */
	public function ajax_record_vote() {
		global $current_user;
		if ( ! is_user_logged_in() ) {
			wp_send_json_error( array() );
		}
		// Verify Nonce.
		if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'votingtallery-record-vote' ) ) {
			wp_send_json_error( array() );
		}

		// Retrieve the vote.
		$vote = absint( filter_input( INPUT_POST, 'vote' ) );

		// Retrieve the post ID.
		$post_id   = absint( filter_input( INPUT_POST, 'post_id' ) );
		$post_type = get_post_type( $post_id );

		// Get the current site information.
		global $current_blog, $wpdb;
		$site_id = 1;
		$blog_id = 1;
		if ( is_multisite() ) {
			$site_id = absint( $current_blog->site_id );
			$blog_id = absint( $current_blog->blog_id );
		}

		// Get the post rating.
		$post_rating = $this->get_post_stats( $post_id );
		$tablename   = Create_Table::get_tablename();
		if ( ! $post_rating ) {
			// Insert new row because it doesn't exist.
			$wpdb->insert(
				$tablename,
				array(
					'site_id'    => absint( $site_id ),
					'blog_id'    => absint( $blog_id ),
					'content_id' => absint( $post_id ),
					'post_type'  => sanitize_text_field( $post_type ),
				),
				array( '%d', '%d', '%d', '%s' )
			);
			$post_rating              = new \stdClass();
			$post_rating->up_votes    = 0;
			$post_rating->down_votes  = 0;
			$post_rating->total_votes = 0;
		}
		$post_rating->total_votes = $post_rating->up_votes + $post_rating->down_votes;
		// Make sure results aren't negative.
		$post_rating->up_votes    = $post_rating->up_votes < 0 ? 0 : $post_rating->up_votes;
		$post_rating->down_votes  = $post_rating->down_votes < 0 ? 0 : $post_rating->down_votes;
		$post_rating->total_votes = $post_rating->total_votes < 0 ? 0 : $post_rating->total_votes;

		// Let's get the total of up and down votes.
		if ( 1 === $vote ) {
			$post_rating->up_votes += 1;
			$vote                   = true;
		} else {
			$post_rating->down_votes += 1;
			$vote                     = false;
		}

		// Update the post.
		$wpdb->update(
			$tablename,
			array(
				'up_votes'   => $post_rating->up_votes,
				'down_votes' => $post_rating->down_votes,
			),
			array(
				'site_id'    => $site_id,
				'blog_id'    => $blog_id,
				'content_id' => $post_id,
			),
			array( '%d', '%d', '%d' )
		);

		// Spiffy, now let's calculate the total ratings for everything and update.
		$total_items = $wpdb->get_var( $wpdb->prepare( "select count( id ) from {$tablename} where site_id = %d and blog_id = %d and post_type = '%s'", $site_id, $blog_id, $post_type ) );

		$results = $wpdb->get_row( $wpdb->prepare( "select SUM(up_votes + down_votes) as total_votes, SUM( ( up_votes * 5 + down_votes * 1 ) / {$total_items} ) as ratings_sum  from {$tablename} where site_id = %d and blog_id = %d and post_type = %s and content_id = %d", $site_id, $blog_id, $post_type, $post_id ) );

		$args = array(
			'total_items'    => $total_items,
			'total_votes'    => $results->total_votes,
			'average_rating' => $results->ratings_sum / $total_items,
			'average_votes'  => $results->total_votes / $total_items,
		);

		$sql = $wpdb->prepare( "UPDATE {$tablename} set rating = ( ( {$args['average_votes']} * {$args['average_rating']} ) + ( ( up_votes + down_votes ) * ( up_votes * 5 + down_votes * 1 ) / ( up_votes + down_votes ) ) ) / {$args['average_votes']} + up_votes + down_votes where site_id = %d and blog_id = %d and post_type = %s and content_id = %d", $site_id, $blog_id, $post_type, $post_id );
		$wpdb->query( $sql );
		wp_send_json_success( array() );
	}

The above code does quite a lot. First, there’s a nonce check and a check to make sure the user is logged in. We then retrieve the vote, create a table entry if one does not exist, and then perform an overall rating for the table entry when compared to other posts of the same type. It’s ugly, but the algorithm works and will provide an easy way to list the top-rated posts. Inside the method, we call get_post_stats. Let’s dive into that method.

        /**
	 * Get rating stats for a post ID.
	 *
	 * @param int $post_id The Post ID to retrieve stats for.
	 *
	 * @return mixed false on failure, object on success.
	 */
	private function get_post_stats( $post_id = 0 ) {
		global $current_user, $current_blog, $wpdb;
		$site_id = 1;
		$blog_id = 1;
		if ( is_multisite() ) {
			$site_id = $current_blog->site_id;
			$blog_id = $current_blog->blog_id;
		}
		$tablename = Create_Table::get_tablename();
		$sql       = "select * from {$tablename} where content_id = %d and site_id = %d and blog_id = %d";
		$results   = $wpdb->get_row( $wpdb->prepare( $sql, $post_id, $site_id, $blog_id ) ); // phpcs:ignore
		if ( $results ) {
			return $results;
		}
		return false;
	}
}

We perform a simple query to get the entry for a post ID. If results are found, they are returned. If not, a boolean false is returned, which triggers a table insertion if not found.

Finally, we instantiate the class in our plugins_loaded method.

        /**
	 * Fired when the plugins for WordPress have finished loading.
	 */
	public function plugins_loaded() {
		// Create the table.
		new VotingTally\Includes\Create_Table();

		// Output the Voting Talley interface.
		new VotingTally\Includes\Output();

		// Enqueue the necessary scripts/styles.
		new VotingTally\Includes\Enqueue();

		// Register Ajax Call.
		new VotingTally\Includes\Ajax();
	}

The result? We now have a Thumbs Up/Thumbs Down interface that stores a ranking in a custom database table. When a user clicks on the Thumbs Up or Thumbs Down button, a loading animation shows, and when the Ajax request completes, a message is shown to the user.

Voting Tally Example
Voting Tally Example

There’s a lot more we can do with this and we’ll be building on this plugin in the next few chapters. An example of what we can do is to create a method for displaying the top-rated posts. Perhaps show a post ranking. And make sure a user can only vote once per-post.

You can take a look at the existing code at: https://wpajax.pro/tally.

Conclusion

In this chapter we learned the advantages and disadvantages of using Admin Ajax. We then built our first Ajax plugin, which is essentially a post ranking tool.

In the next chapter, I’ll cover porting this plugin over to the REST API and including some helpful endpoints.

3 thoughts on “Introduction to Admin Ajax”

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top