0

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?

M -
  • 26,908
  • 11
  • 49
  • 81
  • 2
    This is a hard one, because, as you mentioned, you have unique callbacks for different elements you are observing. I found this on SO that might help - essentially using passed entry data attributes to change the callback: https://stackoverflow.com/questions/52460010/how-to-call-different-functions-for-different-targets-using-an-intersection-obse – disinfor Nov 12 '21 at 21:46

1 Answers1

1

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].

morganney
  • 6,566
  • 1
  • 24
  • 35
  • Thanks for the answer. I see what you're trying to do, using the ID. I guess this is my fault for using IDs in the example, but I don't necessarily have unique IDs per element. I guess I could [create custom `data` attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) instead, but it feels a bit hacky. I was hoping the `IntersectionObserver` had a more object-oriented approach in mind that I didn't know about. – M - Nov 16 '21 at 18:04
  • You need some way to reference the HTML elements that will be observed, whether it be `id`, `class`, or `data-` attributes. Depending on your requirements the callbacks associated with each HTML element can be defined within the `IntersectionObserver` callback if that is what you mean by `more object-oriented approach`. – morganney Nov 16 '21 at 20:16