M -
M -

Reputation: 28482

How do I use a single IntersectionObserver to perform unique callbacks per observed element?

I need to know out when dozens of HTMLElements are inside or outside of the viewport when scrolling down the page. So I'm using the IntersectionObserver API to create several instances of a VisibilityHelper class, each one with its own IntersectionObserver. With this helper class, I can detect when any HTMLElement is 50% visible or hidden:

Working demo:

// Create helper class
class VisibilityHelper {
  constructor(htmlElem, hiddenCallback, visibleCallback) {
    this.observer = new IntersectionObserver((entities) => {
      const ratio = entities[0].intersectionRatio;
      if (ratio <= 0.0) {
        hiddenCallback();
      } else if (ratio >= 0.5) {
        visibleCallback();
      }
    }, {threshold: [0.0, 0.5]});

    this.observer.observe(htmlElem);
  }
}

// Get elements
const headerElem = document.getElementById("header");
const footerElem = document.getElementById("footer");

// Use helper class to know whether visible or hidden
const headerViz = new VisibilityHelper(
  headerElem,
  () => {console.log('header is hidden')},
  () => {console.log('header is visible')},
);
const footerViz = new VisibilityHelper(
  footerElem,
  () => {console.log('footer is hidden')},
  () => {console.log('footer is visible')},
);
#page {
  width: 100%;
  height: 1500px;
  position: relative;
  background: linear-gradient(#000, #fff);
}
#header {
  position: absolute;
  top: 0;
  width: 100%;
  height: 100px;
  background: #f90;
  text-align: center;
}
#footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 100px;
  background: #09f;
  text-align: center;
}
<div id="page">
  <div id="header">
  Header
  </div>


  <div id="footer">
  Footer
  </div>
</div>

The problem is that my demo above creates one IntersectionObserver for each HTMLElement that needs to be watched. I need to use this on 100 elements, and this question indicates that we should only use one IntersectionObserver per page for performance reasons. Secondly, the API also suggests that one observer can be used to watch several elements, since the callback will give you a list of entries.

How would you use a single IntersectionObserver to watch multiple htmlElements and trigger unique hidden/visible callbacks for each element?

Upvotes: 0

Views: 1167

Answers (1)

morganney
morganney

Reputation: 13590

You can define a callback mapping between your target elements and their visibility state. Then inside of your IntersectionObserver callback, use the IntersectionObserverEntry.target to read the id and invoke the associated callback from the map based on the visibility state of visible or hidden.

Here is a simplified approach based on your example. The gist of the approach is defining the callbacks map and reading the target from the IntersectionObserverEntry:

// Create helper class
class VisibilityHelper {
  constructor(htmlElems, callbacks) {
this.callbacks = callbacks;
this.observer = new IntersectionObserver(
  (entities) => {
    const ratio = entities[0].intersectionRatio;
    const target = entities[0].target;

    if (ratio <= 0.0) {
      this.callbacks[target.id].hidden();
    } else if (ratio >= 0.5) {
      this.callbacks[target.id].visible();
    }
  },
  { threshold: [0.0, 0.5] }
);

htmlElems.forEach((elem) => this.observer.observe(elem));
  }
}

// Get elements
const headerElem = document.getElementById("header");
const footerElem = document.getElementById("footer");

// Use helper class to know whether visible or hidden
const helper = new VisibilityHelper([headerElem, footerElem], {
  header: {
visible: () => console.log("header is visible"),
hidden: () => console.log("header is hidden"),
  },
  footer: {
visible: () => console.log("footer is visible"),
hidden: () => console.log("footer is hidden"),
  },
});
#page {
  width: 100%;
  height: 1500px;
  position: relative;
  background: linear-gradient(#000, #fff);
}
#header {
  position: absolute;
  top: 0;
  width: 100%;
  height: 100px;
  background: #f90;
  text-align: center;
}
#footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 100px;
  background: #09f;
  text-align: center;
}
<div id="page">
  <div id="header">
  Header
  </div>


  <div id="footer">
  Footer
  </div>
</div>

You can use a similar approach if you want to loop over the entire array of entities instead of just considering the first entities[0].

Upvotes: 1

Related Questions