CODE

Load More Posts with AJAX

You’ve probably seen many sites where you get fast, short webpages that have a “Load More” button. Clicking on that button performs an AJAX request to the server and fetches a few more posts or headlines. In this article I will show you how you can add one or multiple of those using Timber and Toolbox.

Prerequisites

For this to work, you will need the following plugins:

  • Beaver Builder
  • Toolbox Plugin
  • Timber Library Plugin
  • Easy UIkit Plugin

Part 1: Setting up the Twig Template

For the layout, we are going to setup a single template file that will handle both the first request and the AJAX requests with the same design. For this we are going to add the template as an external post/file.

1. Activate the Twig Template CPT

If you haven’t done so already in your setup, activate the Twig Template CPT by going to “Settings > Toolbox > Timber Templates” and enable the “Enable Twig Templates CPT” setting.

2. Create a new Twig Template

Create a new Twig template by selecting “Add New” on the appropriate CPT.

Naming the template

Make sure to call the template “post-load-more” and check if the slug is also called “post-load-more”. This slug will be used to load the twig template later on.

The template is going to make use of a few query-parameters. Specifically, one for the number of posts (numberposts) we want to show and one for the offset (offset) in case of multiple pages.

One thing we also want, is to set a default number of posts in the template. That way we only need to provide it once in our template.

We are also going to construct the query in the template.

  {% set numberposts = 5 -%}
{% set offset = 0 -%}
{% if constant( 'DOING_AJAX' ) is defined %}
  {% set numberposts = request.get.numberposts|default(numberposts) -%}
  {% set offset = request.get.offset|default(0) -%}
{% endif %}

We’re setting the number of posts to 5 and the offset to 0 to start of with. Next, we are checking for a constant called ‘DOING_AJAX’, which is generally something that is set when entering an AJAX call. This will be done in part 2, where we create the PHP, so for now just remember that.

In this case, when DOING_AJAX is defined, we will try to get the numberposts that are passed with the request, along with the offset that is also passed in.

Now that we have those, we can use the values to set the query-arguments and get the posts.

Getting one extra post

Instead of getting the number of posts that we want, we are going to have it get one extra. That way we’ll be able to tell that there are more posts left to be shown, so we need to add a “load more” button.

We do this by setting the ‘numberposts’ variable to numberposts + 1

  {% set numberposts = 5 -%}
{% set offset = 0 -%}
{% if constant( 'DOING_AJAX' ) is defined %}
  {% set numberposts = request.get.numberposts|default(numberposts) -%}
  {% set offset = request.get.offset|default(0) -%}
{% endif %}
{% set args = {
	        'post_type' : 'post',
	        'numberposts' : numberposts + 1,
	        'offset' : offset,
	        'orderby' : 'date',
	        'order' : 'DESC'
	        } 
-%}
{% set posts = tb.get_posts( args ) -%}

It could well be that we don’t have any posts, so let’s make sure to handle that first:

  {% set numberposts = 5 -%}
{% set offset = 0 -%}
{% if constant( 'DOING_AJAX' ) is defined %}
  {% set numberposts = request.get.numberposts|default(numberposts) -%}
  {% set offset = request.get.offset|default(0) -%}
{% endif %}
{% set args = {
	        'post_type' : 'post',
	        'numberposts' : numberposts + 1,
	        'offset' : offset,
	        'orderby' : 'date',
	        'order' : 'DESC'
	        } 
-%}
{% set posts = tb.get_posts( args ) -%}
{% if posts|length %}
{# we have at least one posts #}
{% endif %}
{% if posts|length == 0  and offset == 0 -%}
    <div class="uk-margin-small-top" uk-grid>
    	<div>
      		<h3>There are currently no posts available</h3>
		</div>
    </div>
{% endif %}

When posts|length is not equal to 0, we know we can show some posts. When posts|length == 0 and offset == 0 we know that even the first request came up empty, so we display a message there.

Next, let’s add our grid:

  {% set numberposts = 5 -%}
{% set offset = 0 -%}
{% if constant( 'DOING_AJAX' ) is defined %}
  {% set numberposts = request.get.numberposts|default(numberposts) -%}
  {% set offset = request.get.offset|default(0) -%}
{% endif %}
{% set args = {
	        'post_type' : 'post',
	        'numberposts' : numberposts + 1,
	        'offset' : offset,
	        'orderby' : 'date',
	        'order' : 'DESC'
	        } 
-%}
{% set posts = tb.get_posts( args ) -%}
{% if posts|length %}
    {% if constant( 'DOING_AJAX' ) is not defined %}
            <div class="uk-grid uk-grid-match uk-child-width-1-1 uk-child-width-1-2@s uk-child-width-1-3@m uk-flex-center" uk-grid>
    {% endif %}
	{% for item in posts|slice(0,numberposts) -%}
	{% set item = Post( item.ID) -%}
	<div class="cpt-post-{{item.ID}}">
    	<div class="uk-card uk-card-default uk-card-body">
        	<h3>{{ item.title -}}</h3>
            {{ item.preview.length(30).read_more( 'Read more' ) -}}
        </div>
    </div>
        {% if loop.last and posts|length == numberposts + 1 %}
            <div class="uk-width-1-1 post-load-more-placeholder">
            	<a href="javascript:void(0);" class="load-more" data-offset="{{ offset + numberposts }}" data-numberposts="{{ numberposts }}" data-action="post_load_more" data-replace=".post-load-more-placeholder">Load more</a>
    	        <div class="post-loading-more uk-hidden"><div uk-spinner></div> Loading more items</div>
            </div>
        {% endif %}
	{% endfor -%}
    {% if constant( 'DOING_AJAX' ) is not defined %}
            </div>
    {% endif %}
{% endif %}
{% if posts|length == 0  and offset == 0 -%}
    <div class="uk-margin-small-top" uk-grid>
    	<div>
      		<h3>There are currently no posts available</h3>
		</div>
    </div>
{% endif %}

Again, you may notice that we are using a check to see if constant DOING_AJAX is defined. In this case, if it is NOT defined, we will add the surrounding uk-grid DIV element. And a few lines further down, close it of course using the same syntax.

In order for us to only return the number of posts that we’ve set (in this example’s case 5), we use the |slice() filter to limit it to the numberposts again.

Because, when getting posts using the get_posts() method, we only have access to the default WP Post Object, we re-set the item to a Timber Post Object, using {% set item = Post(item.ID) %}. This way you’ll have access to lots of great Timber features.

{% if loop.last and posts|length == numberposts + 1 %}..{% endif %} checks if we’ve reached the last item in our loop, and if the number of posts returned by the query does indeed match at least one extra. If so, we also render a full width div that will function as a placeholder and has the link to get more posts.

Notice that in our placeholder, we write our settings as data-attributes, so that our javascript (part 3) can get that easily and perform the AJAX request. The data-offset is set to the current offset + numberposts, and data-numberposts is set to use the same amount again, 5. Also notice that the data-action attribute is set to ‘post_load_more’.

3. Adding a macro

To make things a bit more readable, you can add a twig macro. Use an import at the top to include the template itself as a variable macros, and add the macro itself to the bottom. This concludes our Twig Template.

4. The Finished Twig Template

  {% import _self as macros %}
{% set numberposts = 5 -%}
{% set offset = 0 -%}
{% if constant( 'DOING_AJAX' ) is defined %}
  {% set numberposts = request.get.numberposts|default(numberposts) -%}
  {% set offset = request.get.offset|default(0) -%}
{% endif %}
{% set args = {
	        'post_type' : 'post',
	        'numberposts' : numberposts + 1,
	        'offset' : offset,
	        'orderby' : 'date',
	        'order' : 'DESC'
	        } 
-%}
{% set posts = tb.get_posts( args ) -%}
{% if posts|length %}
    {% if constant( 'DOING_AJAX' ) is not defined %}
            <div class="uk-grid uk-grid-match uk-child-width-1-1 uk-child-width-1-2@s uk-child-width-1-3@m uk-flex-center" uk-grid>
    {% endif %}
	{% for item in posts|slice(0,numberposts) -%}
	    {{ macros.item( item ) -}}
        {% if loop.last and posts|length == numberposts + 1 %}
            <div class="uk-width-1-1 post-load-more-placeholder">
            	<a href="javascript:void(0);" class="load-more" data-offset="{{ offset + numberposts }}" data-numberposts="{{ numberposts }}" data-action="post_load_more" data-replace=".post-load-more-placeholder">Load more</a>
    	        <div class="post-loading-more uk-hidden"><div uk-spinner></div> Loading more items</div>
            </div>
        {% endif %}
	{% endfor -%}
    {% if constant( 'DOING_AJAX' ) is not defined %}
            </div>
    {% endif %}
{% endif %}
{% if posts|length == 0  and offset == 0 -%}
    <div class="uk-margin-small-top" uk-grid>
    	<div>
      		<h3>There are currently no posts available</h3>
		</div>
    </div>
{% endif -%}
{#
		macro for the item
#}
{% macro item(item) %}
	{% set item = Post( item.ID) -%}
	<div class="cpt-post-{{item.ID}}">
    	<div class="uk-card uk-card-default uk-card-body">
        	<h3>{{ item.title -}}</h3>
            {{ item.preview.length(30).read_more( 'Read more' ) -}}
        </div>
    </div>
{% endmacro -%}

Part 2: The PHP Needed for the AJAX calls

Next, we will need to add the PHP necessary for retrieving the posts from the database. We will create a generic function to load_more_twig posts, that you could reuse for different custom post types.

  /**
 * Callback to get more posts but using a twig template
 * @return [type] [description]
 */
function load_more_twig( $twig ) {

	DEFINE( 'DOING_AJAX' , true );

	$data = \Timber::get_context();

	// buffer output
	ob_start();

	// Use try to prevent failure
	try {
		// render $twig template with numberposts and offset as variables
		$result = \Timber::render( $twig , $data );
	} catch( Exception $e ) {
		if ( apply_filters( 'toolbox/twig_error_debug' , true ) ) echo '[ error handling twig template ] ' . $e->getMessage();
	}

	// echo result
	echo ob_get_clean();

	// wp_die() because we want our AJAX Request to end here
	wp_die();

}

add_action( 'wp_ajax_post_load_more' , 'foo_post_load_more' );
add_action( 'wp_ajax_nopriv_post_load_more' , 'foo_post_load_more' );

function foo_post_load_more() {

	load_more_twig( 'post-load-more.twig' );

}

Remember that we set the data-action attribute in the Twig Template? This sets the action query-argument for the AJAX request, and takes the place of {action} in WordPress ajax calls:

  /* example */
add_action( 'wp_ajax_{action}' , 'your_callback_function' );  // logged in users
add_action( 'wp_ajax_nopriv_{action}' , 'your_callback_function' );  //not logged in users, like visitors (no privileges)

With the add_action we are telling WordPress to run the callback/function foo_post_load_more when the request comes in. In turn, it will call the function load_more_twig using the Twig Template you created earlier.

Part 3: The javascript

Last step is setting up the javascript to make it interact. If you examine the Twig template again, you may have noticed that – when we reach the end of our loop, and the number of posts is actually larger than the number of posts we display, that we display a placeholder for the “load more” button.

We will setup some javascript that will trigger the AJAX request when someone clicks the link with the “load-more” class.

You can add the javascript to your Beaver Builder “Global Settings -> Javascript” area.

  (function($){
    $(document).ready( function() {
        $( 'body' ).delegate('.load-more', 'click' , function() {
           const $this = $(this);
           $($this.data( 'replace' )).addClass( 'old' );
           $this.addClass( 'uk-hidden' );
           $($this.data( 'replace' )).find( '[class*=loading-more]' ).removeClass( 'uk-hidden' );
           
           $.ajax( {
               type: 'GET',
               url: '/wp-admin/admin-ajax.php?action=' + $this.data( 'action' ),
               data: { 
                   numberposts: $this.data( 'numberposts' ),
                   offset: $this.data( 'offset' ),
               },
               success: (data) => {
                   $( data ).insertAfter( $this.data( 'replace' ) );
                   $($this.data( 'replace' ) + '.old').remove();
               }
           })
        });
    });
})(jQuery);

This code will replace the placeholder with the newly fetched AJAX request. That’s why we used a check for the DOING_AJAX constant, so that our twig template only returns new grid elements. That way they will simply flow into the layout, generating extra columns where necessary and maintaining responsiveness.

Note that we are using a delegate listener here. Because the link is dynamically inserted each time there appears to be more posts after the last one displayed, its better to make it listen on the body element, and from there track down the .load-more classes, on old or new elements.

Adding the Timber Posts Module

Now that everything is in place, all you need to do is add a “Timber Posts Module” to your Beaver Builder Layout. This is the last step in making it work.

Drag in the module, and leave the content setting to “Main Query”. This way it will use the cached main database-query.

Head over to the “Template” Tab and change it to the following:

  {% include 'post-load-more.twig' %}

That’s it! This will get the Twig Template and display the first 5 posts. At the bottom, you will have “Load More” button. When you click it, the javascript will trigger a request to the server’s admin-ajax.php with the action we setup in the PHP section. It will use Timber to get the same Twig Template again, but this time the constant DOING_AJAX is set, and the javascript has passed in the numberposts and offset, thereby outputting the next set of posts!

Using it as a shortcode

You can also use it anywhere you like, using a simple shortcode. One requirement is that it does need the javascript that we added before, but you can use it in the Gutenberg Editor. All you need to do is add a /shortcode block and add the following shortcode:

  [toolboxtwig twigtemplate="post-load-more"]

Make sure to also have the toolboxtwig shortcode activated in “Settings > Toolbox > Timber Templates” on your Dashboard.

Let me know

Did you find this useful, or did you encounter any bugs or problems? Let me know in the comments!

Beaverplugins

Web ninja with PHP/CSS/JS and Wordpress skills. Also stand-in server administrator, father of four kids and husband to a beautiful wife.
Always spends too much time figuring out ways to do simple things even quicker. So that you can benefit.