1

I am playing around with intersection observer to create an infinite scroll dog website. As you scroll and 6 dogs appear, an api fires off 6 more times to grab more dogs to add to the DOM. I would like for the dogs to load in as a user scrolls but as an already viewed dog leaves the viewport and goes up on the page, that element is then deleted off the page. SO the dogs always load in scrolling down, but scrolling up you are always at the top of the page. My current implementation in the function called lastFunc is causing it to act really weird. How can I achieve the desired effect.

class CardGenerator {
  constructor() {
    this.$cardContainer = document.querySelector('.card-container');
    this.$allCards = undefined;

    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          entry.target.classList.toggle('show', entry.isIntersecting);
          if (entry.isIntersecting) {
            this.observer.unobserve(entry.target);
          }
        });
      },
      {
        threshold: 1,
        rootMargin: '150px',
      }
    );
    this.loadNewCards();
  }

  cacheDOMElements() {
    this.$allCards = document.querySelectorAll('.card');
  }

  loadNewCards() {
    for (let index = 0; index < 6; index++) {
      fetch('https://dog.ceo/api/breeds/image/random', { method: 'GET' })
        .then((result) => {
          return result.json();
        })
        .then((r) => {
          console.log(r);
          const card = document.createElement('div');
          card.classList.add('card');

          const imageElement = document.createElement('img');
          imageElement.classList.add('forza-img');

          imageElement.setAttribute('src', r.message);
          card.appendChild(imageElement);
          this.observer.observe(card);
          this.$cardContainer.append(card);
          this.cacheDOMElements();
          if (this.$allCards.length % 6 === 0) this.lastFunc();
        });
    }
  }

  lastFunc() {
    console.log(this.$allCards);
    if (this.$allCards.length > 12) {
      this.$allCards.forEach((item, idx) => {
        if (idx < 6) {
          item.remove();
        }
      });
    }

    this.$allCards.forEach((card, idx) => {
      this.observer.observe(card);
    });

    const lastCardObserver = new IntersectionObserver((entries) => {
      const $lastCard = entries[0];
      if (!$lastCard.isIntersecting) return;
      this.loadNewCards();
      lastCardObserver.unobserve($lastCard.target);
    });

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

const cardGenerator = new CardGenerator();
html,
body {
  height: 100%;
  width: 100%;
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

.card {
  float: left;
  width: 48vw;
  margin: 1%;
  transform: translateX(100px);
  opacity: 0;
  transition: 150ms;
}

.card.show {
  transform: translateY(0);
  opacity: 1;
}

.card img {
  width: 100%;
  border-radius: 15px;
  height: 30vh;
  object-fit: cover;
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Dog Random Images</h1>
  <div class="card-container"></div>
</body>

<script src="app.js" ></script>
</html>
klaurtar1
  • 700
  • 2
  • 8
  • 29

2 Answers2

2

When deleting elements, the entire contents of the container are shifted and observers start to fire. In order for observers not to be triggered when deleting an element, it is necessary to shift the scroll just before deleting the element to the height of this element.

Example below:

class CardGenerator {
  constructor() {
    this.$cardContainer = document.querySelector('.card-container');
    this.$allCards = undefined;

    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          entry.target.classList.add('show', entry.isIntersecting);
          if (entry.isIntersecting) {
            this.observer.unobserve(entry.target);
          }
        });
      },
      {
        threshold: 1,
        rootMargin: '150px',
      }
    );
    this.loadNewCards();
  }

  cacheDOMElements() {
    this.$allCards = document.querySelectorAll('.card');
  }

  loadNewCards() {
    for (let index = 0; index < 6; index++) {
      fetch('https://dog.ceo/api/breeds/image/random', { method: 'GET' })
        .then((result) => {
          return result.json();
        })
        .then((r) => {
          console.log(r);
          const card = document.createElement('div');
          card.classList.add('card');

          const imageElement = document.createElement('img');
          imageElement.classList.add('forza-img');

          imageElement.setAttribute('src', r.message);
          card.appendChild(imageElement);
          this.observer.observe(card);
          this.$cardContainer.append(card);
          this.cacheDOMElements();
          if (this.$allCards.length % 6 === 0) this.lastFunc();
        });
    }
  }

  lastFunc() {
    console.log(this.$allCards);
    if (this.$allCards.length > 12) {
      this.$allCards.forEach((item, idx) => {
        if (idx < 6) {
          const scrollTop = this.$cardContainer.scrollTop;
          const height = item.offsetHeight;
          this.$cardContainer.scrollTo(0, Math.max(0, scrollTop - height));
          item.remove();
        }
      });
    }

    this.$allCards.forEach((card, idx) => {
      this.observer.observe(card);
    });

    const lastCardObserver = new IntersectionObserver((entries) => {
      const $lastCard = entries[0];
      if (!$lastCard.isIntersecting) return;
      this.loadNewCards();
      lastCardObserver.unobserve($lastCard.target);
    });

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

const cardGenerator = new CardGenerator();
html,
body {
  height: 100%;
  width: 100%;
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

.card {
  float: left;
  width: 48vw;
  margin: 1%;
  transform: translateX(100px);
  opacity: 0;
  transition: 150ms;
}

.card.show {
  transform: translateY(0);
  opacity: 1;
}

.card img {
  width: 100%;
  border-radius: 15px;
  height: 30vh;
  object-fit: cover;
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Dog Random Images</h1>
  <div class="card-container"></div>
</body>

<script src="app.js" ></script>
</html>

I hope this will help you somehow.

Oleg Barabanov
  • 2,468
  • 2
  • 8
  • 17
  • Code improvements: if (this.$allCards.length % 6 === 0) this.lastFunc(), console.log(".length % 6 === 0"); Only call thelastfunc on the last card. The last card acts as a sentinel, it at the bottom of the set of cards and you have to scroll to the bottom to see the last card and when that is fully visible then you remove the first six, also I have if(index==5) console.log("Observing card "),this.observer.observe(card); in the loadnewcards so we only observe the last item. – JoePythonKing Dec 28 '21 at 10:12
  • The intersectionobserver calls loadnewcards, so we get new cards when the last card is fully visible. – JoePythonKing Dec 28 '21 at 10:18
1
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>

</head>
<body>


<style>
body {
  height: 100%;
  width: 100%;
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

.card {
  width: 48vw;
  margin: 1%;
}

.card.show {
//  opacity: 1;
}

.card img {
  width: 100%;
  border-radius: 15px;
  height: 30vh;
  object-fit: cover;
}


.card-container{
    border: solid 1px #00f;
    padding: 20px;
    overflow-y:scroll;
}
</style>


<style>
    #sentinel{
    height:0px;
}
</style>


<h1>Dog Random Images</h1>
<div class="card-container">
    <div id="sentinel"></div>
</div>


<script>
/* Question on https://stackoverflow.com/questions/70482606/delete-elements-that-have-intersected-the-viewport */
class CardGenerator {
    constructor() {
        this.$cardContainer = document.querySelector('.card-container');
        this.$allCards = undefined;
        this.mysentinel = document.querySelector('#sentinel');

        this.observer = new IntersectionObserver(
            (entries) => {

                let [entry] = entries; //destructure array, get first entry - should only be 1 - sentinel
                if (entry.isIntersecting) {
                    this.observer.unobserve(entry.target);
                    this.loadNewCards();
                }

            }, {
                threshold: 1,
                rootMargin: '150px' /*expanded root/viewport(due to null) by 150px*/,
            }
        );
        this.loadNewCards();

    } // end constructor;

    cacheDOMElements() {
        //The Document method querySelectorAll() returns a static (not live) NodeList
        this.$allCards = document.querySelectorAll('.card');
    }

    loadNewCards() {
        /* https://stackoverflow.com/questions/31710768/how-can-i-fetch-an-array-of-urls-with-promise-all  , from peirix*/
        this.mypromises = [];
        this.mymessages = [];
        this.urls = new Array(6).fill("https://dog.ceo/api/breeds/image/random", 0, 6);

        //create array of promises
        var promises = this.urls.map(url => fetch(url).then(y => y.json()));
        //Promise.all() method takes an iterable of promises
        //promise.all returns a single Promise that resolves to an array of the results of the input promises
        Promise.all(promises)
            .then(results => {
                //accumulate all the urls from message property
                results.forEach(v => this.mymessages.push(v.message));

            })
            .finally(() => {

                let idx = 0;
                for (let message of this.mymessages) {
                    const card = document.createElement('div');
                    card.classList.add('card');
                    const imageElement = document.createElement('img');
                    imageElement.setAttribute('src', message);
                    imageElement.setAttribute('title', `${idx++}:${message}`);
                    card.appendChild(imageElement);
                    this.$cardContainer.appendChild(card);

                }// end for
                
                this.cacheDOMElements();
                
                //stop this sentinel possibly hitting the observer to loadnewcards as we (re)move cards
                this.observer.unobserve(this.mysentinel);
                //if number of cards is>12 then takeoff the first 6
                if (this.$allCards.length > 12) {
                    for (let i = 0; i < 6; i++) {
                        this.$allCards[i].remove();
                    }

                }
                //already exists so move it to bottom of container div
                this.$cardContainer.appendChild(this.mysentinel);
                /*this should be outside the root so when it invokes observer it will not fire loadnewcards*/
                this.observer.observe(this.mysentinel);
            }); //end of finally end of Promise.all

    } //end loadnewcards



} //class CardGenerator


const cardGenerator = new CardGenerator();
</script>  


</body>
</html>
JoePythonKing
  • 1,080
  • 1
  • 9
  • 18
  • Uses single sentinel to observe end of card stack and Promise.all for async url loading. Simplified the decorative transitions. – JoePythonKing Jan 24 '22 at 10:52