I'm upgrading a lazy load scroll mechanism with an intersection observer. My scroll mechanism looks like this:
function setScrollListener(callBack) {
document.addEventListener('scroll', throttle(callBack, 1000));
}
const state = {};
function checkInViewport() {
const offset = 400;
state.unrendered = state.unrendered.filter(el => {
const visible = isInViewport(el, offset)
if (visible) {
display(el);
}
return !visible;
});
}
setScrollListener(checkInViewport)
Without going too much into the weeds, isInViewport uses the offset provided and the getClientBoundRect of the element provided to determine if the element is near the viewport accounting for the offset. Essentially, is this element 400px from the viewport? The method display simply displays the element.
Now using an intersection observer I have this:
const observeInViewport = (el, io) => {
const { offset = 400 } = this.props;
if (isInViewport(el, offset)) {
display(el);
io.unobserve(el);
}
}
const observationOpts = {
root: null,
rootMargin: '0px',
threshold: 0.1,
};
const lazyRef = document.getElementsByClassName("lazy");
const io = new IntersectionObserver(([entry]) => {
observeInViewport(entry.target, io);
}, observationOpts);
[].slice.call(lazyRef).forEach(el => {
io.observe(el);
});
I'm using the same isInViewport and display methods using the offset, but the offset won't trigger until the observer does. The observer is of course trigger by its threshold. I can't provide a negative threshold which would be ideal. I tried using the documentElement as my root option and then providing a rootMargin of ${(el.offsetTop - offset)}px 0px 0px 0px. That is a string template, but the formatting here was messed up with the tick marks.
Can I achieve the same offset logic as I did with my scroll listener, but with an intersection observable? Let me know if you have any questions.
Related
I'm trying to create a lazy loading article list. I've set an Intersecton Observer that checks when the viewport reaches the end of the observed element (the article container) and makes a new api call.
The thing is when I load the new articles in the observed element the height changes but the observation gets triggered only on the initial height.
Should I use a ResizeObserver on top of the IntersectonObserver?
const sections = document.querySelectorAll('.articles_container');
function observeArticlesContainer() {
const changeNav = (entries, observer) => {
entries.forEach((entry) => {
// verify the element is intersecting
if (entry.isIntersecting) {
console.log(entry.target)
// $(results_wrapper).hide()
// $(loading_animation).fadeIn(200);
fetchData();
}
});
}
// init the observer
const options = {
root: null,
rootMargin: '0px 0px -100% 0px',
threshold: 0
}
const observer = new IntersectionObserver(changeNav, options);
// target the elements to be observed
sections.forEach(section => {
observer.observe(section);
});
}
ok, I've just set the container below as the target.
const sections = document.querySelectorAll('footer');
also set rootMargin back to 0px
Cypress's visible matcher treats an element as visible based on a variety of factors, however it doesn't take the viewport into account, so an element that is scrolled off-screen is still treated as visible.
I need to test that a link to an on-page anchor is functioning correctly. Once the link is clicked, the page scrolls to the element with the id as defined in the href of the link (example/#some-id).
How can verify that the element is within the viewport?
I've cobbled together the following commands which appear to work so far, but amazed there isn't on out-of-box solution:
Cypress.Commands.add('topIsWithinViewport', { prevSubject: true }, subject => {
const windowInnerWidth = Cypress.config(`viewportWidth`);
const bounding = subject[0].getBoundingClientRect();
const rightBoundOfWindow = windowInnerWidth;
expect(bounding.top).to.be.at.least(0);
expect(bounding.left).to.be.at.least(0);
expect(bounding.right).to.be.lessThan(rightBoundOfWindow);
return subject;
})
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, subject => {
const windowInnerWidth = Cypress.config(`viewportWidth`);
const windowInnerHeight = Cypress.config(`viewportHeight`);
const bounding = subject[0].getBoundingClientRect();
const rightBoundOfWindow = windowInnerWidth;
const bottomBoundOfWindow = windowInnerHeight;
expect(bounding.top).to.be.at.least(0);
expect(bounding.left).to.be.at.least(0);
expect(bounding.right).to.be.lessThan(rightBoundOfWindow);
expect(bounding.bottom).to.be.lessThan(bottomBoundOfWindow);
return subject;
})
I did a little refactoring on Undistracted's approach if anyone is interested:
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).to.be.within(0, window.innerHeight);
expect(rect.right).to.be.within(0, window.innerWidth);
expect(rect.bottom).to.be.within(0, window.innerHeight);
expect(rect.left).to.be.within(0, window.innerWidth);
return subject;
});
Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).not.to.be.within(0, window.innerHeight);
expect(rect.right).not.to.be.within(0, window.innerWidth);
expect(rect.bottom).not.to.be.within(0, window.innerHeight);
expect(rect.left).not.to.be.within(0, window.innerWidth);
return subject;
});
Uses window.innerWidth and window.innerHeight in case you have used cy.viewport before calling. Also uses .within to facilitate the outside addition.
I have an interesting bug I can't seem to work out and I hope someone with better React knowledge than me can help me work out.
Basically, I have a component (slider carousel, like a Netflix queue) that is trying to set the visibility of two elements (nav slider buttons for left and right nav) if there is overflow of the underlying dev and/or if the underlying div is at a certain position. My visibility setter method is called when onComponentDidMount, when the position of the underlying div changes, and with an window resize event listener.
It works like expected most of the time, however, I have an edge case where I can resize the window, even after going to a new route, and it will work as expected... BUT if I go a new route again I get an error when resizing the window at that point.
It appear as if the refs are not being set after switching routes the second time because they return null.
I've tried detecting if ref is null, but couldn't get that work properly.
setCaretVis() {
const el = this.tray.current;
console.log(el);
const parent = this.wrapper.current;
console.log(parent);
const posRight = this.offsetRight();
const posLeft = el.scrollLeft;
const left = this.caretLeft.current;
const right = this.caretRight.current;
const parWidth = el.parentElement.offsetWidth;
const width = el.scrollWidth;
if (parWidth >= width) {
if (!left.classList.contains("invis")) {
left.classList.add("invis");
} else if (left.classList.contains("invis")) {
}
if (!right.classList.contains("invis")) {
right.classList.add("invis");
}
} else if (parWidth < width) {
if (left.classList.contains("invis") && posLeft != 0) {
left.classList.remove("invis");
} else if (!left.classList.contains("invis") && posLeft === 0) {
left.classList.add("invis");
}
if (right.classList.contains("invis") && posRight != 0) {
right.classList.remove("invis");
} else if (!right.classList.contains("invis") && posRight === 0) {
right.classList.add("invis");
}
}
if (posLeft > 0) {
left.classList.remove("invis");
} else {
left.classList.add("invis");
}
if (posRight === 0) {
console.log("true");
right.classList.add("invis");
} else {
right.classList.remove("invis");
}
}
offsetRight() {
const el = this.tray.current;
//const element = this.refs.tray;
const parent = this.wrapper.current;
const parWidth = parent.offsetWidth;
const width = el.scrollWidth;
const left = el.scrollLeft;
let sub = width - parWidth;
let calc = Math.abs(left - sub);
return calc;
};
// The componentDidMount method
componentDidMount() {
this.setCaretVis();
window.addEventListener("resize", this.setCaretVis);
this.setCaretVis();
}
I would like to set the visibility (adding/removing a css class) on resize after route change without error.
current error reads: Uncaught TypeError: Cannot read property 'offsetWidth' of null
I suspect that your component is recreated when you go to a new route again, but the old listener is still invoked by the resize handler. Try to remove event listener in componentWillUnmount:
componentDidMount() {
this.setCaretVis();
window.addEventListener("resize", this.setCaretVis);
this.setCaretVis();
}
componentWillUnmount() {
window.removeEventListener("resize", this.setCaretVis);
}
When router recreates the component, it will subscribe to resize event again.
From the docs:
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any DOM elements that were created in componentDidMount
given IntersectionObserver like this:
const observeVisibility = intersectionMargin => {
const observer = new IntersectionObserver(
nodes => {
if (nodes[0].isIntersecting) {
/* is really in viewport? */
this.observer.disconnect();
}
},
{ rootMargin: intersectionMargin }
);
observer.observe(...);
};
How to check whether the node itself is actually in viewport or it's just the intersectionMargin that caused observer to be called?
The IntersectionObserver will fire immediately on load. After that, your callback passed to IntersectionObserver will be called when isIntersecting changes or when the intersectionRatio crosses one of your configured thresholds.
As you can see, the callback gets the list of entries and then it is up to you to do what you want.
//assuming 'threshold' is defined in scope
nodes => {
nodes.forEach(entry => {
const { isIntersecting, intersectionRatio } = entry;
if (Array.isArray(threshold)) {
threshold = threshold[threshold.length - 1];
}
if (isIntersecting || intersectionRatio >= threshold) {
this.observer.disconnect();
}
}
}
I'm working on a script that recalculates the coordinates of the area attributes inside the imagemap. When loading the page for the first time, the function is initialised so that the area's coordinates adjust to the width of the users browser. The fist time everything goes well.
I've added a 'resize' event listener on the window element. The function fires (same as the init function), and the coordinates are adjusted as expected. So that works. But there is one problem; I keep getting the error you can see below every time I resize the window (The error doesn't occur while innit fires for the first time).
Error:
Uncaught TypeError: Cannot set property 'coords' of undefined
at http://localhost:3000/js/index.js:78697:33
at Array.forEach (native)
at Object.resize (http://localhost:3000/js/index.js:78696:23)
at Object.start (http://localhost:3000/js/index.js:78691:25)
at Object.init (http://localhost:3000/js/index.js:78679:27)
at Object.start (http://localhost:3000/js/index.js:78724:27)
at init (http://localhost:3000/js/index.js:78720:19)
JavaScript
/* ALL IMPORTANT CONFIG DATA
------------------------------------------------ */
const config = {
allMaps: [],
actualMap: document.getElementsByTagName('map')[0],
allAreas: false,
areaCoords: [],
vector: false,
};
/* RECALCULATION FUNCTIONALITY
------------------------------------------------ */
const RecalculateImageMap = {
init() {
/* AUTOMATICALLY UPDATE COORDINATES ON RESIZED WINDOW
------------------------------------------------ */
window.addEventListener('resize', ImageMapSetup.init);
if (!config.actualMap.newSize) {
RecalculateImageMap.start();
}
},
start() {
config.allAreas = config.actualMap.getElementsByTagName('area');
config.vector = document.getElementById('interactive_vector');
/* ALL COORDINATES TO ARRAY
------------------------------------------------ */
for (let i = 0; i < config.allAreas.length; i++) {
const coords = config.allAreas[i].coords;
config.areaCoords.push(coords.replace(/ *, */g, ',').replace(/ +/g, ','));
}
RecalculateImageMap.resize();
},
resize() {
/* FOR EACH AREA => RESIZE THE COORDINATES
------------------------------------------------ */
config.areaCoords.forEach(function(area, i) {
config.allAreas[i].coords = RecalculateImageMap.scale(area);
});
},
scale(area) {
const allValues = area.split(',');
/* CHECK FOR DIFFERENCE IN SCREENSIZE
------------------------------------------------ */
let totalScale = config.vector.width / config.vector.naturalWidth;
let newArea = [];
/* CHANGE EACH COORDINATE BASED ON THE NEW SCALE (DIFFERENCE SINCE LAST WIDTH)
------------------------------------------------ */
allValues.map(function(coordinate) {
let result = Math.round(Number(coordinate) * totalScale);
newArea.push(result);
});
return newArea;
}
};
/* INITIALIZE RESIZING
------------------------------------------------ */
const ImageMapSetup = {
init() {
ImageMapSetup.start(config.actualMap);
},
start(element) {
if (element) {
RecalculateImageMap.init(element);
config.allMaps.push(element);
}
}
};
ImageMapSetup.init();
Does anyone see what goes wrong when firing the init function on the resize event? I've been looking for a solution for a while now.. But without succes.
Thanks in advance!
IMHO the problem is that you are initializing config.allAreas in start(), but you are always adding coords to config.areaCoords
start() {
config.allAreas = config.actualMap.getElementsByTagName('area');
...
for (let i = 0; i < config.allAreas.length; i++) {
const coords = config.allAreas[i].coords;
config.areaCoords.push(coords.replace(/ *, */g, ',').replace(/ +/g, ','));
}
Afterwards in resize() you are looping over config.areaCoords, which now contains much more elements than config.allAreas:
resize() {
/* FOR EACH AREA => RESIZE THE COORDINATES
------------------------------------------------ */
config.areaCoords.forEach(function(area, i) {
config.allAreas[i].coords = RecalculateImageMap.scale(area);
});
},
So I suppose that the error occurs here at the config.allAreas[i].coords = assignment because i is much greater than the length of the config.allAreas array.
Furthermore this would explain your statement "The weird thing is that the function works perfect for the first time".
An assignment like config.areaCoords = []; in the start() function should solve the problem.