Infinite Scroll is an obvious feature to expect on modern E-Commerce websites. Sadly, Shopify doesn’t provide a first class solution for it.
Here is my take on how to implement it in a way that improves the user experience without blocking access to web crawlers and people who doesn’t have javascript enabled.
Start by creating section file called main-collection.liquid. This is section includes a grid of product, a loading spinner and pagination buttons that will be used by web crawlers. Once we load the script, javascript will take over and those buttons won't be necessary.
{% assign page_size = section.settings.page_size %} {% paginate collection.products by page_size %} <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> {% for product in collection.products %} {% render 'product-card', product: product %} {% endfor %} </div> <div class="main-collection-pagination flex m-5 uppercase text-xs"> {% if paginate.previous.is_link %} <div class="flex-1 text-left"> <a class="main-collection-prev-button" href="{{ paginate.previous.url }}"> {{ paginate.previous.title }} </a> </div> {% endif %} {% if paginate.next.is_link %} <div class="flex-1 text-right"> <a class="main-collection-next-button" href="{{ paginate.next.url }}"> {{ paginate.next.title }} </a> </div> {% endif %} </div> <div class="main-collection-loading opacity-0 flex justify-center mx-5 my-10 uppercase text-xs"> <span>Loading...</span> </div> {% endpaginate %} {% schema %} { "name": "Main Collection", "class": "main-collection", "settings": [ { "type": "range", "id": "page_size", "label": "Page size", "min": 4, "max": 16, "step": 1, "default": 8, "info" : "Number of products displayed per page" } ], "presets": [ { "name": "Main Collection" } ] } {% endschema %} Now let's create an entrypoint for the script that will run when the entire document has been loaded by the browser.
import { initMainCollection } from "./main-collection"; async function main() { const { templateName } = document.body.dataset; if (templateName === "collection") { const sectionElement = document.querySelector( ".shopify-section.main-collection" ); if (sectionElement instanceof HTMLElement) { initMainCollection(sectionElement); } } } document.addEventListener("DOMContentLoaded", main); In the main script we will check if the page contains the main-collection section, and if that's the case we will initialize it using a separate function called initMainCollection.
The code for this function is pretty long, but this is the idea behind it.
- Hide pagination buttons
- Get url of next page results from the next button
- Wait for when user has scrolled to the bottom
- If there is another page, fetch the main section html and insert it after the current one.
- Repeat 1-4 on the new section
/** @param {HTMLElement} section */ export function initMainCollection(section) { if (window.IntersectionObserver) { setupInfiniteScroll(section); } } /** @param {HTMLElement} section */ function setupInfiniteScroll(section) { const nextButton = section.querySelector(".main-collection-next-button"); const pagination = section.querySelector(".main-collection-pagination"); const loading = section.querySelector(".main-collection-loading"); // When infinite scroll is active, pagination // buttons should stay hidden if (pagination instanceof HTMLElement) { hideElement(pagination); } if (loading instanceof HTMLElement) { loading.style.opacity = "1.0"; } else { // Loading element is integral to the infinite scroll // because it will be used as intersection target return; } // Grab url of next page from the button href let nextPageUrl; if (nextButton instanceof HTMLAnchorElement) { nextPageUrl = nextButton.href; } // If there is no next page link, there is no work to be done. if (!nextPageUrl) { hideElement(loading); return; } const observer = new IntersectionObserver(function(entries, observer) { const entry = entries[0]; if (entry.isIntersecting) { // Unsubscribe right away observer.unobserve(entry.target); // do something renderNextSection(); } }); observer.observe(loading); async function renderNextSection() { // Fetch main-collection section from next page let nextPageSectionHtml; try { const response = await fetch(nextPageUrl + "§ions=main-collection"); const data = await response.json(); nextPageSectionHtml = data["main-collection"]; } catch (err) { console.error(err); } // If server returns nothing, leave. if (!nextPageSectionHtml) return; if (loading instanceof HTMLElement) { hideElement(loading); } // Insert section HTML after the current section section.insertAdjacentHTML("afterend", nextPageSectionHtml); const nextPageSection = section.nextElementSibling; if (nextPageSection instanceof HTMLElement) { // Setup infinite scroll on the next section setupInfiniteScroll(nextPageSection); } } } /** @param {HTMLElement} element */ function hideElement(element) { element.style.display = "none"; }
Top comments (0)