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();
}
}
}
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
I'm trying to write basic hooks to get currentScroll, lastScroll, scrollSpeed while scrolling.
function useDocScroll() {
const isClient = typeof window === "object"
function getScroll() {
return isClient
? window.pageYOffset || document.documentElement.scrollTop
: undefined
}
const [docScroll, setDocScroll] = useState(getScroll)
const [lastScroll, setLastScroll] = useState(null)
const [scrollSpeed, setScrollSpeed] = useState(Math.abs(docScroll - lastScroll))
useEffect(() => {
if (!isClient) {
return false
}
function handleScroll() {
setDocScroll(getScroll())
setLastScroll(getScroll()) // <-- why is this working?
// setLastScroll(docScroll) // <-- why is this not working?
setScrollSpeed(Math.abs(docScroll - lastScroll)) // <-- why is this not working?
}
window.addEventListener("scroll", handleScroll)
}, [])
return [docScroll, lastScroll, scrollSpeed]
}
It seems like when I do setLastScroll(getScroll()), it saves the last scroll value well.
But I don't understand because when handleScroll() is firing, shouldn't getScroll() value stays the same? I don't get it why setDocScroll(getScroll()) and setLastScroll(getScroll()) have different value.
Also, I thought I could do setLastScroll(docScroll), meaning 'set lastScroll value with current docScroll value', but it just prints '0' while docScroll value changes.
Why is this? I want to understand better.
+) And I can't get scrollSpeed which is calculated by docScroll and lastScroll, but I don't know how to get those values.
I think why your code not working is because of following two reasons:
using docScroll directly after setDocScroll won't work because setState is asynchronous task. There is no guarantee that docScroll is updated before executing next statement
you are getting 0 because of scrolling happening inside some particular element (probably). Since document.documentElement points to html element and there is no scrolling inside it. So you receive 0
Solution:
You don't need multiple useStates. Since scroll events emits too frequently, i think its good idea to use useReducer to reduce number of renders. It is important understand where scrolling happening whether on root level or inside some element.
For below solution i proposed:
if scroll happening on root level (html element) no need to pass element to useDocScroll. If scroll happening inside particular element, you need to pass element reference.
const initState = {
current: 0,
last: 0,
speed: 0,
};
function reducer(state, action) {
switch (action.type) {
case "update":
return {
last: state.current,
current: action.payload,
speed: Math.abs(action.payload - state.current) || 0,
};
default:
return state;
}
}
const isClient = () => typeof window === "object";
function useDocScroll(element = document.documentElement) {
const [{ current, last, speed }, dispatch] = useReducer(reducer, initState);
function getScroll() {
return isClient() ? element.scrollTop : 0;
}
function handleScroll() {
dispatch({ type: "update", payload: getScroll() });
}
useEffect(() => {
if (!isClient()) {
return false;
}
element.addEventListener("scroll", handleScroll);
return () => element.removeEventListener("scroll", handleScroll);
}, []);
return [current, last, speed];
}
Example:
if scroll happening inside window
const {current, last, speed} = useDocScroll()
if scroll happening in particular element
const {current, last, speed} = useDocScroll(document.getElementById("main"))
It not working due to closures:
useEffect(() => {
if (!isClient) {
return false;
}
function handleScroll() {
setDocScroll(getScroll());
// On calling handleScroll the values docScroll & lastScroll
// are always will be of the first mount,
// the listener "remembers" (closures) such values and never gets updated
setLastScroll(docScroll);
setScrollSpeed(Math.abs(docScroll - lastScroll));
// v Gets updated on every call
setLastScroll(getScroll());
}
// v Here you assigning a callback which closes upon the lexical scope of
// docScroll and lastScroll
window.addEventListener('scroll', handleScroll);
}, []);
To fix it, a possible solution can be a combination of a reference (useRef) and functional setState.
For example:
setScrollSpeed(lastScroll => Math.abs(getScroll() - lastScroll))
Not sure why lastScrolled needed:
function useDocScroll() {
const [docScroll, setDocScroll] = useState(getScroll());
const lastScrollRef = useRef(null);
const [scrollSpeed, setScrollSpeed] = useState();
useEffect(() => {
if (!isClient) {
return false;
}
function handleScroll() {
const lastScroll = lastScrollRef.current;
const curr = getScroll();
setScrollSpeed(Math.abs(getScroll() - (lastScroll || 0)));
setDocScroll(curr);
// Update last
lastScrollRef.current = curr;
}
window.addEventListener('scroll', handleScroll);
}, []);
return [docScroll, lastScrollRef.current, scrollSpeed];
}
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
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.
I am trying to determine whether a slot element has a scrollbar. If it does I want to add a show all/show fewer link next to the slot which will expand or contract it accordingly.
I am using IntersectionObserver because it seems designed for this very use case. The slot contains a ul which itself has a variable number of li children. The ul is not populated until after the function that adds the show more/show link is fired - hence the use of IntersectionObserver. The function I wrote should return a boolean:
_isOverflowing( rootElem, childElem ) {
// rootElem is the slot; childElem is the ul
let observer;
const options = {
root: rootElem,
threshold: 1.0
};
function handleIntersect(entries, observer) {
entries.forEach(entry => {
return entry.intersectionRatio >= options.threshold
});
}
observer = new IntersectionObserver(handleIntersect, options);
return observer.observe( childElem );
}
What I am seeing is that the final function
observer.observe( childElem )
returns as undefined. What am I doing wrong?