Infinite Scroll Feature

In this tutorial, we’ll walk through the implementation of an Infinite Scroll feature using JavaScript. 

Posted by Kuligaposten on August 25, 2023

In this tutorial, we’ll walk through the implementation of an Infinite Scroll feature using JavaScript. We’ll create a reusable class, InfiniteScroll, which allows you to load and display content dynamically as the user scrolls down the page. This class will be flexible enough to handle different data sources and customizable templates.
Prerequisites
Basic knowledge of JavaScript.

Getting Started

First, let’s set up the HTML structure for our content container.

<div class="card-container"></div>

We will need some CSS

.card-container {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  gap: 1em 1em;
}

@media (min-width: 768px) {
  .card-container {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 992px) {
  .card-container {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 1200px) {
  .card-container {
    grid-template-columns: repeat(4, 1fr);
  }
}

.card-observed {
  color: var(--bs-tertiary-color);
  border-radius: 0.5rem;
  display: flex;
  flex-direction: column;
  position: relative;
  min-width: 0;
  opacity: 0;
  scale: 0.5;
  transition-property: opacity, scale;
  transition: 120ms ease-in-out;
}

.card-observed.show {
  opacity: 1;
  scale: 1;
}

.card-observed img {
  border-radius: 0.5rem;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  height: 200px;
  width: 100%;
  object-fit: cover;
  object-position: center;
  cursor: pointer;
}

Now to the fun part, our javascript
First our InfiniteScroll class

class InfiniteScroll {
  constructor(template, dataMappings, jsonData, classes) {
    this.perPage = 1;
    this.morePosts = true;
    this.startCount = 0;
    this.classes = classes || ['card-observed', 'card', 'bg-body-tertiary', 'shadow', 'text-body-emphasis'];
    this.cards = document.querySelectorAll('.card-observed');
    this.cardContainer = document.querySelector('.card-container');
    this.template = template;
    this.dataMappings = dataMappings;
    this.post = jsonData;
    this.endPost = jsonData.length;
    this.observer = new IntersectionObserver(this.handleIntersection.bind(this), { threshold: 0 });
    this.getPropertyValue = (object, key) => {
      const keys = key.split('.');
      let value = object;
      for (const k of keys) {
        if (value && k in value) {
          value = value[k];
        } else {
          return 'Not found';
        }
      }
      return value;
    };
  }

  init(jsonData) {
    this.post = jsonData;
    this.loadStartCards();
  }
  handleIntersection(entries) {
    entries.forEach((entry, index) => {
      if (entry.isIntersecting) {
        entry.target.style.transition = '120ms ease-in-out';
        entry.target.classList.add('show');
        entry.target.addEventListener('transitionend', () => entry.target.removeAttribute('style'));
      } else {
        entry.target.classList.remove('show');
      }
    });
  }
  createElement = (type, options = {}) => {
    const element = document.createElement(type);
    Object.entries(options).forEach(([key, value]) => {
      if (key === 'class') {
        element.classList.add(...value);
        return;
      }

      if (key === 'dataset') {
        Object.entries(value).forEach(([dataKey, dataValue]) => {
          element.dataset[dataKey] = dataValue;
        });
        return;
      }

      if (key === 'text') {
        element.textContent = value;
        return;
      }

      element.setAttribute(key, value);
    });
    return element;
  };

  startObserve = () => {
    const lastCardObserver = new IntersectionObserver((entries) => {
      const lastCard = entries[0];
      if (!lastCard.isIntersecting) return;
      this.loadNewCards();
      lastCardObserver.unobserve(lastCard.target);
      if (this.morePosts) {
        lastCardObserver.observe(document.querySelector('.card-observed:last-child'));
      }
    }, {});

    lastCardObserver.observe(document.querySelector('.card-observed:last-child'));

    this.cards.forEach((card) => this.observer.observe(card));
  };

  loadNewCards = () => {
    let thisPage = this.startCount + this.perPage;
    if (thisPage > this.post.length) thisPage = this.post.length;
    let delay = 100;
    if (this.morePosts) {
      for (let i = this.startCount; i < thisPage; i++) {
        const card = this.createElement('div', { class: this.classes });
        let cardHtml = this.template;
        Object.entries(this.dataMappings).forEach(([placeholder, dataKey]) => {
          const placeholderRegex = new RegExp(`{${placeholder}}`, 'g');
          cardHtml = cardHtml.replace(placeholderRegex, this.getPropertyValue(this.post[i], dataKey));
        });
        card.innerHTML = cardHtml;
        this.observer.observe(card);
        this.cardContainer.append(card);
      }
      this.startCount += this.perPage;
      if (this.startCount > this.endPost) {
        this.morePosts = false;
      }
    }
  };

  loadStartCards = () => {
    this.morePosts = true;
    this.startCount = 0;
    this.cardContainer.innerHTML = '';
    let thisPage = this.startCount + this.perPage;
    if (this.morePosts) {
      for (let i = this.startCount; i < thisPage; i++) {
        const card = this.createElement('div', { class: this.classes });
        let cardHtml = this.template;
        Object.entries(this.dataMappings).forEach(([placeholder, dataKey]) => {
          const placeholderRegex = new RegExp(`{${placeholder}}`, 'g');
          cardHtml = cardHtml.replace(placeholderRegex, this.getPropertyValue(this.post[i], dataKey));
        });
        card.innerHTML = cardHtml;
        this.observer.observe(card);
        this.cardContainer.append(card);
      }
      this.startObserve();
      this.startCount += this.perPage;
      if (this.startCount > this.endPost) {
        this.morePosts = false;
      }
    }
  };
}

And this is how we can use the class. In this example we will use an jsonplaceholder endpoint.

// Initialize the InfiniteScroll class with a template and data mappings.
const template = `

Review: {id}

Name: {name}

Email: {email}

{body}

`; // the keys in your json data const dataMappings = { id: 'id', name: 'name', email: 'email', body: 'body', }; const endpoint = 'https://jsonplaceholder.typicode.com/comments'; const App = {}; App.data = []; fetch(endpoint) .then((res) => res.json()) .then((data) => { data.forEach((entry) => { const parsedJson = { id: entry.id, name: entry.name, email: entry.email, body: entry.body, }; App.data.push(parsedJson); }); AlReviews = new InfiniteScroll(template, dataMappings, App.data); AllReviews.loadStartCards(); }) .catch((err) => console.log('Error:', err));
Go back

Here is a live example of the code above