How to “Load More” posts via AJAX?

On the taxonomy-{name}.php template for my “Organisation” taxonomy, I am currently showing a limited number of posts, grouped for each term of taxonomy “Format”.

Where the number of available posts exceeds that which is displayable, I now want to enable AJAX-based loading of more posts.

I have followed countless tutorials and StackExchange questions on this topic, but something is still not clicking. For example, I implemented Misha Rudrastyh’s “Load More” button, but the code just would not load any posts. I am lost and on the verge of giving up.

I have a post block with a “More” link like this, ready and waiting…

enter image description here

That corresponds to posts of “Format” taxonomy term “executive” for one taxonomy “Organisation” term. Here is the relevant portion of taxonomy-organisation.php, containing my query…

 <?php


 // This will output posts in blocks for each "Format" taxonomy term.
 // But we also want to use this loop to output posts *without* a "Format" taxonomy term.


 $formats = array("interview","executive","analyst","oped","","ma","earnings","financialanalysis","ipo","marketindicators","industrymoves");

 foreach ($formats as $format) {

      // Term of the slug above
      $format_term = get_term_by('slug', $format, "format");

      // Formulate the secondary tax_query for "format" with some conditionality
      /* *********************************** */
      /*           Posts by Format           */
      /* *********************************** */
      if (!empty($format)) {
           $posts_per_page = 8;
           $tax_q_format_array = array(
                'taxonomy' => 'format', // from above, whatever this taxonomy is, eg. 'source'
                'field'    => 'slug',
                'terms'    => $format_term->slug,
                'include_children' => false
           );
           // Oh, and set a title for output below
           $section_title = $format_term->name;
      /* *********************************** */
      /*         Format without Format       */
      /* *********************************** */
      } elseif (empty($format)) {
           $posts_per_page = 12;
           $tax_q_format_array = array(
                'taxonomy' => 'format', // from above, whatever this taxonomy is, eg. 'source'
                'operator' => 'NOT EXISTS', // or 'EXISTS'
           );
           // Oh, and set a title for output below
           $section_title="Reporting";
      }

      // Query posts
      $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
      $args = array(
        // pagination
        // 'nopaging' => false,
        'posts_per_page' => $posts_per_page,
        // 'offset' => '4',
        // 'paged' => $paged,
        // posts
        'post_type' => array( 'article', 'viewpoint' ),
        // order
        'orderby' => 'date',
        'order'   => 'DESC',
        // taxonomy
        'tax_query' => array(
          array(
            'taxonomy' => 'company', // from above, whatever this taxonomy is, eg. 'source'
            'field'    => 'slug',
            'terms'    => $organisation->slug,
            'include_children' => false
          ),
          $tax_q_format_array
       ),
      );

      // the query
      $posts_org = new WP_Query($args);



      if ( $posts_org->have_posts() ) { ?>

           <h5 class="mt-0 pt-2 pb-3 text-secondary"><?php echo $section_title; ?> <span class="badge badge-secondary badge-pill"><?php echo $posts_org->found_posts; ?></span></h5>

           <div class="row pb-3">
           <?php
           while( $posts_org->have_posts() ) {
                $posts_org->the_post();
                get_template_part('partials/loops/col', 'vertical');
           }
           if ($posts_org->found_posts > $posts_per_page) {
                echo '<p class="text-secondary pl-3 pb-0 load_more" style="opacity: 0.6"><i class="fas fa-plus-circle"></i> More</p>';
           }
           echo '</div>';
           wp_reset_postdata(); // reset the query

      } else {
           // echo '<div class="col"><p>'.__('Sorry, no posts matched your criteria.').'</p></div>';
      } // end if have_posts


 }










 ?>

And here is the include file used by get_template_part to output post item thumbnails and links…

    <?php
    // Get fallback image if needed
    @require(dirname(__FILE__).'/../source_thumbnail_fallback.php');
    // @ Suppress "Notices" generated when post has no source set (ie no source at array position [0])
    ?>


    <div class="col-sm-6 col-md-4 col-lg-3 col-xl-2 post-item overlay-bg <?php echo $display_class; ?>">
            <a href="https://wordpress.stackexchange.com/questions/328685/<?php the_field("source_url' ); ?>" class="text-dark" target="_blank">
                    <img src="http://www.myserver.com/to/image/location/<?php echo $source_image_url; ?>" class="img-fluid rounded mb-2">
                    <p><?php the_title(); ?></p>
                    <?php
                    //Do something if a specific array value exists within a post
                    $format_terms = wp_get_post_terms($post->ID, 'format', array("fields" => "all"));
                                    // foreach($format_terms as $format_single) { ?>
                        <span class="badge <?php get_format_badge_colour_class($format_terms[0]->term_id); ?> font-weight-normal mr-2 overlay-content"><?php echo $format_terms[0]->name; ?></span>
                                    <?php // } ?>
            </a>
    </div>

From the tutorials and questions, I have gleaned that the process seems to be…

  • Register/enqueue/localise a Javascript file containing the relevant code, and pass parameters to it from PHP functions.php
  • Some sort of WordPress AJAX handler in functions.php which does a WP_Query for more posts?

But I am just not understanding how to implement in my situation.

FYI, my self-built theme is based on Bootstrap and, until now, I have de-registered WordPress’ built-in jquery, replaced by enqueuing Bootstrap’s recommended https://code.jquery.com/jquery-3.3.1.slim.min.js as jQuery. I’m not sure which is the right option, it seemed like this has a bearing on WP_Ajax_*.

I’m aware a similar question has been asked before, but my query seems unique and many questions/answers seem also to refer to unique situations, ie. offering answers specifically designed to work with twentyfifteen theme.

Update (Feb 15, 2019):

I have come a long way toward solving this, using a combination of:

  • Artisans Web tutorial for the basic concept
  • Anti’s suggestion for establishing which of multiple “More” links was clicked (necessary to feed unique arguments back to looped WP queries).
  • Further research/exploration based on available code, to learn how to take and pass those variables.
  • Further jQuery research to learn how to change UI elements.

I can now share the following two pieces of code, which works 90% well and I could consider offering this as an answer, or Anti’s answer.

Theme template fragment:

 <?php
 $organisation = get_query_var('organisation');
 $org_id_prefixed = get_query_var('org_id_prefixed');
 ?>


 <?php


 /* ************************************************************************************************************** */
 /*                                                                                                                */
 /*          LIST POSTS BY FORMAT TERM, WITH DYNAMIC AJAX MORE-POST LOADING                                        */
 /*                                                                                                                */
 /*          Outline:                                                                                             */
 /*          This will output posts in blocks for each "Format" taxonomy term.                                     */
 /*          But we also want to use this loop to output posts *without* a "Format" taxonomy term.                 */
 /*          >> Method via Sajid @ Artisans Web, https://artisansweb.net/load-wordpress-post-ajax/                 */
 /*                                                                                                                */
 /*          Dynamic:                                                                                              */
 /*          1. Javascript: When "More" link is clicked                                                            */
 /*          2. Send current "organisation", clicked "format" and "page" as variables to function                  */
 /*          3. PHP: Do WP query for next page of posts, return to javascript                                      */
 /*          4. Javascript: Append results to the clicked container                                                */
 /*                                                                                                                */
 /*          Dynamic method:                                                                                       */
 /*          $format used as ID for i) .row .my-posts container and ii) .loadmore link, so that we know:           */
 /*          a) Which "Load more" link was clicked                                                                 */
 /*          b) Which box to return new output to                                                                  */
 /*          >> Help via Antti Koskinen @ StackOverflow, https://wordpress.stackexchange.com/a/328760/39300        */
 /*                                                                                                                */
 /* ************************************************************************************************************** */


 // Specify which "Format" terms we want to display post blocks for
 // ("reporting" here is a wildcard, used to accommodate posts without a "Format" term set)
 $formats = array("interview","executive","analyst","oped","reporting","ma","earnings","financialanalysis","ipo","marketindicators","industrymoves");

 // For each of them,
 foreach ($formats as $format) {

      // 1. Get actual term of the slug above
      $format_term = get_term_by('slug', $format, "format");

      // 2. Formulate the secondary tax_query for "format" with some conditionality
      /* *********************************** */
      /*           Posts by Format?          */
      /* *********************************** */
      if ($format!="reporting") {
           // $posts_per_page = 8;
           $tax_q_format_array = array(
                'taxonomy' => 'format', // from above, whatever this taxonomy is, eg. 'source'
                'field'    => 'slug',
                'terms'    => $format_term->slug,
                'include_children' => false
           );
           // Oh, and set a title for output below
           $section_title = $format_term->name;
      /* *********************************** */
      /*         Format without Format?      */
      /* *********************************** */
      } elseif ($format=="reporting") {
           // $posts_per_page = 12;
           $tax_q_format_array = array(
                'taxonomy' => 'format', // from above, whatever this taxonomy is, eg. 'source'
                'operator' => 'NOT EXISTS', // or 'EXISTS'
           );
           // Oh, and set a title for output below
           $section_title="Reporting";
      }

      // 3. Set query arguments
      $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
      $args = array(
        // pagination
        // 'nopaging' => false,
        'posts_per_page' => '8', // $posts_per_page,
        // 'offset' => '4',
        'paged' => $paged,
        // posts
        'post_type' => array( 'article', 'viewpoint' ),
        // order
        'orderby' => 'date',
        'order'   => 'DESC',
        // taxonomy
        'tax_query' => array(
          // #1 Organisation: the current one
          array(
            'taxonomy' => 'company', // from above, whatever this taxonomy is, eg. 'source'
            'field'    => 'slug',
            'terms'    => $organisation->slug,
            'include_children' => false
          ),
          // #2 Format: as above
          $tax_q_format_array
        ),
      );

      // 4. Query for posts
      $posts_org = new WP_Query($args);


      // 5. Output
      if ( $posts_org->have_posts() ) { ?>

           <h5 class="mt-0 pt-4 pb-3 text-secondary"><?php echo $section_title; ?> <span class="badge badge-secondary badge-pill"><?php echo $posts_org->found_posts; ?></span></h5>
           <div class="row pb-0 my-posts" id="<?php echo $format; ?>">
           <?php
           while( $posts_org->have_posts() ) {
                $posts_org->the_post();
                get_template_part('partials/loops/col', 'vertical');
           }
           ?>
           </div>
           <?php
           // wp_reset_postdata(); // reset the query

           // "More" posts link
           if ($posts_org->found_posts > $posts_per_page) {
                echo '<p class="text-secondary pb-0" style="opacity: 0.6"><a href="https://wordpress.stackexchange.com/questions/328685/javascript:;" class="loadmore" id="'.$format.'"><i class="fas fa-plus-circle"></i> <span class="moretext">More</span></a></p>';
           }

      } else {
           // echo '<div class="col"><p>'.__('Sorry, no posts matched your criteria.').'</p></div>';
      } // end if have_posts


 }


 ?>



 <script type="text/javascript">

 // Set starting values which to send from Javascript to WP query function...
 var ajaxurl = "<?php echo admin_url( 'admin-ajax.php' ); ?>";
 var page = 2;                                                                // Infer page #2 to start, then increment at end
 var org_slug = "<?php echo $organisation->slug; ?>";                         // Slug of this "organisation" term

 jQuery(function($) {

      // When this selector is clicked
    $('body').on('click', '.loadmore', function() {

           // Get ID of clicked link (corresponds to original $format value, eg. "executive"https://wordpress.stackexchange.com/"reporting")
           var clicked_format = $(this).attr('id');

           // Change link text to provide feedback
           $('#'+clicked_format+' .moretext').text('Loading...');
           $('#'+clicked_format+' i').attr('class', 'fas fa-cog fa-spin');

           // 1. Send this package of variables to WP query function
        var data = {
            'action': 'load_posts_by_ajax',
            'page': page,
                'org_slug': org_slug,
                'clicked_format': clicked_format,
            'security': '<?php echo wp_create_nonce("load_more_posts"); ?>'
        };

           // 2. Send to query function and get results
        $.post(ajaxurl, data, function(response) {
                // Append the returned output to this selector
            $(response).appendTo('div#'+clicked_format).hide().fadeIn(2000); // was: $('div#'+clicked_format).append(response).fadeIn(4000); Reverse method, cf. https://stackoverflow.com/a/6534160/1375163
                // Change link text back to original
                $('#'+clicked_format+' .moretext').text('More');
                $('#'+clicked_format+' i').attr('class', 'fas fa-plus-circle');
                // Increment page for next click
            page++;
        });





    });



 });
 </script>

In functions.php:

  // Called from org_deck2_many.php

  add_action('wp_ajax_load_posts_by_ajax', 'load_posts_by_ajax_callback');
  add_action('wp_ajax_nopriv_load_posts_by_ajax', 'load_posts_by_ajax_callback');

  function load_posts_by_ajax_callback() {
      check_ajax_referer('load_more_posts', 'security');

      // 1. Query values are passed from referring page, to Javascript and to this query...
      $paged = $_POST['page'];                               // Passed from page: Which page we are on
      $org_slug = $_POST['org_slug'];                        // Passed from page: Organisation taxonomy term slug
      $clicked_format = $_POST['clicked_format'];            // ID of the clicked "More" link (corresponds to original $format value, eg. "executive"https://wordpress.stackexchange.com/"reporting")
      // $tax_q_format_array = $_POST['tax_q_format_array']; // Passed from page: 'Format' term-specifying part for 'tax_query'

      // 2. Formulate the secondary tax_query for "format" with some conditionality
      /* *********************************** */
      /*           Posts by Format?          */
      /* *********************************** */
      if ($clicked_format!="reporting") {
           $tax_q_format_array = array(
                'taxonomy' => 'format', // from above, whatever this taxonomy is, eg. 'source'
                'field'    => 'slug',
                'terms'    => $clicked_format,
                'include_children' => false
           );
           // $offset = NULL;
      /* *********************************** */
      /*         Format without Format?      */
      /* *********************************** */
      } elseif ($clicked_format=="reporting") {
           $tax_q_format_array = array(
                'taxonomy' => 'format', // from above, whatever this taxonomy is, eg. 'source'
                'operator' => 'NOT EXISTS', // or 'EXISTS'
           );
           // $offset="12";      // More articles shown in "Reporting"
      }

      // 3. Set query arguments
      $args = array(
           // posts
          'post_type' => array( 'article', 'viewpoint' ),
          'post_status' => 'publish',
          // 'offset' => $offset,
          // pages
          'posts_per_page' => '8',
          'paged' => $paged,
          // taxonomy
          'tax_query' => array(
            // #1 Organisation: the current one
            array(
             'taxonomy' => 'company', // from above, whatever this taxonomy is, eg. 'source'
             'field'    => 'slug',
             'terms'    => $org_slug,
             'include_children' => false
            ),
            // #2 Format: as above
            $tax_q_format_array
          ),
      );

      // 4. Query for posts
      $posts_org = new WP_Query( $args );

      // 5. Send results to Javascript
      if ( $posts_org->have_posts() ) :
          ?>
          <?php while ( $posts_org->have_posts() ) : $posts_org->the_post(); ?>
              <?php get_template_part('partials/loops/col', 'vertical'); ?>
          <?php endwhile; ?>
          <?php
      endif;

      wp_die();
  }

However, there is a remaining issue that I know about…

There seems to be a Javascript issue with picking up the clicked “More” link’s ID as clicked_format and clicking some of the multiple “More” links on the page. I can see this because the first few “More” clicks succeed, but then clicking a different “More” link can leave the process in the “Loading” state.

I suspect it is something to do with when clicked_format gets set and destroyed (or not). I have tried unsetting it, but to no effect.

Should i consider filing a separate, specific question – in either WordPress Development or StackOverflow (Javascript) – for this?

http://recordit.co/AI7OjJUVmH

2 Answers
2

You can use ajax to load more posts on your archive page.

  1. attach a js/jquery click event on the More link
  2. send ajax request to admin-ajax.php (use wp_localize_script to get the ajax url to front end) with page number (track this with js variable).
  3. handle ajax request in php. Add custom ajax actions with add_action( 'wp_ajax_my_action', 'my_action' ); and add_action( 'wp_ajax_nopriv_my_action', 'my_action' ); (for non-logged in users). Send my_action part of the action hook as ajax request action parameter.
  4. query posts. Get the correct page (WP_Query paged arg, commented out in your code) for the query from the ajax request, e.g. $_POST['page']
  5. send queried posts back to front end
  6. append posts from ajax response to the dom with js/jquery.

I don’t think it matters that much if you use the default jQuery version or a newer one to load more posts.

Please have a look at the code examples here, https://www.billerickson.net/infinite-scroll-in-wordpress/ (not my blog), for more detailed example and explanation. I know code examples are preferred to links, but the code examples written by Bill Erickson are rather long so I think it is more to convenient post a link insted of copy-pasting the examples here.

You should also have a look at the codex entry about ajax, it is really helpfull. https://codex.wordpress.org/AJAX_in_Plugins

I only took a cursory glance at the query function you posted, but I think it should be fine for the post most part for loading more posts with ajax.


UPDATE 15.2.2019

If you need to identify which “Load more” link/button is clicked you can do this in js/jquery.

// Html
<div id="posts-cat-1" class="posts-section">
// posts here
<button id="cat-name-1" class="load-more">Load more</button>
</div>

<div id="posts-cat-2" class="posts-section">
// posts here
<button class="load-more" data-category="cat-name-2">Load more</button>
</div>

<div id="cat-name-3" class="posts-section">
// posts here
<button class="load-more">Load more</button>
</div>

// jQuery
jQuery('.load-more').on('click',function(event){
// get cat name from id
var catName1 = jQuery(this).attr('id');
// or from data attribute
var catName2 = jQuery(this).attr('data-category'); // or .data('category');
// or from parent section, perhaps not ideal
var catName3 = jQuery(this).closest('.posts-section').attr('id');

// Then pass the catName to your ajax request data so you can identify which category to query in php.
});

UPDATE 16.2.2019

To hide the “Load more” button when there are no more posts to show you can add a simple if-else conditional to your jquery ajax call.

// 2. Send to query function and get results
$.post(ajaxurl, data, function(response) {

  // Check if there's any content in the response.
  // I think your ajax php should return empty string when there's no posts to show
  if ( '' !== response ) {

    // Append the returned output to this selector
    $(response).appendTo('div#'+clicked_format).hide().fadeIn(2000); // was: $('div#'+clicked_format).append(response).fadeIn(4000); Reverse method, cf. https://stackoverflow.com/a/6534160/1375163

    // Change link text back to original
    $('a#'+clicked_format+' i').attr('class', 'fas fa-plus-circle'); // Icon
    $('a#'+clicked_format+' .moretext').text('More');                // Text

    // Increment "data-page" attribute of clicked link for next click
    // page++;
    $('a#'+clicked_format).find('span').data().page++

  } else {

    // This adds display:none to the button, use .remove() to remove the button completely
    $('a#'+clicked_format).hide();

  }

});

Leave a Comment