Ajax pagination for posts linked via ACF Relationships

This tutorial will help you thread together a few techniques that have been discussed elsewhere but can be challenging to weave together: how to use Ajax in WordPress in general, how to set up pagination with ACF, and how to query ACF relationship fields.

For this example, we’ll have two custom post types, bike and bike feature. If you’re not familiar with creating custom post types, Custom Post Type UI is a great plugin that can help you.

Our store has multiple bikes and multiple bike features. Each bike has several bike features, and each bike feature can be linked to multiple bikes. (In database lingo, this is a many-to-many relationship.)

In ACF, create a Relationship field. Within the settings for the Relationship field, set “Filter by Post Type” to “bike.” Set the field group “Location” to “Show this field group if post type is equal to bike feature.” Now when you are editing a bike feature, you’ll be able to select bikes that have that feature using the ACF relationship field.

Okay, let’s look at some code!

Connecting PHP and JavaScript with Ajax

First, let’s connect the PHP function and the JavaScript function via Ajax:

pagination.php

[php]
<?php function ajax_pagination_enqueue() { // Register and enqueue our javascript file wp_register_script( ‘pagination’, get_template_directory_uri() . ‘/library/js/pagination.js’, // or wherever you put the file in your theme directory array(‘jquery’)); wp_enqueue_script(‘pagination’); // Connect to WordPress’ built-in Ajax file wp_localize_script( ‘pagination’, ‘ajaxpagination’, array( ‘ajaxurl’ => admin_url(‘admin-ajax.php’)
));

}

add_action( ‘wp_enqueue_scripts’, ‘ajax_pagination_enqueue’ );

// Hook this function to WordPress’ Ajax actions
add_action( ‘wp_ajax_nopriv_dl_bike_features’, ‘dl_bike_features’ );
add_action( ‘wp_ajax_dl_bike_features’, ‘dl_bike_features’ );

function dl_bike_features() {
// Do stuff
}
?>
[/php]

pagination.js

[code lang=”js”]
jQuery(document).ready(function($){

$(document).on( ‘click touchstart’, ‘#pagination a’, function( event ) {

event.preventDefault();
$.ajax({
url: ajaxpagination.ajaxurl,
type: ‘post’,
data: {
action: ‘dl_bike_features’, // the name of the function in your php file
},
success: function( result ) {
// Do stuff
},
error: function(MLHttpRequest, textStatus, errorThrown){
alert(‘Sorry, something went wrong. Please try again.’);
}
})
})

});
[/code]

In pagination.php, you hook the dl_bike_features() function to WordPress’ built-in Ajax file. Then in pagination.js you point the Ajax url to the JavaScript object you set up with wp_localize_script() (i.e. ajaxpagination.ajaxurl) and name the function that you want to execute in the data object’s action property: 'dl_bike_features'.

ACF Relationship Query

Next we add some the first bit of code to dl_bike_features().

pagination.php

[php]
<?php function dl_bike_features() { /* ** ACF Relationship query ** */ global $post; if ($post->ID) {
// On initial page load, just grab the post ID…
$bike_id = $post->ID;
} else {
// … but once you’re using Ajax, need to get the ID via Ajax
$bike_id = $_POST[‘bikeID’];
}

// Get all posts that are linked to the current post via ACF Relationship
$features = get_posts(array(
// The post type of the posts linked to current post
‘post_type’ => ‘bike-features’,
// Fetch all of them–we’ll divide them into separate pages in a moment
‘posts_per_page’ => -1,
‘meta_query’ => array(
array(
// Name of ACF Relationship field
‘key’ => ‘bikes_with_this_feature’,
// ID of current post
‘value’ => $bike_id,
‘compare’ => ‘LIKE’
)
)
));
}
?>
[/php]

The first time the page loads, the post ID will be available via the $post global, but because this function is being called by our Ajax function, this would return as “Null” unless we need to pass the post id $bike_id from the initial page load to store in a JavaScript variable, which will subsequently be passed back to the php function via the $_POST['bikeID']–more on that in a moment.

Pagination variables

Now let’s set up the variables for the pagination:

pagination.php

[php]
<?php function dl_bike_features() { // ACF relationship query omitted for brevity… /* ** Pagination variables */ if (isset($_POST[‘page’])) { // Set $page to data from Ajax, if available $page = $_POST[‘page’]; } else { // … if not, set default to 1 (for initial page load) $page = 1; } // Set up $current_page to calculate page for previous / next buttons if (!empty($page)) { $current_page = intval($page); } // $page is coming from the paginator links, so need to extract the page from the url $tens = substr($page, -3, 1); $hundreds = substr($page, -4, 1); /* ** If the character 3 or 4 spaces back from the end is not a number, ** intval will convert it to 0, which will test as false in the if statements below */ $tens = intval($tens); $hundreds = intval($hundreds); if ($hundreds) { // triple-digit number $page = intval(substr($page, -4, 3)); } elseif ($tens) { // triple-digit number $page = intval(substr($page, -3, 2)); } else { // single-digit number $page = intval(substr($page, -2, 1)); } $feature_count = 0; $features_per_page = 16; // How many features to display on each page $total = count( $features ); $pages = ceil( $total / $features_per_page ); $min = ( ( $page * $features_per_page ) – $features_per_page ) + 1; $max = ( $min + $features_per_page ) – 1; } ?>
[/php]

The $page variable’s value will come from the Ajax function via $_POST['page'] (same idea as a minute ago with $bike_id.) The tricky thing is, this value is a URL such as “http://4/”, so we need to extract the page number from there and then convert it to an interval so we can do some math with that value. We also need to check to see if the page number is a single-, double-, or triple-digit number.

Displaying linked posts and pagination

Display the posts and pagination:

pagination.php

[php]
<?php function dl_bike_features() { // ACF relationship query omitted for brevity… // Pagination variables omitted for brevity… if( $features ) { // Iterator for rows $i = 1; ?>

<div id="bike-features-container">

<div id="bike-features-inner">

<div class="bike-features-content">
<!– Note: using Bootstrap grid –>

<div class="row">
<?php foreach( $features as $feature ) {

$feature_count++;

// Ignore this feature if $feature_count is lower than $min
if($feature_count < $min) { continue; } // Stop loop completely if $feature_count is higher than $max if($feature_count > $max) { break; }
?>

<div class="col-xs-6 col-sm-3">

<article id="post-<?php echo $feature->ID; ?>" <?php post_class(‘clearfix bike-features’); ?> role="article">

<h2><?php echo $feature->post_title; ?></h2>

<?php echo $feature->post_content; ?>

</article>

<!– end article –>
</div>

<?php
// Start a new row after every 4 posts
if ($i % 4 == 0) {
echo ‘</div>

<!– .row –>
<div class="row">’;
}
$i++;
};
?>
</div>

<!– .row –>
</div>

<!– .bike-features-content –>

<div id="pagination">
<?php

// Ajax will grab the post ID from here.
echo ‘<span id="bike-id" data-bike-id="’ . $bike_id . ‘"></span>’;

// Pagination
$prev = ($current_page – 1);
$next = ($current_page + 1);

$pagination = paginate_links( array(
‘base’ => get_permalink() . ‘%#%’ . ‘/’,
‘format’ => ‘?page=%#%’,
‘current’ => $page,
‘total’ => $pages,
‘prev_text’ => ‘&lsaquo;’,
‘next_text’ => ‘&rsaquo;’,
‘type’ => ‘plain’,
) );

echo $pagination;
?>
</div>

<!– #pagination –>
</div>

<!– #bike-features-inner –>
</div>

<!– #bike-features-container –>

<?php } // end if ($features) wp_reset_postdata(); } ?>
[/php]

One thing to note in the above snippet: the JavaScript function will grab the $bike_id from here to then pass back to the php file via $_POST[‘bikeID’] as mentioned earlier. You can see the JavaScript side of this below, in the Ajax ‘data’ object.

The Ajax script

Okay, finally we add the Ajax to load more linked posts without reloading the page!

pagination.js

[code lang=”js”]
jQuery(document).ready(function($){

$(document).on( ‘click touchstart’, ‘#pagination a’, function( event ) {

// Fade out the old content for a smooth transition
$(‘#bike-features-inner’).fadeOut();

// The pagination button you clicked will pass the page number back to the callback function
var page = $(this).attr(‘href’);

// This will pass the post ID back to the callback function
var bikeID = $(this).closest(‘#pagination’).find(‘#bike-id’).attr(‘data-bike-id’);

event.preventDefault();
$.ajax({
url: ajaxpagination.ajaxurl,
type: ‘post’,
data: {
action: ‘dl_bike_features’, // the name of the function in your php file
page: page, // access in php function via $_POST[‘page’]
bikeID: bikeID // access in php function via $_POST[‘bikeID’]
},
success: function( result ) {
// Replace the content of the container div w/ the output from dl_bikeFeatures()
$(‘#bike-features-inner’).html(result);
// fade in this content for a smooth transition
$(‘#bike-features-inner’).fadeIn();
},
error: function(MLHttpRequest, textStatus, errorThrown){
alert(‘Sorry, something went wrong. Please try again.’);
}
})
})

});

[/code]

The Ajax data object properties ‘bikeID’ and ‘page’ (lines 20 and 21 in the js file above) provide the values for $_POST['bikeID'] and $_POST['page'] in the below php function (lines 32 and 37), as indicated in the Ajax property type: post. As I mentioned a moment ago, JavaScript grabs the value for bikeID from the span with id=”bike-id.” It’s a bit circular, I know. Now that you’ve seen the full code, here’s the full life cycle of that variable: it starts with PHP getting the post ID from the $post global, which passes to the JavaScript variable bikeID, which then gets passed back to the PHP function via $_POST['bikeID'].

All together now

Here’s the php script all in one piece:

pagination.php

[php]
<?php function ajax_pagination_enqueue() { // Register and enqueue our javascript file wp_register_script( ‘pagination’, get_template_directory_uri() . ‘/library/js/pagination.js’, // or wherever you put the file in your theme directory array(‘jquery’)); wp_enqueue_script(‘pagination’); // Connect to WordPress’ built-in Ajax file wp_localize_script( ‘pagination’, ‘ajaxpagination’, array( ‘ajaxurl’ => admin_url(‘admin-ajax.php’)
));

}

add_action( ‘wp_enqueue_scripts’, ‘ajax_pagination_enqueue’ );

// Hook this function to WordPress’ Ajax actions
add_action( ‘wp_ajax_nopriv_dl_bike_features’, ‘dl_bike_features’ );
add_action( ‘wp_ajax_dl_bike_features’, ‘dl_bike_features’ );

function dl_bike_features() {

/*
** ACF Relationship query
**
*/
global $post;

if ($post->ID) {
// On initial page load, just grab the post ID…
$bike_id = $post->ID;
} else {
// … but once you’re using Ajax, need to get the ID via Ajax
$bike_id = $_POST[‘bikeID’];
}

// Get all posts that are linked to the current post via ACF Relationship
$features = get_posts(array(
// the post type of the posts linked to current post
‘post_type’ => ‘bike-features’,
// fetch all of them–we’ll divide them into separate pages in a moment
‘posts_per_page’ => -1,
‘meta_query’ => array(
array(
// name of ACF Relationship field
‘key’ => ‘bikes_with_this_feature’,
// ID of current post
‘value’ => $bike_id,
‘compare’ => ‘LIKE’
)
)
));

/*
** Pagination variables
*/

if (isset($_POST[‘page’])) {
// Set $page to data from Ajax, if available
$page = $_POST[‘page’];
} else {
// … if not, set default to 1 (for initial page load)
$page = 1;
}

// Set up $current_page to calculate page for previous / next buttons
if (!empty($page)) {
$current_page = intval($page);
}

// $page is coming from the paginator links, so need to extract the page from the url
$tens = substr($page, -3, 1);
$hundreds = substr($page, -4, 1);

/*
** If the character 3 or 4 spaces back from the end is not a number,
** intval will convert it to 0, which will test as false in the if statements below
*/
$tens = intval($tens);
$hundreds = intval($hundreds);

if ($hundreds) { // triple-digit number
$page = intval(substr($page, -4, 3));
} elseif ($tens) { // triple-digit number
$page = intval(substr($page, -3, 2));
} else { // single-digit number
$page = intval(substr($page, -2, 1));
}

$feature_count = 0; // iterator
$features_per_page = 16; // How many features to display on each page
$total = count( $features );
$pages = ceil( $total / $features_per_page );
$min = ( ( $page * $features_per_page ) – $features_per_page ) + 1;
$max = ( $min + $features_per_page ) – 1;

if( $features ) {
// Iterator for rows
$i = 1;
?>

<div id="bike-features-container">

<div id="bike-features-inner">

<div class="bike-features-content">
<!– Note: using Bootstrap grid –>

<div class="row">
<?php foreach( $features as $feature ) {

$feature_count++;

// Ignore this feature if $feature_count is lower than $min
if($feature_count < $min) { continue; } // Stop loop completely if $feature_count is higher than $max if($feature_count > $max) { break; }
?>

<div class="col-xs-6 col-sm-3">

<article id="post-<?php echo $feature->ID; ?>" <?php post_class(‘clearfix bike-features’); ?> role="article">

<h2><?php echo $feature->post_title; ?></h2>

<?php echo $feature->post_content; ?>

</article>

<!– end article –>
</div>

<?php
// Start a new row after every 4 posts
if ($i % 4 == 0) {
echo ‘</div>

<!– .row –>
<div class="row">’;
}
$i++;
};
?>
</div>

<!– .row –>
</div>

<!– .bike-features-content –>

<div id="pagination">
<?php

// Ajax will grab the post ID from here.
echo ‘<span id="bike-id" data-bike-id="’ . $bike_id . ‘"></span>’;

// Pagination
$prev = ($current_page – 1);
$next = ($current_page + 1);

$pagination = paginate_links( array(
‘base’ => get_permalink() . ‘%#%’ . ‘/’,
‘format’ => ‘?page=%#%’,
‘current’ => $page,
‘total’ => $pages,
‘prev_text’ => ‘&lsaquo;’,
‘next_text’ => ‘&rsaquo;’,
‘type’ => ‘plain’,
) );

echo $pagination;
?>
</div>

<!– #pagination –>
</div>

<!– #bike-features-inner –>
</div>

<!– #bike-features-container –>

<?php } // end if ($features) wp_reset_postdata(); } ?>
[/php]

A wee bit of CSS

Last but not least, a couple of simple style rules to ensure that the jQuery fades work properly.

pagination.css

[css]
#bike-features-inner {
visibility: hidden;
}

#bike-features-inner > * {
visibility: visible;
}
[/css]

Good luck, and let us know if you have any questions!

Additional Resources

jQuery Ajax documentation
How to use Ajax in WordPress in general
How to set up pagination with ACF
How to query ACF relationship fields
Custom Post Type UI

DesignLoud

DesignLoud is a web development & digital marketing agency located in Wilmington, NC. Our team takes great pleasure in teaching others how to build and market their websites to see higher returns.

Leave a Comment