Reputation: 28482
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:
// 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
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