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

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_t3he_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.


Comments

Leave a Reply

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