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)