I’ll build this as a strong reusable ajax-load-more.js for RX Theme, with button loading, infinite scroll, filters, search, pagination states, accessibility, error handling, and WordPress AJAX/REST compatibility.
My dear friend, create this file:
rx-theme/inc/js/ajax-load-more.js
Paste the full code below.
/*!
* RX Theme - Advanced Ajax Load More
* File: inc/js/ajax-load-more.js
*
* Features:
* - Load more button
* - Infinite scroll
* - Auto pagination
* - Category / tag / taxonomy filter support
* - Search support
* - Author / post type / order support
* - Skeleton loading
* - Spinner loading
* - Duplicate post prevention
* - Accessibility messages
* - Browser URL update
* - Back/forward state support
* - Retry on network error
* - Abort old request
* - REST API or admin-ajax.php support
* - Works with multiple load-more sections
*/
(function () {
'use strict';
/**
* Default settings.
* These can be overridden by window.rxAjaxLoadMore.
*/
var defaults = {
ajaxUrl: '',
restUrl: '',
nonce: '',
action: 'rx_load_more_posts',
containerSelector: '[data-rx-post-container]',
itemSelector: '[data-rx-post-item]',
buttonSelector: '[data-rx-load-more]',
statusSelector: '[data-rx-load-status]',
filterSelector: '[data-rx-filter]',
searchSelector: '[data-rx-search]',
formSelector: '[data-rx-filter-form]',
mode: 'button', // button, scroll, mixed
method: 'POST', // POST or GET
apiType: 'ajax', // ajax or rest
page: 1,
maxPages: 1,
postsPerPage: 10,
postType: 'post',
order: 'DESC',
orderby: 'date',
category: '',
tag: '',
taxonomy: '',
term: '',
author: '',
search: '',
exclude: [],
include: [],
updateUrl: true,
pushState: true,
preventDuplicates: true,
autoInit: true,
scrollOffset: 400,
debounceDelay: 350,
retryLimit: 2,
timeout: 20000,
loadingClass: 'rx-is-loading',
loadedClass: 'rx-is-loaded',
errorClass: 'rx-has-error',
hiddenClass: 'rx-hidden',
disabledClass: 'rx-is-disabled',
activeClass: 'rx-is-active',
loadingText: 'Loading...',
loadMoreText: 'Load More',
noMoreText: 'No more posts',
errorText: 'Something went wrong. Please try again.',
emptyText: 'No posts found.',
retryText: 'Retry',
enableSkeleton: true,
skeletonCount: 3,
skeletonClass: 'rx-post-skeleton',
enableDebug: false
};
var config = extend(defaults, window.rxAjaxLoadMore || {});
var instances = [];
/**
* Small helper: object merge.
*/
function extend(target) {
var output = {};
var i;
var key;
for (key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
output[key] = target[key];
}
}
for (i = 1; i < arguments.length; i++) {
if (!arguments[i]) {
continue;
}
for (key in arguments[i]) {
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
output[key] = arguments[i][key];
}
}
}
return output;
}
/**
* Debug logger.
*/
function debug() {
if (!config.enableDebug || !window.console) {
return;
}
console.log.apply(console, ['RX Ajax Load More:'].concat(Array.prototype.slice.call(arguments)));
}
/**
* Debounce helper.
*/
function debounce(fn, delay) {
var timer = null;
return function () {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
/**
* Convert NodeList to array.
*/
function toArray(list) {
return Array.prototype.slice.call(list || []);
}
/**
* Get closest parent.
*/
function closest(el, selector) {
if (!el) {
return null;
}
if (el.closest) {
return el.closest(selector);
}
while (el && el.nodeType === 1) {
if (matches(el, selector)) {
return el;
}
el = el.parentNode;
}
return null;
}
/**
* Matches helper.
*/
function matches(el, selector) {
var proto = Element.prototype;
var fn =
proto.matches ||
proto.webkitMatchesSelector ||
proto.mozMatchesSelector ||
proto.msMatchesSelector ||
proto.oMatchesSelector;
if (!fn) {
return false;
}
return fn.call(el, selector);
}
/**
* Serialize form data.
*/
function serializeForm(form) {
var data = {};
if (!form) {
return data;
}
var fields = form.querySelectorAll('input, select, textarea');
toArray(fields).forEach(function (field) {
if (!field.name || field.disabled) {
return;
}
if ((field.type === 'checkbox' || field.type === 'radio') && !field.checked) {
return;
}
if (field.type === 'checkbox') {
if (!data[field.name]) {
data[field.name] = [];
}
data[field.name].push(field.value);
return;
}
data[field.name] = field.value;
});
return data;
}
/**
* Build query string.
*/
function buildQuery(data) {
var parts = [];
Object.keys(data).forEach(function (key) {
var value = data[key];
if (value === undefined || value === null || value === '') {
return;
}
if (Array.isArray(value)) {
value.forEach(function (item) {
parts.push(encodeURIComponent(key + '[]') + '=' + encodeURIComponent(item));
});
} else {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
}
});
return parts.join('&');
}
/**
* Parse HTML safely.
*/
function parseHTML(html) {
var template = document.createElement('template');
template.innerHTML = html.trim();
return template.content;
}
/**
* Create live region.
*/
function createLiveRegion() {
var region = document.getElementById('rx-ajax-live-region');
if (region) {
return region;
}
region = document.createElement('div');
region.id = 'rx-ajax-live-region';
region.setAttribute('aria-live', 'polite');
region.setAttribute('aria-atomic', 'true');
region.style.position = 'absolute';
region.style.width = '1px';
region.style.height = '1px';
region.style.padding = '0';
region.style.margin = '-1px';
region.style.overflow = 'hidden';
region.style.clip = 'rect(0, 0, 0, 0)';
region.style.whiteSpace = 'nowrap';
region.style.border = '0';
document.body.appendChild(region);
return region;
}
/**
* RX Ajax Load More class.
*/
function RXAjaxLoadMore(root, options) {
this.root = root;
this.options = extend(config, options || {}, this.getDataOptions(root));
this.container = root.querySelector(this.options.containerSelector) || root;
this.button = root.querySelector(this.options.buttonSelector);
this.status = root.querySelector(this.options.statusSelector);
this.form = root.querySelector(this.options.formSelector);
this.page = parseInt(this.options.page, 10) || 1;
this.maxPages = parseInt(this.options.maxPages, 10) || 1;
this.loading = false;
this.finished = this.page >= this.maxPages;
this.retryCount = 0;
this.controller = null;
this.loadedIds = [];
this.liveRegion = createLiveRegion();
this.handleButtonClick = this.handleButtonClick.bind(this);
this.handleScroll = debounce(this.handleScroll.bind(this), 100);
this.handleFilterChange = debounce(this.handleFilterChange.bind(this), this.options.debounceDelay);
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handlePopState = this.handlePopState.bind(this);
this.init();
}
/**
* Read data attributes from wrapper.
*/
RXAjaxLoadMore.prototype.getDataOptions = function (root) {
var data = root.dataset || {};
var options = {};
if (data.rxMode) {
options.mode = data.rxMode;
}
if (data.rxPage) {
options.page = parseInt(data.rxPage, 10);
}
if (data.rxMaxPages) {
options.maxPages = parseInt(data.rxMaxPages, 10);
}
if (data.rxPostType) {
options.postType = data.rxPostType;
}
if (data.rxPostsPerPage) {
options.postsPerPage = parseInt(data.rxPostsPerPage, 10);
}
if (data.rxCategory) {
options.category = data.rxCategory;
}
if (data.rxTag) {
options.tag = data.rxTag;
}
if (data.rxTaxonomy) {
options.taxonomy = data.rxTaxonomy;
}
if (data.rxTerm) {
options.term = data.rxTerm;
}
if (data.rxAuthor) {
options.author = data.rxAuthor;
}
if (data.rxOrder) {
options.order = data.rxOrder;
}
if (data.rxOrderby) {
options.orderby = data.rxOrderby;
}
if (data.rxSearch) {
options.search = data.rxSearch;
}
return options;
};
/**
* Init.
*/
RXAjaxLoadMore.prototype.init = function () {
this.collectLoadedIds();
this.bindEvents();
this.updateUI();
debug('Instance initialized', this.root);
};
/**
* Bind events.
*/
RXAjaxLoadMore.prototype.bindEvents = function () {
var self = this;
if (this.button) {
this.button.addEventListener('click', this.handleButtonClick);
}
if (this.options.mode === 'scroll' || this.options.mode === 'mixed') {
window.addEventListener('scroll', this.handleScroll, { passive: true });
window.addEventListener('resize', this.handleScroll, { passive: true });
}
if (this.form) {
this.form.addEventListener('submit', this.handleFormSubmit);
toArray(this.form.querySelectorAll('input, select, textarea')).forEach(function (field) {
field.addEventListener('change', self.handleFilterChange);
if (field.type === 'search' || field.type === 'text') {
field.addEventListener('input', self.handleFilterChange);
}
});
}
toArray(this.root.querySelectorAll(this.options.filterSelector)).forEach(function (filter) {
filter.addEventListener('click', function (event) {
event.preventDefault();
self.handleFilterButton(filter);
});
});
if (this.options.pushState) {
window.addEventListener('popstate', this.handlePopState);
}
};
/**
* Destroy instance.
*/
RXAjaxLoadMore.prototype.destroy = function () {
if (this.button) {
this.button.removeEventListener('click', this.handleButtonClick);
}
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleScroll);
window.removeEventListener('popstate', this.handlePopState);
if (this.controller) {
this.controller.abort();
}
};
/**
* Button click.
*/
RXAjaxLoadMore.prototype.handleButtonClick = function (event) {
event.preventDefault();
if (this.finished) {
return;
}
this.loadNext();
};
/**
* Infinite scroll.
*/
RXAjaxLoadMore.prototype.handleScroll = function () {
if (this.loading || this.finished) {
return;
}
var rect = this.root.getBoundingClientRect();
var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
if (rect.bottom - viewportHeight <= this.options.scrollOffset) {
this.loadNext();
}
};
/**
* Form submit.
*/
RXAjaxLoadMore.prototype.handleFormSubmit = function (event) {
event.preventDefault();
this.resetAndLoad();
};
/**
* Filter change.
*/
RXAjaxLoadMore.prototype.handleFilterChange = function () {
this.resetAndLoad();
};
/**
* Filter button.
*/
RXAjaxLoadMore.prototype.handleFilterButton = function (filter) {
var group = filter.getAttribute('data-rx-filter-group') || 'default';
var value = filter.getAttribute('data-rx-filter-value') || '';
var key = filter.getAttribute('data-rx-filter-key') || 'category';
toArray(this.root.querySelectorAll('[data-rx-filter-group="' + group + '"]')).forEach(
function (el) {
el.classList.remove(config.activeClass);
el.setAttribute('aria-pressed', 'false');
}
);
filter.classList.add(this.options.activeClass);
filter.setAttribute('aria-pressed', 'true');
this.options[key] = value;
this.resetAndLoad();
};
/**
* Browser back/forward support.
*/
RXAjaxLoadMore.prototype.handlePopState = function (event) {
if (!event.state || !event.state.rxAjaxLoadMore) {
return;
}
this.page = event.state.page || 1;
this.options.search = event.state.search || '';
this.options.category = event.state.category || '';
this.options.tag = event.state.tag || '';
this.options.term = event.state.term || '';
this.resetAndLoad(false);
};
/**
* Load next page.
*/
RXAjaxLoadMore.prototype.loadNext = function () {
if (this.loading || this.finished) {
return;
}
this.page += 1;
this.load(false);
};
/**
* Reset then load.
*/
RXAjaxLoadMore.prototype.resetAndLoad = function (updateUrl) {
if (typeof updateUrl === 'undefined') {
updateUrl = true;
}
this.page = 1;
this.finished = false;
this.retryCount = 0;
this.loadedIds = [];
this.container.innerHTML = '';
this.load(true, updateUrl);
};
/**
* Main load method.
*/
RXAjaxLoadMore.prototype.load = function (replace, updateUrl) {
var self = this;
if (this.loading) {
return;
}
if (typeof updateUrl === 'undefined') {
updateUrl = true;
}
this.loading = true;
this.root.classList.add(this.options.loadingClass);
this.root.classList.remove(this.options.errorClass);
this.setStatus(this.options.loadingText);
this.setButtonLoading(true);
this.addSkeletons();
if (this.controller) {
this.controller.abort();
}
this.controller = new AbortController();
var payload = this.getPayload();
this.request(payload)
.then(function (response) {
self.removeSkeletons();
self.handleResponse(response, replace);
if (updateUrl && self.options.updateUrl) {
self.updateBrowserUrl();
}
self.retryCount = 0;
})
.catch(function (error) {
self.removeSkeletons();
if (error && error.name === 'AbortError') {
return;
}
self.handleError(error, replace);
})
.finally(function () {
self.loading = false;
self.root.classList.remove(self.options.loadingClass);
self.setButtonLoading(false);
self.updateUI();
});
};
/**
* Build payload.
*/
RXAjaxLoadMore.prototype.getPayload = function () {
var formData = serializeForm(this.form);
var payload = extend(
{
action: this.options.action,
nonce: this.options.nonce,
page: this.page,
posts_per_page: this.options.postsPerPage,
post_type: this.options.postType,
order: this.options.order,
orderby: this.options.orderby,
category: this.options.category,
tag: this.options.tag,
taxonomy: this.options.taxonomy,
term: this.options.term,
author: this.options.author,
search: this.options.search,
exclude: this.options.exclude,
include: this.options.include,
loaded_ids: this.loadedIds
},
formData
);
var searchField = this.root.querySelector(this.options.searchSelector);
if (searchField && searchField.value) {
payload.search = searchField.value;
}
return payload;
};
/**
* AJAX request.
*/
RXAjaxLoadMore.prototype.request = function (payload) {
var self = this;
var url = this.options.apiType === 'rest' ? this.options.restUrl : this.options.ajaxUrl;
if (!url) {
return Promise.reject(new Error('AJAX URL is missing.'));
}
var fetchOptions = {
method: this.options.method,
credentials: 'same-origin',
signal: this.controller.signal,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
};
if (this.options.apiType === 'rest') {
fetchOptions.headers['X-WP-Nonce'] = this.options.nonce || '';
}
if (this.options.method.toUpperCase() === 'GET') {
url += (url.indexOf('?') === -1 ? '?' : '&') + buildQuery(payload);
} else {
fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
fetchOptions.body = buildQuery(payload);
}
return this.fetchWithTimeout(url, fetchOptions, this.options.timeout).then(function (res) {
if (!res.ok) {
throw new Error('Request failed with status ' + res.status);
}
return res.json().then(function (json) {
return self.normalizeResponse(json);
});
});
};
/**
* Fetch timeout.
*/
RXAjaxLoadMore.prototype.fetchWithTimeout = function (url, options, timeout) {
return new Promise(function (resolve, reject) {
var timer = setTimeout(function () {
reject(new Error('Request timeout.'));
}, timeout);
fetch(url, options)
.then(function (response) {
clearTimeout(timer);
resolve(response);
})
.catch(function (error) {
clearTimeout(timer);
reject(error);
});
});
};
/**
* Normalize WordPress admin-ajax or REST response.
*/
RXAjaxLoadMore.prototype.normalizeResponse = function (json) {
if (json && json.success && json.data) {
return json.data;
}
return json || {};
};
/**
* Handle response.
*/
RXAjaxLoadMore.prototype.handleResponse = function (response, replace) {
var html = response.html || response.posts || '';
var count = parseInt(response.count, 10) || 0;
var maxPages = parseInt(response.max_pages, 10) || parseInt(response.maxPages, 10) || this.maxPages;
var currentPage = parseInt(response.page, 10) || this.page;
this.maxPages = maxPages;
this.page = currentPage;
if (response.found_posts !== undefined) {
this.root.setAttribute('data-rx-found-posts', response.found_posts);
}
if (response.query_id !== undefined) {
this.root.setAttribute('data-rx-query-id', response.query_id);
}
if (!html || count === 0) {
if (replace) {
this.container.innerHTML = this.getEmptyMarkup();
}
this.finished = true;
this.setStatus(this.options.emptyText);
this.announce(this.options.emptyText);
return;
}
this.insertPosts(html, replace);
this.collectLoadedIds();
this.finished = this.page >= this.maxPages || response.has_more === false;
if (this.finished) {
this.setStatus(this.options.noMoreText);
this.announce(this.options.noMoreText);
} else {
this.setStatus(count + ' posts loaded.');
this.announce(count + ' posts loaded.');
}
this.dispatchEvent('rx:ajax-loaded', {
response: response,
page: this.page,
maxPages: this.maxPages,
finished: this.finished
});
};
/**
* Insert posts.
*/
RXAjaxLoadMore.prototype.insertPosts = function (html, replace) {
var fragment = parseHTML(html);
var nodes = toArray(fragment.children);
if (this.options.preventDuplicates) {
nodes = this.filterDuplicates(nodes);
}
if (replace) {
this.container.innerHTML = '';
}
nodes.forEach(function (node) {
this.container.appendChild(node);
}, this);
this.animateNewItems(nodes);
};
/**
* Prevent duplicate posts.
*/
RXAjaxLoadMore.prototype.filterDuplicates = function (nodes) {
var self = this;
return nodes.filter(function (node) {
var id =
node.getAttribute('data-post-id') ||
node.getAttribute('data-id') ||
node.id ||
'';
if (!id) {
return true;
}
if (self.loadedIds.indexOf(id) !== -1) {
return false;
}
self.loadedIds.push(id);
return true;
});
};
/**
* Collect currently loaded post IDs.
*/
RXAjaxLoadMore.prototype.collectLoadedIds = function () {
var self = this;
var items = this.container.querySelectorAll(this.options.itemSelector + ', [data-post-id]');
toArray(items).forEach(function (item) {
var id =
item.getAttribute('data-post-id') ||
item.getAttribute('data-id') ||
item.id ||
'';
if (id && self.loadedIds.indexOf(id) === -1) {
self.loadedIds.push(id);
}
});
};
/**
* New item animation.
*/
RXAjaxLoadMore.prototype.animateNewItems = function (nodes) {
nodes.forEach(function (node) {
node.classList.add('rx-ajax-new-item');
requestAnimationFrame(function () {
node.classList.add('rx-ajax-visible');
});
setTimeout(function () {
node.classList.remove('rx-ajax-new-item');
node.classList.remove('rx-ajax-visible');
}, 600);
});
};
/**
* Error handler.
*/
RXAjaxLoadMore.prototype.handleError = function (error, replace) {
debug('Error:', error);
this.root.classList.add(this.options.errorClass);
if (this.retryCount < this.options.retryLimit) {
this.retryCount += 1;
this.page = Math.max(1, this.page - 1);
this.setStatus(this.options.errorText + ' Retrying...');
this.announce(this.options.errorText + ' Retrying.');
this.load(replace);
return;
}
this.page = Math.max(1, this.page - 1);
this.setStatus(this.options.errorText);
this.announce(this.options.errorText);
if (this.button) {
this.button.textContent = this.options.retryText;
this.button.removeAttribute('disabled');
this.button.classList.remove(this.options.disabledClass);
}
this.dispatchEvent('rx:ajax-error', {
error: error
});
};
/**
* Update UI.
*/
RXAjaxLoadMore.prototype.updateUI = function () {
if (!this.button) {
return;
}
if (this.finished) {
this.button.textContent = this.options.noMoreText;
this.button.setAttribute('disabled', 'disabled');
this.button.classList.add(this.options.disabledClass);
this.button.setAttribute('aria-disabled', 'true');
if (this.options.mode === 'scroll') {
this.button.classList.add(this.options.hiddenClass);
}
return;
}
this.button.textContent = this.options.loadMoreText;
this.button.removeAttribute('disabled');
this.button.classList.remove(this.options.disabledClass);
this.button.setAttribute('aria-disabled', 'false');
};
/**
* Set button loading state.
*/
RXAjaxLoadMore.prototype.setButtonLoading = function (state) {
if (!this.button) {
return;
}
if (state) {
this.button.textContent = this.options.loadingText;
this.button.setAttribute('disabled', 'disabled');
this.button.classList.add(this.options.disabledClass);
this.button.setAttribute('aria-busy', 'true');
} else {
this.button.removeAttribute('aria-busy');
}
};
/**
* Set status text.
*/
RXAjaxLoadMore.prototype.setStatus = function (message) {
if (!this.status) {
return;
}
this.status.textContent = message || '';
};
/**
* Accessibility announce.
*/
RXAjaxLoadMore.prototype.announce = function (message) {
if (!this.liveRegion) {
return;
}
this.liveRegion.textContent = '';
var region = this.liveRegion;
setTimeout(function () {
region.textContent = message;
}, 50);
};
/**
* Skeleton loading.
*/
RXAjaxLoadMore.prototype.addSkeletons = function () {
if (!this.options.enableSkeleton) {
return;
}
this.removeSkeletons();
var wrapper = document.createElement('div');
wrapper.className = 'rx-skeleton-wrapper';
wrapper.setAttribute('data-rx-skeleton-wrapper', 'true');
for (var i = 0; i < this.options.skeletonCount; i++) {
var skeleton = document.createElement('div');
skeleton.className = this.options.skeletonClass;
skeleton.innerHTML =
'<div class="rx-skeleton-thumb"></div>' +
'<div class="rx-skeleton-content">' +
'<div class="rx-skeleton-line rx-skeleton-line-lg"></div>' +
'<div class="rx-skeleton-line"></div>' +
'<div class="rx-skeleton-line rx-skeleton-line-sm"></div>' +
'</div>';
wrapper.appendChild(skeleton);
}
this.container.appendChild(wrapper);
};
/**
* Remove skeletons.
*/
RXAjaxLoadMore.prototype.removeSkeletons = function () {
var skeletons = this.container.querySelectorAll('[data-rx-skeleton-wrapper]');
toArray(skeletons).forEach(function (skeleton) {
skeleton.parentNode.removeChild(skeleton);
});
};
/**
* Empty markup.
*/
RXAjaxLoadMore.prototype.getEmptyMarkup = function () {
return (
'<div class="rx-ajax-empty" role="status">' +
'<p>' +
this.escapeHTML(this.options.emptyText) +
'</p>' +
'</div>'
);
};
/**
* Escape HTML.
*/
RXAjaxLoadMore.prototype.escapeHTML = function (str) {
return String(str).replace(/[&<>"']/g, function (match) {
var map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return map[match];
});
};
/**
* Update browser URL.
*/
RXAjaxLoadMore.prototype.updateBrowserUrl = function () {
if (!window.history || !window.history.pushState) {
return;
}
var params = new URLSearchParams(window.location.search);
if (this.page > 1) {
params.set('rxpage', this.page);
} else {
params.delete('rxpage');
}
var payload = this.getPayload();
if (payload.search) {
params.set('s', payload.search);
} else {
params.delete('s');
}
if (payload.category) {
params.set('category', payload.category);
} else {
params.delete('category');
}
if (payload.tag) {
params.set('tag', payload.tag);
} else {
params.delete('tag');
}
if (payload.term) {
params.set('term', payload.term);
} else {
params.delete('term');
}
var newUrl =
window.location.pathname +
(params.toString() ? '?' + params.toString() : '') +
window.location.hash;
var state = {
rxAjaxLoadMore: true,
page: this.page,
search: payload.search || '',
category: payload.category || '',
tag: payload.tag || '',
term: payload.term || ''
};
window.history.pushState(state, document.title, newUrl);
};
/**
* Dispatch custom event.
*/
RXAjaxLoadMore.prototype.dispatchEvent = function (name, detail) {
var event;
if (typeof CustomEvent === 'function') {
event = new CustomEvent(name, {
detail: detail,
bubbles: true
});
} else {
event = document.createEvent('CustomEvent');
event.initCustomEvent(name, true, true, detail);
}
this.root.dispatchEvent(event);
};
/**
* Public reload method.
*/
RXAjaxLoadMore.prototype.reload = function () {
this.resetAndLoad();
};
/**
* Public load more method.
*/
RXAjaxLoadMore.prototype.loadMore = function () {
this.loadNext();
};
/**
* Public set option method.
*/
RXAjaxLoadMore.prototype.setOption = function (key, value) {
this.options[key] = value;
};
/**
* Public get option method.
*/
RXAjaxLoadMore.prototype.getOption = function (key) {
return this.options[key];
};
/**
* Init all wrappers.
*/
function initRXAjaxLoadMore() {
var roots = document.querySelectorAll('[data-rx-ajax-load-more]');
toArray(roots).forEach(function (root) {
if (root.rxAjaxLoadMoreInstance) {
return;
}
var instance = new RXAjaxLoadMore(root);
root.rxAjaxLoadMoreInstance = instance;
instances.push(instance);
});
}
/**
* Public API.
*/
window.RXAjaxLoadMore = {
init: initRXAjaxLoadMore,
instances: instances,
create: function (root, options) {
var instance = new RXAjaxLoadMore(root, options);
root.rxAjaxLoadMoreInstance = instance;
instances.push(instance);
return instance;
},
reloadAll: function () {
instances.forEach(function (instance) {
instance.reload();
});
},
destroyAll: function () {
instances.forEach(function (instance) {
instance.destroy();
});
instances.length = 0;
}
};
/**
* Auto init.
*/
if (config.autoInit) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initRXAjaxLoadMore);
} else {
initRXAjaxLoadMore();
}
}
})();
Also add this small CSS to your theme CSS for loading effect:
.rx-is-loading {
opacity: 0.88;
}
.rx-is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
.rx-hidden {
display: none !important;
}
.rx-ajax-new-item {
opacity: 0;
transform: translateY(12px);
transition: opacity 0.35s ease, transform 0.35s ease;
}
.rx-ajax-new-item.rx-ajax-visible {
opacity: 1;
transform: translateY(0);
}
.rx-skeleton-wrapper {
display: grid;
gap: 20px;
margin-top: 20px;
}
.rx-post-skeleton {
display: grid;
grid-template-columns: 120px 1fr;
gap: 16px;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.rx-skeleton-thumb,
.rx-skeleton-line {
background: linear-gradient(90deg, #eeeeee 25%, #f7f7f7 50%, #eeeeee 75%);
background-size: 200% 100%;
animation: rxSkeletonMove 1.3s infinite;
border-radius: 8px;
}
.rx-skeleton-thumb {
width: 120px;
height: 90px;
}
.rx-skeleton-content {
display: grid;
gap: 10px;
align-content: center;
}
.rx-skeleton-line {
height: 14px;
}
.rx-skeleton-line-lg {
width: 80%;
}
.rx-skeleton-line-sm {
width: 45%;
}
@keyframes rxSkeletonMove {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.rx-ajax-empty {
padding: 24px;
text-align: center;
border: 1px dashed #d1d5db;
border-radius: 12px;
}
Use this HTML structure in your theme template:
<div
class="rx-blog-posts-area"
data-rx-ajax-load-more
data-rx-mode="mixed"
data-rx-page="1"
data-rx-max-pages="<?php echo esc_attr( $wp_query->max_num_pages ); ?>"
data-rx-post-type="post"
data-rx-posts-per-page="<?php echo esc_attr( get_option( 'posts_per_page' ) ); ?>"
>
<div class="rx-post-grid" data-rx-post-container>
<?php
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
?>
<article id="post-<?php the_ID(); ?>" data-rx-post-item data-post-id="<?php the_ID(); ?>">
<h2>
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h2>
<?php the_excerpt(); ?>
</article>
<?php
endwhile;
endif;
?>
</div>
<p data-rx-load-status aria-live="polite"></p>
<button type="button" class="rx-load-more-button" data-rx-load-more>
Load More
</button>
</div>
And enqueue/localize it in functions.php or your enqueue file:
function rx_theme_enqueue_ajax_load_more() {
wp_enqueue_script(
'rx-ajax-load-more',
get_template_directory_uri() . '/inc/js/ajax-load-more.js',
array(),
'1.0.0',
true
);
wp_localize_script(
'rx-ajax-load-more',
'rxAjaxLoadMore',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'restUrl' => esc_url_raw( rest_url( 'rx-theme/v1/load-more' ) ),
'nonce' => wp_create_nonce( 'rx_ajax_load_more_nonce' ),
'action' => 'rx_load_more_posts',
'apiType' => 'ajax',
'mode' => 'mixed',
'postsPerPage' => get_option( 'posts_per_page' ),
'loadingText' => esc_html__( 'Loading...', 'rx-theme' ),
'loadMoreText' => esc_html__( 'Load More', 'rx-theme' ),
'noMoreText' => esc_html__( 'No more posts', 'rx-theme' ),
'errorText' => esc_html__( 'Something went wrong. Please try again.', 'rx-theme' ),
'emptyText' => esc_html__( 'No posts found.', 'rx-theme' ),
'enableDebug' => false,
)
);
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_ajax_load_more' );
You will also need the PHP AJAX callback. Add this in functions.php or better in:
inc/ajax/ajax-load-more.php
<?php
/**
* RX Theme Ajax Load More Callback
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function rx_theme_ajax_load_more_posts() {
check_ajax_referer( 'rx_ajax_load_more_nonce', 'nonce' );
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
$posts_per_page = isset( $_POST['posts_per_page'] ) ? absint( $_POST['posts_per_page'] ) : get_option( 'posts_per_page' );
$post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : 'post';
$order = isset( $_POST['order'] ) ? sanitize_key( wp_unslash( $_POST['order'] ) ) : 'DESC';
$orderby = isset( $_POST['orderby'] ) ? sanitize_key( wp_unslash( $_POST['orderby'] ) ) : 'date';
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
$category = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : '';
$tag = isset( $_POST['tag'] ) ? sanitize_text_field( wp_unslash( $_POST['tag'] ) ) : '';
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
$term = isset( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
$author = isset( $_POST['author'] ) ? absint( $_POST['author'] ) : 0;
$loaded_ids = array();
if ( isset( $_POST['loaded_ids'] ) && is_array( $_POST['loaded_ids'] ) ) {
$loaded_ids = array_map( 'absint', wp_unslash( $_POST['loaded_ids'] ) );
$loaded_ids = array_filter( $loaded_ids );
}
$args = array(
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => $posts_per_page,
'paged' => $page,
'order' => $order,
'orderby' => $orderby,
'ignore_sticky_posts' => true,
);
if ( ! empty( $search ) ) {
$args['s'] = $search;
}
if ( ! empty( $category ) ) {
if ( is_numeric( $category ) ) {
$args['cat'] = absint( $category );
} else {
$args['category_name'] = sanitize_title( $category );
}
}
if ( ! empty( $tag ) ) {
$args['tag'] = sanitize_title( $tag );
}
if ( ! empty( $author ) ) {
$args['author'] = $author;
}
if ( ! empty( $loaded_ids ) ) {
$args['post__not_in'] = $loaded_ids;
}
if ( ! empty( $taxonomy ) && ! empty( $term ) ) {
$args['tax_query'] = array(
array(
'taxonomy' => $taxonomy,
'field' => is_numeric( $term ) ? 'term_id' : 'slug',
'terms' => is_numeric( $term ) ? absint( $term ) : sanitize_title( $term ),
),
);
}
$query = new WP_Query( $args );
ob_start();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( 'rx-post-card' ); ?> data-rx-post-item data-post-id="<?php the_ID(); ?>">
<?php if ( has_post_thumbnail() ) : ?>
<a class="rx-post-card__thumbnail" href="<?php the_permalink(); ?>">
<?php the_post_thumbnail( 'medium_large' ); ?>
</a>
<?php endif; ?>
<div class="rx-post-card__content">
<h2 class="rx-post-card__title">
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h2>
<div class="rx-post-card__meta">
<span><?php echo esc_html( get_the_date() ); ?></span>
<span><?php echo esc_html( get_the_author() ); ?></span>
</div>
<div class="rx-post-card__excerpt">
<?php the_excerpt(); ?>
</div>
<a class="rx-post-card__read-more" href="<?php the_permalink(); ?>">
<?php esc_html_e( 'Read More', 'rx-theme' ); ?>
</a>
</div>
</article>
<?php
}
}
$html = ob_get_clean();
wp_reset_postdata();
wp_send_json_success(
array(
'html' => $html,
'count' => $query->post_count,
'found_posts' => $query->found_posts,
'page' => $page,
'max_pages' => $query->max_num_pages,
'has_more' => $page < $query->max_num_pages,
)
);
}
add_action( 'wp_ajax_rx_load_more_posts', 'rx_theme_ajax_load_more_posts' );
add_action( 'wp_ajax_nopriv_rx_load_more_posts', 'rx_theme_ajax_load_more_posts' );
Then include this PHP file from functions.php:
require_once get_template_directory() . '/inc/ajax/ajax-load-more.php';
This gives your RX Theme a powerful modern Ajax load-more system. Button mode, scroll mode, and mixed mode will all work.