How can I convert scrollIntoView with smooth animation to a Promise? - javascript

I have to scrollIntoView a particular element smoothly and then do something.
Example
element.scrollIntoView({behavior: 'smooth'}).then(() => {
// Do something here
})
I know that it can't be done this way as native scrollIntoView doesn't return a Promise. But, how do I achieve something like this?
I'm using Angular 7 BTW. So if there are any directives that could help me achieve this, it would be great.

You can work with prototypes, I think this could be a solution to your problem without download any npm packages
/* Extends Element Objects with a function named scrollIntoViewPromise
* options: the normal scrollIntoView options without any changes
*/
Element.prototype.scrollIntoViewPromise = function(options){
// "this" refers to the current element (el.scrollIntoViewPromise(options): this = el)
this.scrollIntoView(options);
// I create a variable that can be read inside the returned object ({ then: f() }) to expose the current element
let parent = this;
// I return an object with just a property inside called then
// then contains a function which accept a function as parameter that will be execute when the scroll ends
return {
then: function(x){
// Check out https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API for more informations
const intersectionObserver = new IntersectionObserver((entries) => {
let [entry] = entries;
// When the scroll ends (when our element is inside the screen)
if (entry.isIntersecting) {
// Execute the function into then parameter and stop observing the html element
setTimeout(() => {x(); intersectionObserver.unobserve(parent)}, 100)
}
});
// I start to observe the element where I scrolled
intersectionObserver.observe(parent);
}
};
}
element.scrollIntoViewPromise({behavior: "smooth"}).then(()=>console.log("EHI!"));
I've created an example. I know it's not an angular application, but it's a good starting point. You just need to implement it (If you're using typescript you have to create an interface which extends Element with the new function).

One way you can solve this is by using smooth-scroll-into-view-if-nedded it actually return a promise so you can bind to it and apply your logic.

There is an idea how you may catch animation ending.
You may do it in vanilla JS with a 'scroll' event listener.
Check this example https://codepen.io/Floky87/pen/BEOYvN
var hiddenElement = document.getElementById("box");
var btn = document.querySelector(".btn");
var isScrolling;
function handleScroll(event) {
// Clear our timeout throughout the scroll
window.clearTimeout(isScrolling);
// Set a timeout to run after scrolling ends
isScrolling = setTimeout(function() {
alert(1);
document.removeEventListener("scroll", handleScroll);
}, 66);
}
function handleButtonClick() {
document.addEventListener("scroll", handleScroll, false);
hiddenElement.scrollIntoView({ block: "center", behavior: "smooth" });
}
btn.addEventListener("click", handleButtonClick);

I made it like this
const scrollIntoViewPromise = async (node: HTMLElement, options?: ScrollIntoViewOptions) => {
node.scrollIntoView(options);
return new Promise((resolve) => {
const intersectionObserver = new IntersectionObserver((entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setTimeout(() => {
resolve(true);
intersectionObserver.unobserve(node);
}, 100);
}
});
intersectionObserver.observe(node);
});
};

Related

React state console logs updated state although while drag&dropping later on it uses previous state

I am developing an application in react typescript environment and I faced a problem which I cannot resolve on my own. Whenever I try to set state to its initial value I get my UI updated although console.log(state) would still return previous state. When I console.log it using useEffect with dependency to my state I get the updated value printed out.
My biggest concern is how can that happen that react 'saves' previous state somewhere and uses it when user is performing a drag and drop interaction.
const [board, setBoard] = useState(initialState);
function clearBoard(){
updateBoard(initialState)
console.log(board)
}
function updateBoard(board: UnitHex[][]){
setBoard(board);
//some side actions
}
const draggableElements = document.querySelectorAll(".draggable");
const droppableElements = document.querySelectorAll(".droppable");
function dragStart(event: any) {
event.dataTransfer.setData("text", event.target.id);
}
function dragOver(event: any) {
event.preventDefault();
}
function drop(event: any) {
event.preventDefault();
var tempBoard: UnitHex[][] = board;
console.log("inside drop fn")
console.log(tempBoard);
//drop logic
updateBoard(tempBoard);
event.stopImmediatePropagation();
}
useEffect(() => {
droppableElements.forEach((element) => {
element.addEventListener("dragover", dragOver);
element.addEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.addEventListener("dragstart", dragStart);
});
console.log(board)
}, [board]);
Does anyone know what's going on?
Thanks in advance!!
It's not React that's storing the old information, it's your code. :-)
You're getting elements directly from the DOM during the main run of your component function:
const draggableElements = document.querySelectorAll(".draggable");
const droppableElements = document.querySelectorAll(".droppable");
...which is not how you should do things in React. Then you're adding event handlers to those elements:
useEffect(() => {
droppableElements.forEach((element) => {
element.addEventListener("dragover", dragOver);
element.addEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.addEventListener("dragstart", dragStart);
});
console.log(board);
}, [board]);
...and never removing old handlers. So the elements keep the old handler and the new one. The old handler runs first, does event.stopImmediatePropagation(), and that prevents the new handler from running.
At a minimum:
Don't grab the elements during render, do it in an effect afterward
Remove old handlers with a cleanup callback
Like this:
useEffect(() => {
const draggableElements = document.querySelectorAll(".draggable");
const droppableElements = document.querySelectorAll(".droppable");
droppableElements.forEach((element) => {
element.addEventListener("dragover", dragOver);
element.addEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.addEventListener("dragstart", dragStart);
});
return () => { // Cleanup callback
droppableElements.forEach((element) => {
element.removeEventListener("dragover", dragOver);
element.removeEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.removeEventListener("dragstart", dragStart);
});
};
}, [board]);
That way, only the up-to-date version of dragOver and drop will be hooked up on the elements.
But again, using DOM methods like document.querySelectorAll is usually not best practice in React code. There are rare exceptions; drag and drop may be one of them, although handling it at the component level with global elements seems a bit off.

How to optimize native JavaScript code when refactoring it from Jquery

I need to rewrite some Jquery into native JavaScript code but I am facing a problem that I am not so sure how to solve.
This is Jquery code I need to rewrite in native JS:
$('.class1').click(function () {
setTimeout(() => {
$('.class2').css('top', '252px');
$('.class3').css('bottom', '0px');
}, 200);
$('.class2').css('z-index', '-1');
$('.class1').css('z-index', '-1');
});
And this is what I have written in native JavaScrip:
if (document.querySelector('.class1')){
document.querySelector('.class1').addEventListener('click', function () {
setTimeout(() => {
if (document.querySelector('.class2')) {
document.querySelector('.class2').style.top = '252px';
}
if ( document.querySelector('.class3')) {
document.querySelector('.class3').style.bottom = '0px';
}
}, 200);
if (document.querySelector('.class2')) {
document.querySelector('.class2').style.zIndex = '-1';
}
if ( document.querySelector('.class1')) {
document.querySelector('.class1').style.zIndex = '-1';
}
})
}
I was hoping that people could explain to me how to solve two things:
Is there a more elegant way to check for an element on the current page if the code runs on the whole site?
Is there something else that I can replace those if statements inside the function?
In Jquery those statements are executed one by one but in my case I need to check for an element first and if it is there do something with it.
You can make the code more succinct by storing the result of querySelector within a variable. Also note that a class selector in jQuery can return multiple elements, so the native equivalent of it is querySelectorAll().
As such you will need to loop through all the elements in that collection and add the event handlers, or update their style, as necessary. Due to this loop you don't need to explicitly check for the existence of the elements, as the forEach() will simply not execute if the collection is empty.
With that said, try this:
let class1 = document.querySelectorAll('.class1');
let class2 = document.querySelectorAll('.class2');
let class3 = document.querySelectorAll('.class3');
class1.forEach(el => {
el.addEventListener('click', e => {
setTimeout(() => {
class2.forEach(el => el.style.top = '252px');
class3.forEach(el => el.style.top = '0px');
}, 200);
class1.forEach(el => el.style.zIndex = -1);
class2.forEach(el => el.style.zIndex = -1);
});
});

Is it possible to remove and readd React eventListener within the handler function?

I am working with something like fullpage.js with React, and I need to remove the eventListener while the transition is ongoing.
Is it possible?
React code
function App() {
const wheelHandler = (event) => {
// I need to remove wheelHandler here
setTimeout(() => {
// I need to readd wheelHandler here
}, 1000); // Assume that the transition ends after 1000ms
};
return (
<div className="App" onWheel={wheelHandler} />
);
}
Vanilla JS equivalent
const wheelHandler = (event) => {
window.removeEventListener(wheelHandler);
setTimeout(() => {
window.addEventListener(wheelHandler);
}, 1000);
};
window.addEventListener(wheelHandler);
P.S. I tried the Vanilla JS solution on React but the event handler got triggered multiple times on one wheel scroll. Therefore I got no choice but React's SyntheticEvent.
With the way you're hooking it up, you can't without using a piece of state that tells you whether to hook up the handler and re-rendering, which is probably overkill.
Instead, I'd set a flag (perhaps on an object via a ref) telling the handler to ignore calls during the time you want calls ignored.
Something long these lines:
function App() {
const {current: scrolling} = useRef({flag: false});
const wheelHandler = (event) => {
// I need to remove wheelHandler here
if (scrolling.flag) {
// Bail out
return;
}
scrolling.flag = true;
// ...other logic if any...
setTimeout(() => {
// I need to readd wheelHandler here
scrolling.flag = false;
}, 1000); // Assume that the transition ends after 1000ms
};
return (
<div className="App" onWheel={wheelHandler} />
);
}
Or you can also do it like this, you don't need an extra object (I tend to prefer to use a single ref that holds all of my non-state instance data, but you don't have to):
function App() {
const scrolling = useRef(false);
const wheelHandler = (event) => {
// I need to remove wheelHandler here
if (scrolling.current) {
// Bail out
return;
}
scrolling.current = true;
// ...other logic if any...
setTimeout(() => {
// I need to readd wheelHandler here
scrolling.current = false;
}, 1000); // Assume that the transition ends after 1000ms
};
return (
<div className="App" onWheel={wheelHandler} />
);
}
As they say in the useRef documentation, refs are useful for non-state instance information:
However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.

window.devicePixelRatio change listener

window.devicePixelRatio will return 1 or 2 depending on if I'm using my retina monitor or standard. If I drag the window between the two monitors, this property will change. Is there a way I can have a listener fire when the change occurs?
You can listen to a media query with matchMedia that will tell you when the devicePixelRatio goes past a certain barrier (unfortunately not for arbitrary scale changes).
e.g:
window.matchMedia('screen and (min-resolution: 2dppx)')
.addEventListener("change", function(e) {
if (e.matches) {
/* devicePixelRatio >= 2 */
} else {
/* devicePixelRatio < 2 */
}
});
The listener will be called when you drag a window between monitors, and when plugging in or unplugging an external non-retina monitor (if it causes the window to move from a retina to non-retina screen or vice-versa).
window.matchMedia is supported in IE10+, and all other modern browsers.
References: https://code.google.com/p/chromium/issues/detail?id=123694, MDN on min-resolution
Most (or all?) answers on the internet only detect a specific change. Typically they detect whether the value is 2 or something else.
The issue probably lies in the MediaQuery, because they only allow checking for specific hardcoded values.
With some programming, it's possible to dynamically create a media query, which checks for a change of the current value.
let remove = null;
const updatePixelRatio = () => {
if(remove != null) {
remove();
}
let mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
let media = matchMedia(mqString);
media.addListener(updatePixelRatio);
remove = function() {media.removeListener(updatePixelRatio)};
console.log("devicePixelRatio: " + window.devicePixelRatio);
}
updatePixelRatio();
I took the IMO best answer (by #Neil) and made it a bit more human-readable:
function listenOnDevicePixelRatio() {
function onChange() {
console.log("devicePixelRatio changed: " + window.devicePixelRatio);
listenOnDevicePixelRatio();
}
matchMedia(
`(resolution: ${window.devicePixelRatio}dppx)`
).addEventListener("change", onChange, { once: true });
}
listenOnDevicePixelRatio();
No fixed boundary or variables needed.
Thanks #florian-kirmaier this is exactly what I was looking for and if you pass in the option {once: true} in the event listener there is no need to manually keep track and remove the event listener.
(function updatePixelRatio(){
matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
.addEventListener('change', updatePixelRatio, {once: true});
console.log("devicePixelRatio: " + window.devicePixelRatio);
})();
I prefer this one, so that I can provide a callback, and for the callback not to fire initially but only on changes, and to be able to stop it when no longer needed:
function onPixelRatioChange(cb) {
let mediaQuery
const listenerOptions = { once: true }
let firstRun = true
function onChange() {
if (firstRun) firstRun = false
else cb()
mediaQuery = matchMedia(`(resolution: ${devicePixelRatio}dppx)`)
mediaQuery.addEventListener('change', onChange, listenerOptions)
}
onChange()
return function unlisten() {
mediaQuery.removeEventListener('change', onChange, listenerOptions)
}
}
// Then use it like this:
const unlisten = onPixelRatioChange(() => {
console.log('pixel ratio changed:', devicePixelRatio)
})
// later, stop listening if desired:
unlisten()
Here's a typescript object version of #Florian's answer
export default class DevicePixelRatioObserver {
mediaQueryList: MediaQueryList | null = null
constructor(readonly onDevicePixelRatioChanged: () => void) {
this._onChange = this._onChange.bind(this)
this.createMediaQueryList()
}
createMediaQueryList() {
this.removeMediaQueryList()
let mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
this.mediaQueryList = matchMedia(mqString);
this.mediaQueryList.addEventListener('change', this._onChange)
}
removeMediaQueryList() {
this.mediaQueryList?.removeEventListener('change', this._onChange)
this.mediaQueryList = null
}
_onChange(event: MediaQueryListEvent) {
this.onDevicePixelRatioChanged()
this.createMediaQueryList()
}
destroy() {
this.removeMediaQueryList()
}
}

How to check if an element is clickable using Protractor test

I have an element that does not allow it to be clickable using CSS property pointer-events: none; How can I check whether that element is clickable or not as doing a .click() on the element throws an exception that I cannot catch UnknownError: unknown error: Element is not clickable at point The element is a link so I just want to check if the redirect happened but because of this error it ends the test right away and try catch cannot catch the exception.
I don't know about protractor, but using plain JS you can do:
window.getComputedStyle(element).getPropertyValue('pointer-events') == 'none';
however support for getComputedStyle may not be available in all browsers you wish to support, see MDN compatibility matrix, which indicates no support in IE 8, however it may not support the pointer-events CSS property anyway.
if you really want to use protractor you can use the following:
expect( protractor.ExpectedConditions.elementToBeClickable(element)).not.toBe(true);
There are actually two methods to check it.
1) Using ExpectedConditions
var EC = protractor.ExpectedConditions;
// Waits for the element with id 'abc' to be clickable.
browser.wait(EC.elementToBeClickable($('#abc')), 5000);
If not found or not clickable, will return error.
2) Using protractor's isEnabled, isDisplayed and isPresent
So as far as my understanding goes, you can create isClickable, which will return true if element is present, displayed and enabled and false otherwise:
function isClickable(element) {
return element.isPresent().then((isPresent) => {
if (isPresent) {
return element.isDisplayed().then((isDisplayed) => {
if (isDisplayed) {
return element.isEnabled();
}
return false;
});
}
return false;
});
}
I have wrote small check utility method, keep in mind it will click on element immediately when it become clickable:
import { ElementFinder, promise } from 'protractor';
export let testHelpers = {
isClickable(el: ElementFinder): promise.Promise<boolean> {
return new promise.Promise(resolve => {
let interval = setInterval(() => {
el.click().then(() => {
clearInterval(interval);
setTimeout(() => {
resolve(true);
}, 500);
}, () => { });
}, 100);
});
}
}
In your test code:
mport { testHelpers } from '../src/core/e2e/helpers';
describe('App', () => {
it('should do something', () {
let btn = $('.cls');
browser.wait(testHelpers.isClickable(btn), 3000);
});
});

Categories

Resources