Infinite-Scroll

A vanilla JS implementation

Introduction

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

Character JSON

Pieces

We will be implementing the following:

Demo

Go to the demo

HTML

Contains an empty div to hold the items, a loading spinner, and the script that powers things.

Item Container

<div id="scroll-content"></div>

Loading Spinner

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.

Item Template

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:

template result

Here is the accompanying CSS:

<style>
.scroll-item { background:#f4f4f4; border-radius:9px; 
  padding:10px; margin:5px 0 5px; 
  overflow:auto;
}
</style>

Script things

<script is:inline src="/lib/infiscroll-marvel.js"></script>
<script is:inline type="text/javascript">
  infiscroll.init(1);
  infiscroll.getPage(); // get first page
</script>

infiscroll-marvel.js

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;
  }
};

Conclusion

Vanilla so no React or Vue client-side libs.

What next

Make it XElement



Back to Post List
ok!