CODE

Lazy Load Listings

Lazy loading for long lists of items automatically as the user scrolls down. Hands-free pagination while improving initial page load performance.

v1.0.0
  1. We'll start by setting up the page on your site that displays the listing. This can be any page you like.

    NOTE:
    Adjust the module component to replace "My Listings" with the name of your module, and set the limit to the number of items you wish to load at a time.

    {% comment %}<!-- Treehouse CODE v1.0.0 -->{% endcomment %}
    <section class="section">
      <h1>My Listings</h1>
    
      {% component type: "module", source: "My Listings", layout: "Lazy List", limit: "6", lazyInt: "true", object: "collection" %}
    
      <div id="lazyItemsSentinel" aria-hidden="true"></div>
      <div id="lazyItemsLoader" aria-live="polite" aria-busy="false"></div>
    </section>
  2. Next, we need to create a page on your site where we'll configure our Liquid code to look up the paged items. This page will be accessed, in the background, by the AJAX in the next step.

    For this example, we've set up a page folder called `_ajax` (this is useful for if we later add additional AJAX page functionality).

    Now, in the `_ajax` page folder, add a page called `lazy-list` and add the below code (using the code view editor).
    The page should then be accessible at `/_ajax/lazy-list`.

    IMPORTANT:
    Set this page to use a blank template. We don't want to load unecessary files here or any other content.

    TIP:
    We suggest you disable this page from Site Search and from Search Engines to avoid it being publically accessed.

    The Liquid code below returns the next set of items to be loaded in, and it uses the same list Layout as the initial call, so the layout is consistant and only needs to be updated in one place.

    {% comment %}<!-- Treehouse CODE v1.0.0 -->{% endcomment %}
    {% comment %}Server-side pagination via ?page=N (1-based).{% endcomment %}
    {% if request.request_url.params.page == "" %}
      {% assign _p = 1 %}
    {% else %}
      {% assign _p = request.request_url.params.page | plus: 0 %}
    {% endif %}
    {% if _p < 1 %}{% assign _p = 1 %}{% endif %}
    {% assign _per = 3 %}
    {% assign _skip = _p | minus: 1 | times: _per %}
    
    {% component type: "module", source: "My Listings", layout: "Lazy List", lazyPer: "{{_per}}", lazySkip: "{{_skip}}", object: "collection", limit: 1000 %}
    
  3. Next, create a new list Layout for your module called "Lazy List", and add the following code.

    {% comment %}<!-- Treehouse CODE v1.0.0 -->{% endcomment %}
    {% if this.params.lazyInt == true %}
    <div class="lazy-list-grid" id="lazyListGrid"
      data-total="{{ this.Pagination.TotalItemsCount }}"
      data-ajax-url="/_ajax/lazy-list">
    {% endif %}
    
    {% for item in this.Items limit: this.params.lazyPer offset: this.params.lazySkip %}
    <div class="lazy-list-card">
      <h3><a href="{{ item.Url }}">{{ item.Name }}</a></h3>
      <!-- Your list layout -->
    </div>
    {% endfor %}
    
    {% if this.params.lazyInt == true %}
    </div>
    {% endif %}
  4. Add some styles to your existing CSS file for the loading message (adjust as required).

    /* Treehouse CODE v1.0.0 */
    /* ── Lazy-load loader & sentinel ────────────────────────────────── */
    #lazyItemsLoader {
      min-height: 2rem;
      text-align: center;
      padding: .75rem 0;
      color: #718096;
      font-size: .9rem;
    }
    #lazyItemsSentinel {
      height: 1px;
    }
  5. Lastely, add the following Javascript to your site's JS file.

    //## Treehouse CODE v1.0.0 ##
    (function () {
      'use strict';
    
      var grid = document.getElementById('lazyListGrid');
      if (!grid) return;
    
      var loader    = document.getElementById('lazyItemsLoader');
      var sentinel  = document.getElementById('lazyItemsSentinel');
      var totalItems  = parseInt(grid.getAttribute('data-total'), 10) || 0;
      var ajaxUrl = grid.getAttribute('data-ajax-url') || '/_ajax/lazy-list';
    
      var currentPage  = 1;   // page 1 already rendered server-side
      var loading      = false;
      var allLoaded    = false;
      var activeFilter = 'all';
    
      // ── Helpers ──────────────────────────────────────────────────────
    
      function countLoadedCards() {
        return grid.querySelectorAll('.lazy-list-card').length;
      }
    
      function setLoading(state) {
        loading = state;
        if (!loader) return;
        loader.setAttribute('aria-busy', state ? 'true' : 'false');
        loader.textContent = state ? 'Loading more items…' : '';
      }
    
      // Returns true when the sentinel element is within the visible viewport,
      // meaning the list does not yet extend past the bottom of the screen.
      function isSentinelVisible() {
        if (!sentinel) return false;
        return sentinel.getBoundingClientRect().top < window.innerHeight;
      }
    
      // ── Fetch next page ───────────────────────────────────────────────
    
      function fetchNextPage() {
        if (loading || allLoaded) return;
    
        currentPage++;
        setLoading(true);
    
        fetch(ajaxUrl + '?page=' + currentPage)
          .then(function (res) {
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return res.text();
          })
          .then(function (html) {
            var temp = document.createElement('div');
            temp.innerHTML = html;
            var newCards = Array.prototype.slice.call(temp.querySelectorAll('.lazy-list-card'));
    
            if (newCards.length === 0) {
              // Server returned no cards — all items consumed.
              allLoaded = true;
            } else {
              newCards.forEach(function (card) { grid.appendChild(card); });
              applyFilterToCards(newCards);
              // Compare DOM card count with the known total to detect the last page.
              allLoaded = countLoadedCards() >= totalItems;
            }
    
            setLoading(false);
    
            // If the sentinel is still visible after inserting cards, the new batch
            // did not push it past the fold — keep loading until it does or until
            // all items are exhausted.
            if (!allLoaded) checkAndLoad();
          })
          .catch(function () {
            // On network / server error: wind back the page counter so the next
            // scroll event can retry this page rather than silently skipping it.
            currentPage--;
            setLoading(false);
          });
      }
    
      // ── Viewport / scroll check ───────────────────────────────────────
    
      function checkAndLoad() {
        if (!allLoaded && !loading && isSentinelVisible()) {
          fetchNextPage();
        }
      }
    
      // Debounced scroll listener (passive for scroll-performance).
      var scrollTimer;
      window.addEventListener('scroll', function () {
        clearTimeout(scrollTimer);
        scrollTimer = setTimeout(checkAndLoad, 100);
      }, { passive: true });
    
      // ── Initial viewport fill check ───────────────────────────────────
      // After the browser paints the layout, check whether the sentinel is
      // already visible (i.e. 3 items do not fill the viewport height).
      // If so, begin loading the next page automatically — the user would
      // have no scrollable area to trigger the scroll listener otherwise.
      if (totalItems > countLoadedCards()) {
        requestAnimationFrame(function () {
          setTimeout(checkAndLoad, 50);
        });
      }
    
    }());

Comments or questions? Head over to the WebinOne forum to discuss with the community.