Implementing vanilla JS infinite scroll.
Ideally you would use an API that knows pagination
, like supporting &page
and &itemsPerPage
parameters, sometimes also encountered as &size
, or &limit
and &offset
.
For this example, I will be using an API from Marvel. Rate-limit: 3000 calls/day.
What the data looks like
We will be implementing the following:
Contains an empty div to hold the items, a loading spinner, and the script that powers things.
<div id="scroll-content"></div>
Something like
<div id="loading-spinner" class="alert alert-info mt-4 mb-4" style="display:none;text-align:center;">
<i class="fas fa-spinner fa-4x fa-spin"></i><br/>
Loading results...
</div>
Important to display:none;
so it starts invisible.
This defines how each item will be rendered as. It contains placeholders that will be replaced by an item’s properites.
i.e. #name#
will be replaced by character.name
and #thumbnail.path#
will be replaced by character.thumbnail.path
Other placeholders in this template are: #thumbnail.extension#
and #description#
<div id="item-template" style="display:none;">
<div class="scroll-item">
<img class="float-start me-3" src="#thumbnail.path#.#thumbnail.extension#" height="180" style="border-radius:12px;" />
<b>#name#</b><br/>
#description#
</div>
</div>
Result:
Here is the accompanying CSS:
<style>
.scroll-item { background:#f4f4f4; border-radius:9px;
padding:10px; margin:5px 0 5px;
overflow:auto;
}
</style>
<script src="/lib/infiscroll-marvel.js"></script>
<script type="text/javascript">
infiscroll.init(1);
infiscroll.getPage(); // get first page
</script>
let infiscroll = {
/* STATE VARIABLES */
currentPage: 1,
noMore: false,
container: '',
spinner: '',
/* INITIALIZATION */
init: function (startingPage, container, spinner) {
this.currentPage = startingPage || 1;
this.container = container || 'scroll-content';
this.spinner = spinner || 'loading-spinner';
/* ATTACH TO SCROLL-TO-END EVENT */
window.addEventListener('scroll', function () {
var scrollTop = document.documentElement.scrollTop;
var scrollHeight = document.documentElement.scrollHeight;
var clientHeight = document.documentElement.clientHeight;
if (scrollTop + clientHeight >= scrollHeight - 5 && !infiscroll.noMore) {
infiscroll.getPage();
}
}, { passive:true });
},
/* GET THE NEXT PAGE */
getPage: function () {
/*
assumes:
div#item-template
*/
if (this.noMore) return;
var self = this;
document.getElementById(self.spinner).style.display = '';
var offset = (self.currentPage-1) * 10;
setTimeout(function () {
axios.get('https://gateway.marvel.com:443/v1/public/characters?apikey=5c30b609f2fa2d5a7ea9a0d66892983a&limit=10&offset='+offset)
.then(function (response) {
self.currentPage += 1;
var characters = response.data.data.results;
var itemTpl = document.getElementById('item-template').innerHTML;
var markup = '';
var nwcontent = document.createElement('div');
for (var i = 0; i < characters.length; i++) {
markup += self.templatize(characters[i], itemTpl);
console.log(characters[i]);
}
if (characters.length === 0) {
markup = '<p>No More Results.</p>';
self.noMore = true;
}
nwcontent.innerHTML = markup;
document.getElementById(self.container).appendChild(nwcontent);
document.getElementById(self.spinner).style.display = 'none';
});
}, 3200);
},
/* HELPER */
startsWith: function (findThis, here) {
return here.substr(0, findThis.length) === findThis;
},
/* HELPER -
Templatize an object into plahecolder tokens */
templatize: function (obj, tpl, pathPrefix) {
var t = tpl;
pathPrefix = pathPrefix || '';
if (this.startsWith('.', pathPrefix)) {
pathPrefix = pathPrefix.substring(1);
}
for (var p in obj) {
var val = obj[p];
if (val !== null && typeof val === 'object') {
t = this.templatize(val, t, pathPrefix + '.' + p);
} else {
var re = new RegExp('#' + (pathPrefix.length === 0 ? '' : pathPrefix + '.') + p + '#', 'gi');
t = t.replace(re, val);
}
}
return t;
}
};
Vanilla so no React or Vue client-side libs.
Make it XElement