Create Infinite Scroll In WordPress With AJAX Request — Custom Code Without A Plugin.

Imran Sayed
6 min readAug 1, 2021

--

In this blog, we will learn about the infinite scroll. We will also add pagination for Google, which will be hidden for users. Let’s take a brief look at the steps required:

  1. Create a nonce and pass it to the JavaScript file, so it can be verified.
  2. We will create a function that renders the initial set of posts.
  3. Create a function that verifies the nonce and then makes a WP_Query to get more posts
  4. We use Intersection Observer API to track the load more button, so that when it comes into view, we trigger a load more AJAX request through JavaScript function, that calls the above PHP function to make a query and loop through posts and displays them.

Create None for Security and Enqueue Scripts

Let’s Create a nonce for AJAX request and pass the nonce to our JavaScript file which we will create later in the blog. Every time a request is made the nonce will be verified for security.

namespace MyApp;function asset_loader() {
// Registers scripts.
wp_register_script( 'app', 'url-path-to/loadmore.js' ), [ 'jquery' ], filemtime( get_stylesheet_directory() . '/file-path-to/loadmore.js' ), true );


wp_enqueue_script( 'app' );

wp_localize_script( 'app', 'siteConfig', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'ajax_nonce' => wp_create_nonce( 'loadmore_post_nonce' ),
] );
}

add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\\asset_loader' );

Create a post Template

Let’s create a post template. `template-parts/post-card.php`. Please note that for demonstration purposes we are using tailwind CSS. You can add the CSS according to your own requirement.

<?php
/**
* Post Card
*
* Note: Should be called with The Loop
*/
namespace MyApp;

$post_permalink = get_the_permalink();
?>

<section id="post-<?php the_ID(); ?>"
class="mb-5 lg:mb-8 xl:mb-10 px-1 w-full overflow-hidden sm:w-1/2 md:w-1/3 lg:w-1/4">
<header>
<a href="<?php echo esc_url( $post_permalink ); ?>" class="block">
<figure class="img-container relative w-full">
<?php the_post_thumbnail( 'post-thumbnail', [ 'class' => 'absolute w-full h-full left-0 top-0 object-cover' ] ); ?>
</figure>
</a>
</header>
<div class="post-content">
<p class="line-clamp-5 leading-6"><?php echo wp_strip_all_tags( get_the_content() ); ?></p>
</div>
</section>

Now let’s add some functions in functions.php

<?php
/**
* Loadmore functions
*
*/
namespace MyApp;
use \WP_Query;

/**
* Load more script call back
*
*
@param bool $initial_request Initial Request( non-ajax request to load initial post ).
*
*/
function ajax_script_post_load_more( bool $initial_request = false ) {

if ( !$initial_request && ! check_ajax_referer( 'loadmore_post_nonce', 'ajax_nonce', false ) ) {
wp_send_json_error( __( 'Invalid security token sent.', 'text-domain' ) );
wp_die( '0', 400 );
}

// Check if it's an ajax call.
$is_ajax_request = ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) === 'xmlhttprequest';
/**
* Page number.
* If get_query_var( 'paged' ) is 2 or more, its a number pagination query.
* If $_POST['page'] has a value which means its a loadmore request, which will take precedence.
*/
$page_no = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;
$page_no = ! empty( $_POST['page'] ) ? filter_var( $_POST['page'], FILTER_VALIDATE_INT ) + 1 : $page_no;

// Default Argument.
$args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 4, // Number of posts per page - default
'paged' => $page_no,
];

$query = new WP_Query( $args );;

if ( $query->have_posts() ):
// Loop Posts.
while ( $query->have_posts() ): $query->the_post();
get_template_part( 'template-parts/post-card' );
endwhile;

// Pagination for Google.
if ( ! $is_ajax_request ) :
$total_pages = $query->max_num_pages;
get_template_part( 'template-parts/common/pagination', null, [
'total_pages' => $total_pages,
'current_page' => $page_no,
] );
endif;
else:
// Return response as zero, when no post found.
wp_die( '0' );
endif;

wp_reset_postdata();

/**
* Check if its an ajax call, and not initial request
*
*
@see https://wordpress.stackexchange.com/questions/116759/why-does-wordpress-add-0-zero-to-an-ajax-response
*/
if ( $is_ajax_request && ! $initial_request ) {
wp_die();
}

}

/*
* Load more script ajax hooks
*/
add_action( 'wp_ajax_nopriv_load_more', __NAMESPACE__ . '\\ajax_script_post_load_more' );
add_action( 'wp_ajax_load_more', __NAMESPACE__ . '\\ajax_script_post_load_more' );

/*
* Initial posts display.
*/
function post_script_load_more() {

// Initial Post Load.
?>
<div class="vl-container mt-20 md:mt-28 xl:mt-32 mb-28 md:mb-36 xl:mb-40">
<div id="load-more-content" class="flex flex-wrap -mx-1 overflow-hidden">
<?php
ajax_script_post_load_more( true );

// If user is not in editor and on page one, show the load more.
?>
</div>
<button id="load-more" data-page="1"
class="load-more-btn mt-20 block mx-auto px-4 py-2 border border-transparent transition ease-in-out duration-150 cursor-not-allowed">
<span class="screen-reader-text"><?php esc_html_e( 'Load More', 'text-domain' ); ?></span>
<svg class="animate-spin -ml-1 mr-3 h-8 w-8 text-brand-light-blue" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
<?php
}

/**
* Create a short code.
*
* Usage echo do_shortcode('[post_listings]');
*/
add_shortcode( 'post_listings', __NAMESPACE__ . '\\post_script_load_more' );

Pagination Template

<?php
/**
* Pagination Template.
*
* To be used inside the WordPress loop.
* Pagination for Google.
*
*
@package Aquila
*/

if ( empty( $args['total_pages'] ) || empty( $args['current_page'] ) ) {
return null;
}

if ( 1 < $args['total_pages'] ) {
?>
<div id="post-pagination" class="hidden-pagination hidden" data-max-pages="<?php echo esc_attr( $args['total_pages'] ); ?>">
<?php
echo paginate_links( [
'base' => get_pagenum_link( 1 ) . '%_%',
'format' => 'page/%#%',
'current' => $args['current_page'],
'total' => $args['total_pages'],
'prev_text' => __( '« Prev', 'aquila' ),
'next_text' => __( 'Next »', 'aquila' ),
] );
?>
</div>
<?php
}

Now call this wherever you want to load the posts with load more.

echo do_shortcode('[post_listings]');

Add JavaScript

JavaScript. Create a file called `loadmore.js`. We will use the Intersection Observer API for tracking our load more button and firing an AJAX request when the button comes into view.

( function( $ ) {
class LoadMore {
constructor() {
this.ajaxUrl = siteConfig?.ajaxUrl ?? '';
this.ajaxNonce = siteConfig?.ajax_nonce ?? '';
this.loadMoreBtn = $( '#load-more' );

this.options = {
root: null,
rootMargin: '0px',
threshold: 1.0, // 1.0 means set isIntersecting to true when element comes in 100% view.
};

this.init();

}

init() {

if ( ! this.loadMoreBtn.length ) {
return;
}
this.totalPagesCount = $( '#post-pagination' ).data( 'max-pages' );
/**
* Add the IntersectionObserver api, and listen to the load more intersection status.
* so that intersectionObserverCallback gets called if the element intersection status changes.
*
*
@type {IntersectionObserver}
*/
let observer = new IntersectionObserver( ( entries ) => this.intersectionObserverCallback( entries ), this.options );
observer.observe( this.loadMoreBtn[0] );
}

/**
* Gets called on initial render with status 'isIntersecting' as false and then
* everytime element intersection status changes.
*
*
@param {array} entries No of elements under observation.
*
*
@return null
*/
intersectionObserverCallback( entries ) { // array of observing elements

// The logic is apply for each entry ( in this case it's just one loadmore button )
entries.forEach( entry => {
// If load more button in view.
if ( entry?.isIntersecting ) {
this.handleLoadMorePosts();
}
} );
}

/**
* Load more posts.
*
* 1.Make an ajax request, by incrementing the page no. by one on each request.
* 2.Append new/more posts to the existing content.
* 3.If the response is 0 ( which means no more posts available ), remove the load-more button from DOM.
* Once the load-more button gets removed, the IntersectionObserverAPI callback will not be triggered, which means
* there will be no further ajax request since there won't be any more posts available.
*
*
@return null
*/
handleLoadMorePosts() {

// Get page no from data attribute of load-more button.
const page = this.loadMoreBtn.data( 'page' );
if ( !page ) {
return null;
}

const nextPage = parseInt(page) + 1; // Increment page count by one.

$.ajax( {
url: this.ajaxUrl,
type: 'post',
data: {
page: page,
action: 'load_more',
ajax_nonce: this.ajaxNonce
},
success: ( response ) => {

this.loadMoreBtn.data( 'page', nextPage );
$( '#load-more-content' ).append( response );
this.removeLoadMoreIfOnLastPage(nextPage)
},
error: ( response ) => {
console.log( response );
},
} );
}
/**
* Remove Load more Button If on last page.
*
*
@param {int} nextPage New Page.
*/
removeLoadMoreIfOnLastPage = ( nextPage ) => {
if ( nextPage + 1 > this.totalPagesCount ) {
this.loadMoreBtn.remove();
}
}
}

new LoadMore();

} )( jQuery );

That’s all folks. Thank you.

--

--

Imran Sayed

👤 Full Stack Developer at rtCamp, Speaker, Blogger, YouTuber, Wordpress, React, Node, Laravel Developer http://youtube.com/ImranSayedDev