Null ref after switching route in React - javascript

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

Related

React Hooks - set last value?

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];
}

How to convert javascript code for Angular

I'm trying to implement something like the following into an Angular project: https://codepen.io/vincentorback/pen/NGXjda
The code compiles just fine in VS code, but when I try and preview in the browser, I get the following two errors:
Uncaught (in promise): TypeError undefined is not an object (evaluating 'this.context.addEventListener')
TypeError undefined is not an object (evaluating 'this.getScrollPos')
Stackblitz: https://stackblitz.com/edit/ionic-rv4ju7
home.page.ts
export class HomePage implements OnInit {
context = document.getElementsByClassName('loop')[0];
startElement = document.getElementsByClassName('is-start')[0];
clones = document.getElementsByClassName('is-clone');
disableScroll = false;
scrollWidth;
scrollPos;
clonesWidth;
i;
constructor() {
window.requestAnimationFrame(this.reCalc);
this.context.addEventListener('scroll', function () {
window.requestAnimationFrame(this.scrollUpdate);
}, false);
window.addEventListener('resize', function () {
window.requestAnimationFrame(this.reCalc);
}, false);
}
getScrollPos() {
return (this.context.pageXOffset || this.context.scrollLeft) - (this.context.clientLeft || 0);
}
setScrollPos(pos) {
this.context.scrollLeft = pos;
}
getClonesWidth() {
this.clonesWidth = 0;
this.i = 0;
for (this.i; this.i < this.clones.length; this.i += 1) {
this.clonesWidth = this.clonesWidth + this.clones[this.i].clientWidth;
}
return this.clonesWidth;
}
reCalc() {
this.scrollPos = this.getScrollPos();
this.scrollWidth = this.context.scrollWidth;
this.clonesWidth = this.getClonesWidth();
if (this.scrollPos <= 0) {
this.setScrollPos(1);
}
}
scrollUpdate() {
if (this.disableScroll === false) {
this.scrollPos = this.getScrollPos();
if (this.clonesWidth + this.scrollPos >= this.scrollWidth) {
// Scroll to the left when you’ve reached the far right
this.setScrollPos(1); // Scroll 1 pixel to allow scrolling backwards.
this.disableScroll = true;
} else if (this.scrollPos <= 0) {
// Scroll to the right when you reach the far left.
this.setScrollPos(this.scrollWidth - this.clonesWidth);
this.disableScroll = true;
}
if (this.disableScroll) {
// Disable scroll-jumping for a short time to avoid flickering.
window.setTimeout(function () {
this.disableScroll = false;
}, 40);
}
}
}
}
You need to move your code from the constructor to AfterViewInit, where DOM elements are available. As a further recommendation, I would recommend that keep what you can out of the constructor. Constructor is mainly used for initializing variables, not doing any logic.
Furthermore, you have issues with this, this doesn't point to what you think it does. Take a read: How to access the correct `this` inside a callback? Very useful reading! So I would recommend to use arrow functions instead of function to keep the context of this.
So change things like:
this.context.addEventListener('scroll', function () {
to:
this.context.addEventListener('scroll', () => {
Here's a fork of your StackBlitz
PS: where you can, make use of Angular tools instead of accessing the DOM like .getElementById. Just as a future hint. Many times angular has own set of tools, and accessing and manipulating the DOM from the component should be a last resort.

Plain javascript ScrollTop is not working

When I get a new message or send a new message chat should scroll down. I handle that with this watcher (I update random value in Vuex and watch for changes within component for chat):
watch: {
scrollChatDown: function (val) {
if (this.$refs.chat !== undefined) {
this.$refs.chat.scrollTop = 9999999999999999999999
console.log('WORKING!')
}
}
}
I get this console.log in Mozilla but scrollTop is not working, is there any other solution for this? :D
See this one, uses the current height to scroll down
watch: {
scrollChatDown: function (val) {
if (this.$refs.chat !== undefined) {
this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight
console.log('WORKING!')
}
}
}

WebDriverError: element click intercepted: Other element would receive the click - detection

I would like to detect if an element is clickable before attempting to click it. In my specific use case the element is hidden by another element on top of it during processing and when done, the overlay is removed and the element can be clicked. Unfortunately the condition elementIsVisible does not consider an element to be hidden by another element nor does the method WebElement.isDisplayed of an element.
// find an element that is hidden behind some overlay
const hiddenElement = await driver.findElement(By.id('hiddenElement'));
// wait for element returns the "hidden" element
const await visibleElement = driver.wait(webdriver.until.elementIsVisible(hiddenElement));
// "isDisplayed" reports the "hidden" element as visible
const visible = await hiddenElement.isDisplayed();
I could clearly use the overlay element to detect if the element is hidden but this would have to be customized for each different type of overlay and I'm actually looking for a more generic way to detect if an element is actually clickable.
I did actually find a solution that works as expected by myself that can be distilled to the following:
A function isElementClickable that is responsible for checking if an element is clickable:
function isElementClickable(element) {
const SCRIPT = `
const element = arguments[0];
// is element visible by styles
const styles = window.getComputedStyle(element);
if (!(styles.visibility !== 'hidden' && styles.display !== 'none')) {
return false;
}
// is the element behind another element
const boundingRect = element.getBoundingClientRect();
// adjust coordinates to get more accurate results
const left = boundingRect.left + 1;
const right = boundingRect.right - 1;
const top = boundingRect.top + 1;
const bottom = boundingRect.bottom - 1;
if (document.elementFromPoint(left, top) !== element ||
document.elementFromPoint(right, top) !== element ||
document.elementFromPoint(left, bottom) !== element ||
document.elementFromPoint(right, bottom) !== element) {
return false;
}
return true;
`;
return element.getDriver().executeScript(SCRIPT, element);
}
A function 'elementIsClickableCondition' that can be used as a replacement condition instead of webdriver.until.elementIsVisible:
function elementIsClickableCondition(locator) {
return new webdriver.WebElementCondition('until element is visible', async function (driver) {
try {
// find the element(s)
const elements = await driver.findElements(locator);
if (elements.length > 1) {
throw new Error(`elementIsClickableCondition: the locator "${locator.toString()} identifies "${elements.length} instead of 1 element`);
} else if (elements.length < 1) {
return null;
}
const element = elements[0];
// basic check if the element is visible using the build-in functionality
if (!await element.isDisplayed()) {
return null;
}
// really check if the element is visible
const visible = await isElementClickable(element);
return visible ? element : null;
} catch (err) {
if (err instanceof webdriver.error.StaleElementReferenceError) {
return null;
} else {
throw err;
}
}
});
}

how to check if node is visible in intersectionObserver api

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();
}
}
}

Categories