15 1 0 4000 1 https://codeblock.co.za 300 true 0
Infinite Scroll & Load More With JQuery and PHP

Infinite Scroll & “Load More” With JQuery and PHP

6 Comments

In the previous post we built a pagination system with search, filter and sorting features using PHP and JQuery. In this article, we’ll go over an alternative by lazy loading our products to have an infinite scroll and as a bonus, instead of  only having them auto-load, we’ll include a “load more” button that lets the user choose to load more items. If you’d like to use only one of these features, you can easily delete omit the code responsible.

Prerequisites

  • An intermediate understanding of PHP.
  • An intermediate understanding of JQuery and how Ajax works.
  • JQuery installed/included.

You can find a working version of the tutorial here. If you’d like to download the code, you can find the repo on GitHub.

For this tutorial, I’m going to use the same products array I created before but the same concept applies to any array. So you can pull the data from your own database, a remote API or a manually defined array, like this one.

What’s the Objective

We know that the goal is but to (hopefully) make it easier to understand what we need to do, I’m going to break it down a bit.

  • First off, when the page loads, we’ll have to call the first row of products and place it into the designated container.
  • Secondly, if there are more products than the ones that are available, there should be a “load more” button for the user to click (if you’re going to use that option).
  • If the user clicks the “load more” button or scrolls to the bottom of the page, we need to load the next row of products.
  • Additionally we need to ensure that if any active sorting and filters are in place, the Ajax call should send those queries when we call the next row.
  • And finally, if there are no more products when the use scrolls or clicks, the “load more” button shouldn’t be loaded.

Setting Up the HTML

For the most part, the index.php is very similar to the one we used previously, but there are two important changes.

<div class="row page-title">
    <div class="col-md-12">
        <h1 style=""><?php echo $title; ?></h1>
    </div>
</div>

<div class="filters row">
    <div class="col-md-12">
        <form id="filter-form" class="form-inline" action='' method="GET">
            <div class="form-row">
                
                <div class="col-lg-auto col-md-12 col-sm-6 px-2">
                    
                    <span>Filter Products</span>
                    
                    <select name="cats" class="form-control form-control-sm">
                        <option value="">Filter by Category</option>
                        <option value="Caps">Caps</options>
                        <option value="Jackets">Jackets</options>
                        <option value="Pants">Pants</options>
                        <option value="Shirts">Shirts</options>
                    </select>
                    
                    <input type="text" name="search" placeholder="Search" class="form-control form-control-sm" title="Search by product name or SKU" />
                    
                </div>
                
                <div class="col-lg-auto col-md-12 col-sm-6  px-2">
                    
                    <span> Sort By </span>
                    
                    <select name="sort-by" class="form-control form-control-sm">
                        <option value="title">Title</option>
                        <option value="price">Price</option>
                    </select>
                    
                    <select name="sort-order" class="form-control form-control-sm">
                        <option value="ASC">ASC</option>
                        <option value="DESC">DESC</option>
                    </select>
                    
                </div>
                
                <div class="col-lg-auto col-md-12 col-sm-12  px-2">
                    <input type="submit" value="Filter" class="btn btn-sm btn-secondary btn-block"/>
                </div>
                
            </div>
            
        </form>
        
        <input type="hidden" id="current-query" value="" >
        
    </div>
    
</div>

<div id="all-products" class="row all-products">

</div>

Looking at the code above, you’ll see I have a hidden input which I’ve purposefully placed outside of the search and filter form. <input type="hidden" id="current-query" value=""> I’ve put this hidden input outside of the form because I don’t want it to be submitted with the rest of the form. I’m actually going to use it to store any current filters that are place. Remember, with an Ajax call, we don’t have query parameters in the URL, so we need to have them somewhere.

The second change is the #all-products div. Before, we placed our products template in this div. This time, we’ll dynamically insert the content into this div with JQuery and Ajax when the page loads.

Keeping the PHP Search and Filter Logic Intact With Load More and Infinite Scroll

As with the HTML, the PHP used in the previous post is similar but there are some important changes. I’ll include the full code for you to compare the two, but I’ll also explain it all.

<?php 

// Bring in the products
require('products-array.php');

// Filters
$cat_filter = isset($_GET['cats']) ? $_GET['cats'] : '';
$search_filter = isset($_GET['search']) ? $_GET['search'] : '';

// Default limit
$limit = isset($_GET['limit']) ? $_GET['limit'] : 3;

// Default offset
$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;

// filter products array based on query
if(!empty($cat_filter) || !empty($search_filter)) {
    $filtered_products = array();
    foreach($products as $product) {
        if( !empty($cat_filter) && !empty($search_filter) ) {
            
            if( ( strpos($product['title'], $search_filter) !== false || $product['sku'] == $search_filter ) && $product['category'] == $cat_filter ) {
                $filtered_products[] = $product;
            }
        }
        else if(!empty($cat_filter) && $product['category'] == $cat_filter) {

            $filtered_products[] = $product;
        }
        else if(!empty($search_filter) && ( strpos($product['title'], $search_filter) !== false || $product['sku'] == $search_filter) ) {
            
            $filtered_products[] = $product;
        }
        
    }
    
    $products = $filtered_products;
}

// Sorting
function sortByQuery($a, $b) {
    $sort_by = isset($_GET['sort-by']) ? $_GET['sort-by'] : 'title';
    $sort_order = isset($_GET['sort-order']) ? $_GET['sort-order'] : 'ASC';
    
    if ($sort_order == 'DESC') {
        return $a[$sort_by] < $b[$sort_by];
    }
    else {
        return $a[$sort_by] > $b[$sort_by];
    }
}
usort($products, 'sortByQuery');

$queried_products = array_slice($products, $offset, $limit);

$total_products = count($products);

// Get the total rows rounded up the nearest whole number
$total_rows = (int)ceil( $total_products / $limit );

$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;

if (count($queried_products)) {
    foreach ($queried_products as $product) { ?>
       
       <div class="col-md-4 product-wrapper">
           <div class="product">
               <div class="product-image">
                   <img src="<?php echo $product['image']; ?>" />
                </div>
                <div class="product-title">
                    <h3><?php echo $product['title']; ?></h3>
                </div>
                <div class="product-info">
                    <p class="product-sku"><?php echo $product['sku']; ?></p>
                    <p class="product-price">R <?php echo $product['price']; ?></p>
                    <p class="product-category">Listed in <?php echo $product['category']; ?></p>
                </div>
           </div>
           
       </div>
           
     <?php }
}

else {
    return false;
}

if ( $current_row < $total_rows ) { ?>
    <div class="col-md-12 text-center">
        <button id="load-more" class="btn btn-primary mx-auto" >Load more</button>
    </div>
<?php }
else {
    return false;
}

Most of this code is self-explanatory. Right at the top, we import our products from the hard-coded array, but these can come from anywhere. Right after that, we’re simply defining some variables to hold any filter queries that are in place. The same applies to the limit but the offset is different. Before, we weren’t getting queries to change the offset because the pagination logic was calculating the offset. This time, however, the Ajax call will send a value that will need to be queried, which you’ll see when we get to that section.

The code in the filters and sorting logic is exactly the same as before. We’re simply rearranging and filtering the products array based on the query we get from Ajax and that new array will be stored as $queried_products.

$total_rows and $total_products also remain the same as before. In the pagination tutorial, I referred to pages but since we’re not dealing with pages here, I’ve renamed pages to rows to make it easier to understand.

Just like the offset, the $current_row (previously $current_page) is no longer being defined by the pagination logic. This time, we’re calculating which row is being called using the $offset and $limit values. If the offset is 0, then we are on the first row, otherwise it’s the offset divided by the limit plus 1. For example, if we have 20 products and we’re showing 4 per row, we’ll have 5 rows (20 divided by 4 is 5). If our offset is currently 8 (we’ve seen two rows), it means our first item is actually the 9th item. To get which row the 9th item is in, we take 8, our offset, and divide it by 4, our limit, which gives us 2. Add 1 to that and we’re on row 3.

The next bit is nothing more than an HTML template for a single product, which you should ideally include in a separate file.

Finally, at the end of the PHP logic, if there is a query in place and no products are in place, we’ll send a false response for Ajax to capture. Additionally, if the current row number is less than the last row number, we’ll show the “load more” button but if it’s not, again, we’ll return false.

Adding the JQuery Functions to Infinite Scroll / Load More

Instead of pasting all the code into one block here, I’ll first break up the sections of the code.

Global Variables

First I’ve defined some global variables and I’ve purposefully made them global so that when they’re changed, they aren’t reset when the “load more” function runs. They are as follows:

var limit = 3;
var offset = 0;
var noMoreProducts = false;
var loadingInProgress = false;

The limit and offset variables are self explanatory. The offset is dynamic and will change as more products are loaded but the limit is hard-coded. You can make this dynamic, but I won’t discuss that here. Personally, I think it would be unnecessary since JQuery will load more content with the infinite scroll or the “load more” button. I’ve made the limit only 3 items for this tutorial, but you could make it more.

The noMoreProducts variable is initially set to false because we have products to load at first, but this will change when we scroll to the end of the queried of the queried.

The loadingInProgress variable will also be set to true right before we do the Ajax call and after the content is loaded we’ll set it back to false. This is just there to prevent duplicate calls which will load the same row twice.

The “Load More” Javascript Function

function loadProducts( extraQueries = {}) {
    
    if (noMoreProducts) return;
    
    let queries = {
        'offset' : offset,
        'limit' : limit
    };
    
    // Additional Queries to be pushed to the "queries" object
    if( ! jQuery.isEmptyObject(extraQueries) ) {
        $.each(extraQueries, function(param, value){
          queries[param] = value;
        });
    }
    
    if (!loadingInProgress) {
        
        loadingInProgress = true;
        
        $.get('content/resources/products.php', queries, function(data) {
            
            if(!data) {
                noMoreProducts = true;
                $('#load-more').remove();
            }
            else {
                offset += limit;
                $('#load-more').remove();
                $('#all-products').append(data);
            }
            
            loadingInProgress = false;
            
        });
    }
    
}

Here we have a function called loadProducts() which takes in one optional parameter of any extra query parameters we want to include before querying the products. This parameter must be a JavaScript Object. If you look in the function we have an existing object called queries which contains the limit and offset. The function will send these values with the Ajax request. If you include others in the functions parameter, it will send those with the request as well.

Before any of the code in this function even runs, we need to check if noMoreProducts is true and simply return and exit the function if it is. If there are more products, we set loadingInProgress to true and make the Ajax call to grab the content of the PHP we implemented earlier.

Now, if you remember, in the PHP, if there aren’t any products left, we’ll return a false directive. So, when we get the response from the Ajax call, our first point of order is to check if that is false. If it is, we set noMoreProducts to true, change loadingInProgress back to false and subsequently leave the function since there’s nothing left to do there.

If the response from Ajax wasn’t false, our response should be the HTML data which we then append to the #all-products container. Since the products template has the “load more” button, we should remove the one already there before we load the data and also change the offset to the sum of limit and the current offset. That means we’ve set the offset to the start of the next row and if you remember, this is a global variable so when we call the function, the value will not be reset. If this variable was included in the function, it would be reset when the function is called.

So in a nutshell, this function that will actually load the data does only three things if there is HTML data to get.

  • It changes the offset to the next set of items,
  • It removes the load more button
  • and it loads the fetched HTML into the products div.

Load the First Set of Products

Now that we have our function in place we can use it to load the first set of products (the first row) into the products container. This will happen when the page loads using JQuery’s $(document).ready().

$(document).ready(function() {
    loadProducts();
})

Submitting The Filter Form

One thing to note about the filter form is that, although it still works with Ajax, whenever the use submits it, it will reset the infinite scrolling and “load more” functionality. If we don’t do this, things could get wacky since we’re appending data to the “#all-products” div. This is the only time we’ll do this.

$(document).on('submit', '#filter-form', function(e){
    e.preventDefault();
    
    let form = $(this);
    
    $.get('content/resources/products.php', $(form).serialize(), function(data){
        
        $('#all-products').html(data);
        
        let formQueries = {};

        $.each($('#filter-form').serializeArray(), function(_, input) {
          formQueries[input.name] = input.value;
        });
            
        $('#current-query').val(JSON.stringify(formQueries));
        
        // make offset limit because when the form loads, the first batch of products will already be there, then it must be offset to the next batch
        offset = limit;
        noMoreProducts = false;
        
    });
});

In the code above, there are some busy things happening here. Right. So we make an Ajax call to products.php with the serialized data from the form inputs. We then put that data into the page but then we have to save that query somewhere. If we don’t and we try to load more products, it won’t know we have a query in place and just start loading from the unfiltered products array. This is where that hidden input comes into play.

So, we serialize each of the inputs from the form and then “push” each one them to the formQueries. The hidden input (with and ID of #current-query) is given the value of formQueries. Since it’s not possible to save a JavaScript object as an HTML property, we have to convert it to a string first and we use JSON.stringify to achieve that.

After that when we’re just about done, we need to set the offset to the limit because the first set of items will already be loaded but when we load more or scroll now, we want to start from the next set. We’ll also set noMoreProducts to false since we’re basically resetting our infinite scroll.

Adding Functionality to the “Load More” Button

When a user clicks the “load more” button, obviously we want to load the next set of products but if there’s a filter query in place we also want to send that information. If we don’t, we’ll end up getting the original, unfiltered array of products and we don’t want that. So we can check the hidden input with the ID of #current-query if it has a value. If there is a value, remember it will be saved as a string, but loadProducts() will only accept an object parameter. We can use JSON.parse to convert that string back to an object and pass that parameter into loadProducts(). Now, when loadProducts() is called, all our current queries and our limit and offset are all sent to products.php which will return what we requested. For fun, we’ll also change the text in the “load more” button to indicate to the use that something is happening as well as disable it so it can’t be resubmitted. Since we will ultimately removed and replace the button with the newly loaded one, we don’t have to change the text back.

If you don’t want the “load more” button and prefer to have infinite scroll only, you can simply exclude this function from your code.

$(document).on('click', '#load-more', function() {
    
    let currentQuery = {};
    
    $('#load-more').html('Loading...').prop('disabled', true);
    
    if($('#current-query').val().length) {
        currentQuery = JSON.parse($('#current-query').val());
    }
    loadProducts( currentQuery );
    
});

Adding the Infinite Scroll JavaScript Function

This function is pretty much the same as button one except the event we’re looking for is scroll. Here we check if the user has reached the bottom of the page and if they have, we’ll invoke the same set of rules we did in the button function.

If you don’t want the infinite scrolling and prefer to have the “load more button” only, you can simply exclude this function from your code.

$(window).scroll(function() {
    
    if( ( $(window).scrollTop() + $(window).height() ) >= $(document).height() ) {
        
        let currentQuery = {};
    
        if( $('#current-query').val().length) {
            currentQuery = JSON.parse($('#current-query').val());
        }

        $('#load-more').html('Loading...').prop('disabled', true);
        
        loadProducts(currentQuery);
        
    }
});

You’re Locked and Loaded

That’s all there is to it. Congratulations if you managed to read all of this. It’s long but I try to explain things as much as possible. Hopefully this infinite scroll and “load more” functionality will help you with one of your projects.

If you find any bugs or you think the code can be improved, please let me know so I can fix it. The goal is to help other developers learn things in new ways but not wrong ways so I’m always open to correction being a mid-level developer myself.

Is this still valid in 2021? Please let me know in the comments below.

Was This Helpful?

How to Sort a Multi-Dimensional Array in PHP
Previous Post
How to Sort a Multi-Dimensional Array in PHP
Create a Custom Alert and Confirmation Dialogue With PHP and JQuery
Next Post
Create a Custom Alert and Confirmation Dialogue With PHP and JQuery

6 Comments

  • 26th Jun 2020 at 4:58 pm
    Hendri

    This is amazinggg, thank you so much Codeblock.co.za.. very very useful !!

    Reply
    • 26th Jun 2020 at 7:01 pm

      You're very welcome and thank you for reading. 🙂

      Reply
  • 8th Jan 2021 at 12:03 pm
    Sam

    Hi Brinley,

    Nice article. What's your solution to the user retaining their place in the product feed when leaving via a product and coming back to continue where they left off?

    Reply
    • 11th Jan 2021 at 10:33 pm

      Apologies for the late reply. Hmm. That's not something I ever thought about but I can see how that could be useful. I found this tweet that you could use to do something similar. It works by storing the position (or in our case, an initial product count) in local storage and then grabs that information the next time the page loads. I'll see if I can make a working example of how I would implement it.

      Reply
  • 1st Mar 2021 at 2:22 pm
    Hashmi

    can i have full code in 2 files please? i am new to programming and i wanna learn executing it. thanks

    Reply
    • 9th Apr 2021 at 11:09 am

      Hi Hashmi. The code is on Github available to download.

      Reply

Leave a Reply