Hey, I need to add a “Load more” button below the collection page. I am using dawn theme. Can anyone help?
Thanks ![]()
Goal: Add either infinite scroll or a “Load more” button to the Dawn theme collection page.
Implemented solutions:
Results/outcomes:
Issue reported (half items loaded):
Notes:
Hey, I need to add a “Load more” button below the collection page. I am using dawn theme. Can anyone help?
Thanks ![]()
'use strict';
var Ajaxinate = function ajaxinateConstructor(config) {
var settings = config || {};
/*
pagination: Selector of pagination container
method: [options are 'scroll', 'click']
container: Selector of repeating content
offset: 0, offset the number of pixels before the bottom to start loading more on scroll
loadingText: 'Loading', The text changed during loading
callback: null, function to callback after a new page is loaded
*/
var defaultSettings = {
pagination: '#AjaxinatePagination',
method: 'scroll',
container: '#AjaxinateLoop',
offset: 0,
loadingText: 'Loading',
callback: null
};
// Merge configs
this.settings = Object.assign(defaultSettings, settings);
// Bind 'this' to applicable prototype functions
this.addScrollListeners = this.addScrollListeners.bind(this);
this.addClickListener = this.addClickListener.bind(this);
this.checkIfPaginationInView = this.checkIfPaginationInView.bind(this);
this.stopMultipleClicks = this.stopMultipleClicks.bind(this);
this.destroy = this.destroy.bind(this);
// Set up our element selectors
this.containerElement = document.querySelector(this.settings.container);
this.paginationElement = document.querySelector(this.settings.pagination);
this.initialize();
};
Ajaxinate.prototype.initialize = function initializeTheCorrectFunctionsBasedOnTheMethod() {
// Find and initialise the correct function based on the method set in the config
if (this.containerElement) {
var initializers = {
click: this.addClickListener,
scroll: this.addScrollListeners
};
initializers[this.settings.method]();
}
};
Ajaxinate.prototype.addScrollListeners = function addEventListenersForScrolling() {
if (this.paginationElement) {
document.addEventListener('scroll', this.checkIfPaginationInView);
window.addEventListener('resize', this.checkIfPaginationInView);
window.addEventListener('orientationchange', this.checkIfPaginationInView);
}
};
Ajaxinate.prototype.addClickListener = function addEventListenerForClicking() {
if (this.paginationElement) {
this.nextPageLinkElement = this.paginationElement.querySelector('a');
this.clickActive = true;
if (this.nextPageLinkElement !== null) {
this.nextPageLinkElement.addEventListener('click', this.stopMultipleClicks);
}
}
};
Ajaxinate.prototype.stopMultipleClicks = function handleClickEvent(event) {
event.preventDefault();
if (this.clickActive) {
this.nextPageLinkElement.innerHTML = this.settings.loadingText;
this.nextPageUrl = this.nextPageLinkElement.href;
this.clickActive = false;
this.loadMore();
}
};
Ajaxinate.prototype.checkIfPaginationInView = function handleScrollEvent() {
var top = this.paginationElement.getBoundingClientRect().top - this.settings.offset;
var bottom = this.paginationElement.getBoundingClientRect().bottom + this.settings.offset;
if (top <= window.innerHeight && bottom >= 0) {
this.nextPageLinkElement = this.paginationElement.querySelector('a');
this.removeScrollListener();
if (this.nextPageLinkElement) {
this.nextPageLinkElement.innerHTML = this.settings.loadingText;
this.nextPageUrl = this.nextPageLinkElement.href;
this.loadMore();
}
}
};
Ajaxinate.prototype.loadMore = function getTheHtmlOfTheNextPageWithAnAjaxRequest() {
this.request = new XMLHttpRequest();
this.request.onreadystatechange = function success() {
if (this.request.readyState === 4 && this.request.status === 200) {
var newContainer = this.request.responseXML.querySelectorAll(this.settings.container)[0];
var newPagination = this.request.responseXML.querySelectorAll(this.settings.pagination)[0];
this.containerElement.insertAdjacentHTML('beforeend', newContainer.innerHTML);
this.paginationElement.innerHTML = newPagination.innerHTML;
if (this.settings.callback && typeof this.settings.callback === 'function') {
this.settings.callback(this.request.responseXML);
}
this.initialize();
}
}.bind(this);
this.request.open('GET', this.nextPageUrl);
this.request.responseType = 'document';
this.request.send();
};
Ajaxinate.prototype.removeClickListener = function removeClickEventListener() {
this.nextPageLinkElement.addEventListener('click', this.stopMultipleClicks);
};
Ajaxinate.prototype.removeScrollListener = function removeScrollEventListener() {
document.removeEventListener('scroll', this.checkIfPaginationInView);
window.removeEventListener('resize', this.checkIfPaginationInView);
window.removeEventListener('orientationchange', this.checkIfPaginationInView);
};
Ajaxinate.prototype.destroy = function removeEventListenersAndReturnThis() {
// This method is used to unbind event listeners from the DOM
// This function is called manually to destroy "this" Ajaxinate instance
var destroyers = {
click: this.removeClickListener,
scroll: this.removeScrollListener
};
destroyers[this.settings.method]();
return this;
};
{{ 'ajaxinate.min.js' | asset_url | script_tag }}
Find the file pagination.liquid:
Search for {%- if paginate.next -%} and add class=“infinite_next” in
{%- if paginate.next -%}
- {%- render 'icon-caret' -%}
{%- endif -%}
Adding code in facet.js File
Search for static renderProductCount(html) and below that add the following code:
const endlessCollection = new Ajaxinate({
container: '#product-grid',
pagination: '.infinite_next',
});
.pagination__list{
display: none;
}
The above code will help you automatically load more products on the collection page when customers scroll to the bottom of the page
@leechoostore if you want the “load more” button. follow the step
Add file collection-load-more.js in assest with content
const products_on_page = document.getElementById('product-grid');
const nextUrl = document.getElementById('paginateNext');
let next_url = nextUrl.dataset.nextUrl;
const load_more_btn = document.getElementsByClassName('load-more_btn')[0];
const load_more_spinner = document.getElementsByClassName('load-more_spinner')[0];
async function getNextPage() {
try {
let res = await fetch(next_url);
return await res.text();
} catch (error) {
console.log(error);
}
}
async function loadMoreProducts() {
load_more_btn.style.display = 'none';
load_more_spinner.style.display = 'block';
let nextPage = await getNextPage();
const parser = new DOMParser();
const nextPageDoc = parser.parseFromString(nextPage, 'text/html');
load_more_spinner.style.display = 'none';
const productgrid = nextPageDoc.getElementById('product-grid');
const new_products = productgrid.getElementsByClassName('grid__item');
const newUrl = document.getElementById('paginateNext');
const new_url = newUrl.dataset.nextUrl;
if (new_url) {
load_more_btn.style.display = 'flex';
}
next_url = new_url;
for (let i = 0; i < new_products.length; i++) {
products_on_page.appendChild(new_products[i]);
}
}
and replace
{%- if paginate.pages > 1 -%}
{% render 'pagination', paginate: paginate, anchor: '' %}
{%- endif -%}
to
{%- if paginate.pages > 1 -%}
Load More
{%- endif -%}
Add this code to base.css
.load-more {
position: relative !important;
justify-content: center !important;
display: flex !important;
}
Please try to follow my instructions @leechoostore
This worked perfectly for infinite scroll! Thank you so much!
This process worked perfectly for “Load more” button! Thanks a bunch!
life saver, thank you!
This is great - really helpful, thank you.
I’m having a small issue which is the button is only loading half the number of items that the pagination is set to. I’m not a complete novice when it comes to javascript but I couldn’t see anything in the code that would be causing that, and when I revert to the shopify built-in pagination with pages, the correct number of products loads per page.
Any ideas what could be causing this? I haven’t edited your JS file, and my product grid liquid is below:
{{ 'template-collection.css' | asset_url | stylesheet_tag }}
{{ 'component-card.css' | asset_url | stylesheet_tag }}
{{ 'component-price.css' | asset_url | stylesheet_tag }}
{% if section.settings.image_shape == 'blob' %}
{{ 'mask-blobs.css' | asset_url | stylesheet_tag }}
{%- endif -%}
{%- unless section.settings.quick_add == 'none' -%}
{{ 'quick-add.css' | asset_url | stylesheet_tag }}
{%- endunless -%}
{{ 'custom__card.css' | asset_url | stylesheet_tag }}
{{ 'custom__collection.css' | asset_url | stylesheet_tag }}
{%- if section.settings.quick_add == 'standard' -%}
{%- endif -%}
{%- if section.settings.quick_add == 'bulk' -%}
{%- endif -%}
{%- style -%}
.section-{{ section.id }}-padding {
padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
}
@media screen and (min-width: 750px) {
.section-{{ section.id }}-padding {
padding-top: {{ section.settings.padding_top }}px;
padding-bottom: {{ section.settings.padding_bottom }}px;
}
}
{%- endstyle -%}
{%- paginate collection.products by section.settings.products_per_page -%}
{% comment %} Sort is the first tabbable element when filter type is vertical {% endcomment %}
{%- if section.settings.enable_sorting and section.settings.filter_type == 'vertical' -%}
{%- endif -%}
{{ 'component-facets.css' | asset_url | stylesheet_tag }}
{%- if section.settings.enable_filtering or section.settings.enable_sorting -%}
## {{ collection.title }}
{% if section.settings.show_description %}
{% endif %}
{% render 'facets',
results: collection,
enable_filtering: section.settings.enable_filtering,
enable_sorting: section.settings.enable_sorting,
filter_type: section.settings.filter_type,
paginate: paginate
%}
{% if section.settings.show_description %}
{{ collection.description }}
{% endif %}
{%- endif -%}
{{ paginate.next.url }}
{%- if collection.products.size == 0 -%}
##
{{ 'sections.collection_template.empty' | t -}}
{{
'sections.collection_template.use_fewer_filters_html'
| t: link: collection.url, class: 'underlined-link link'
}}
{%- else -%}
{% assign skip_card_product_styles = false %}
{%- for product in collection.products -%}
{% assign page_number = paginate.current_page | minus: 1 %}
{% assign base_index = section.settings.products_per_page | times: page_number %}
{% assign grid_index = base_index | plus: forloop.index %}
{% assign lazy_load = false %}
{%- if grid_index > 4 -%}
{%- assign lazy_load = true -%}
{%- endif -%}
{% for block in section.blocks %}
{% case block.type %}
{% when 'content' %}
{% if block.settings.index == grid_index %}
{% render 'grid-content-item',
span_horizontal: block.settings.horizontal_span,
span_vertical: block.settings.vertical_span,
forloop: forloop,
image: block.settings.image,
title: block.settings.title,
link: block.settings.link,
link_label: block.settings.link_label,
color_scheme: block.settings.color_scheme,
block_id: block.id
%}
{% endif %}
{% endcase %}
{% endfor %}
{% render 'grid-card-product',
forloop: forloop,
card_product: product,
media_aspect_ratio: section.settings.image_ratio,
image_shape: section.settings.image_shape,
show_secondary_image: section.settings.show_secondary_image,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating,
lazy_load: lazy_load,
skip_styles: skip_card_product_styles,
quick_add: section.settings.quick_add,
section_id: section.id
%}
{%- assign skip_card_product_styles = true -%}
{%- endfor -%}
{%- if paginate.pages > 1 -%}
Load More
{%- endif -%}
{%- endif -%}
{%- endpaginate -%}
{% if section.settings.image_shape == 'arch' %}
{{ 'mask-arch.svg' | inline_asset_content }}
{%- endif -%}
I have also the same issue with you.
let nextPage = await getNextPage();
This fetches the exact next page(page=2), but after parsing to DOM
( const nextPageDOM = parser.parseFromString(nextPage, ‘text/html’);), the log of nextPageDOM shows that it is page=1, and display the half number of page=2. Really weird and hope you help me out, thanks.
I ended up using a different version of Ajaxinate that works for me.
"use strict";
var Ajaxinate = function ajaxinateConstructor(config) {
var settings = config || {};
/*
pagination: Selector of pagination container
method: [options are 'scroll', 'click']
container: Selector of repeating content
offset: 0, offset the number of pixels before the bottom to start loading more on scroll
loadingText: 'Loading', The text changed during loading
callback: null, function to callback after a new page is loaded
*/
var defaultSettings = {
pagination: "#AjaxinatePagination",
method: "scroll",
container: "#AjaxinateLoop",
offset: 0,
loadingText: "Loading",
callback: null,
};
// Merge configs
this.settings = Object.assign(defaultSettings, settings);
// Bind 'this' to applicable prototype functions
this.addScrollListeners = this.addScrollListeners.bind(this);
this.addClickListener = this.addClickListener.bind(this);
this.checkIfPaginationInView = this.checkIfPaginationInView.bind(this);
this.stopMultipleClicks = this.stopMultipleClicks.bind(this);
this.destroy = this.destroy.bind(this);
// Set up our element selectors
this.containerElement = document.querySelector(this.settings.container);
this.paginationElement = document.querySelector(this.settings.pagination);
this.initialize();
};
Ajaxinate.prototype.initialize =
function initializeTheCorrectFunctionsBasedOnTheMethod() {
// Find and initialise the correct function based on the method set in the config
if (this.containerElement) {
var initializers = {
click: this.addClickListener,
scroll: this.addScrollListeners,
};
initializers[this.settings.method]();
}
};
Ajaxinate.prototype.addScrollListeners =
function addEventListenersForScrolling() {
if (this.paginationElement) {
document.addEventListener("scroll", this.checkIfPaginationInView);
window.addEventListener("resize", this.checkIfPaginationInView);
window.addEventListener(
"orientationchange",
this.checkIfPaginationInView
);
}
};
Ajaxinate.prototype.addClickListener = function addEventListenerForClicking() {
if (this.paginationElement) {
this.nextPageLinkElement = this.paginationElement.querySelector("a");
this.clickActive = true;
if (this.nextPageLinkElement !== null) {
this.nextPageLinkElement.addEventListener(
"click",
this.stopMultipleClicks
);
}
}
};
Ajaxinate.prototype.stopMultipleClicks = function handleClickEvent(event) {
event.preventDefault();
if (this.clickActive) {
this.nextPageLinkElement.innerHTML = this.settings.loadingText;
this.nextPageUrl = this.nextPageLinkElement.href;
this.clickActive = false;
this.loadMore();
}
};
Ajaxinate.prototype.checkIfPaginationInView = function handleScrollEvent() {
var top =
this.paginationElement.getBoundingClientRect().top - this.settings.offset;
var bottom =
this.paginationElement.getBoundingClientRect().bottom +
this.settings.offset;
if (top <= window.innerHeight && bottom >= 0) {
this.nextPageLinkElement = this.paginationElement.querySelector("a");
this.removeScrollListener();
if (this.nextPageLinkElement) {
this.nextPageLinkElement.innerHTML = this.settings.loadingText;
this.nextPageUrl = this.nextPageLinkElement.href;
this.loadMore();
}
}
};
Ajaxinate.prototype.loadMore = function loadMore() {
this.request = new XMLHttpRequest();
this.request.onreadystatechange = function success() {
if (!this.request.responseXML) {
return;
}
if (!this.request.readyState === 4 || !this.request.status === 200) {
return;
}
var newContainer = this.request.responseXML.querySelectorAll(
this.settings.container
)[0];
var newPagination = this.request.responseXML.querySelectorAll(
this.settings.pagination
)[0];
this.containerElement.insertAdjacentHTML(
"beforeend",
newContainer.innerHTML
);
if (typeof newPagination === "undefined") {
this.removePaginationElement();
} else {
this.paginationElement.innerHTML = newPagination.innerHTML;
if (
this.settings.callback &&
typeof this.settings.callback === "function"
) {
this.settings.callback(this.request.responseXML);
}
this.initialize();
}
}.bind(this);
this.request.open("GET", this.nextPageUrl);
this.request.responseType = "document";
this.request.send();
};
Ajaxinate.prototype.removeClickListener = function removeClickEventListener() {
this.nextPageLinkElement.addEventListener("click", this.stopMultipleClicks);
};
Ajaxinate.prototype.removePaginationElement =
function removePaginationElement() {
this.paginationElement.innerHTML = "";
this.destroy();
};
Ajaxinate.prototype.removeScrollListener =
function removeScrollEventListener() {
document.removeEventListener("scroll", this.checkIfPaginationInView);
window.removeEventListener("resize", this.checkIfPaginationInView);
window.removeEventListener(
"orientationchange",
this.checkIfPaginationInView
);
};
Ajaxinate.prototype.destroy = function removeEventListenersAndReturnThis() {
// This method is used to unbind event listeners from the DOM
// This function is called manually to destroy "this" Ajaxinate instance
var destroyers = {
click: this.removeClickListener,
scroll: this.removeScrollListener,
};
destroyers[this.settings.method]();
return this;
};
It’s also necessary to updates facets.js (assuming you’re on Dawn or another base Shopify template) to refire the ajaxinate when facets are applied or disapplied. This is done by replacing this function in facets.js:
static renderProductGridContainer(html) {
document.getElementById('ProductGridContainer').innerHTML = new DOMParser()
.parseFromString(html, 'text/html')
.getElementById('ProductGridContainer').innerHTML;
document
.getElementById('ProductGridContainer')
.querySelectorAll('.scroll-trigger')
.forEach((element) => {
element.classList.add('scroll-trigger--cancel');
});
}
with this:
static renderProductGridContainer(html) {
document.getElementById("ProductGridContainer").innerHTML = new DOMParser()
.parseFromString(html, "text/html")
.getElementById("ProductGridContainer").innerHTML;
document
.getElementById("ProductGridContainer")
.querySelectorAll(".scroll-trigger")
.forEach((element) => {
element.classList.add("scroll-trigger--cancel");
});
const endlessCollection = new Ajaxinate({
container: "#product-grid",
pagination: "#AjaxinatePagination",
method: "click",
});
}
Obviously adjust the settings here as needed according to the Ajaxinate settings you want to use.
Then on my main collection page I have this for the button:
{%- if paginate.next -%}
Load More
{%- endif -%}
and this script at the bottom of the section:
(Again obviously adjust settings as needed). Hope that helps!