I have a react component that uses scroll events. When the scroll event is called it basically runs the handler as expected. However I need to be able to call a method once when the scroll events begin to fire. I understand the idea of a debounce method which would fire when the scroll stops, but I need to find a way to fire once when scrolling begins. (For sure NO jQuery can be used).
componentDidMount() {
window.addEventListener('scroll', this.onScroll);
this.setState({
// sets some state which is compared in a shouldComponentUpdate
});
}
componentWillUnmount() {
window.removeEventListener('scroll', this.onScroll);
}
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
The handler seen below runs some code:
onScroll() {
this.scrollTop = document.documentElement.scrollTop;
this.update();
}
But I need a function that runs once:
scrollStart() {
}
I would love to say I have tried something but unfortunately I have no ideas. Any assist would be greatly appreciated.
There is no real scrolling state in the browser; the scroll event happens, and then it's over.
You could e.g. create a new timeout each time the user scrolls and set your own scrolling state to false if the user hasn't scrolled until the timeout function is run.
Example
class App extends React.Component {
timeout = null;
state = { isScrolling: false };
componentDidMount() {
window.addEventListener("scroll", this.onScroll);
}
componentWillUnmount() {
window.removeEventListener("scroll", this.onScroll);
}
onScroll = () => {
clearTimeout(this.timeout);
const { isScrolling } = this.state;
if (!isScrolling) {
this.setState({ isScrolling: true });
}
this.timeout = setTimeout(() => {
this.setState({ isScrolling: false });
}, 200);
};
render() {
return (
<div
style={{
width: 200,
height: 1000,
background: this.state.isScrolling ? "green" : "red"
}}
/>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Do you want the scrollStart() function to only fire once, the very first time a user scrolls? If do, this could be easily accomplished with a scrollStarted variable. if (scrollStarted == false) { scrollStarted = true; scrollStart(); }.
I can imagine a similar scenario if you want the function to fire when a user starts scrolling from the top (it can fire again if the user returns to the top). Just replace (scrollStarted == false) with scrollTop == 0.
There is no scrollstart event in javascript, however you can register pointer events on the parent element and scroll events on the target element.
Then, for example, in the pointerdown callback reset a variable that gets set when scrolling starts.
If you want you can even dispatch a custom "scrollstart" event on the target when the scroll event is triggered and var scrolling is not set.
For document.body you can listen for pointer ( or touch or mouse ) events on window.
For this you could define a static variable and when the scroll starts, put true in the variable and enter a while that keeps checking if the scrool continues. Using this logic, you may be able to do something.
Related
I've faced with problem using React and React Material-UI components. What I need:
1) User clicks button in my component - I should add mousemove listener to the page and show ProgressBar.
2) User moves mouse - I count events, and update my ProgressBar.
3) When count of events is 50, I remove mousemove listener and hide ProgressBar.
I tried to do this with React useEffect, useState Hooks, but it does not remove listener. I don't understand, why.
Here is my code:
const [completed, setCompleted] = React.useState(0);
const [keyGenState, setKeyGenState] = React.useState(0);
const updateMousePosition = ev => {
console.log("UMP");
setCompleted(old => old + 1);
/*I tried to check completed value here, but it always is 0 - maybe, React re-renders component on setState..
And I decided to take useEffect hook (see below)*/
};
useEffect(() => {
console.log(completed); /*Just to understand, what happens) */
if (completed === 49) {
return () => {
/*When completed value is 50, this log message appears, but mouse listener still works! */
console.log("Finish!");
document.removeEventListener("mousemove", updateMousePosition);
setKeyGenState(2);
}
}
}, [completed]);
function handleChange(e) {
switch (e.currentTarget.id) {
/*startKeyGen - it is ID of my Button. When user clicks it, I show ProgressBar and add mousemove listener*/
case "startKeyGen" : {
setKeyGenState(1);
document.addEventListener("mousemove", updateMousePosition);
break;}
}
}
/*Other logics. And finally, JSX code for my ProgressBar from Material-UI*/
<LinearProgress hidden={keyGenState !== 1 } variant="determinate" value={completed} style={{marginTop: 10}} />
It looks really strange: why React ignores removeEventListener.
Please, explain, where is my mistake.
UPD: Thanks a lot! I used useCallback hook, in this manner:
const updateMousePosition = React.useCallback(
(ev) => {
//console.log("Calback");
console.log(ev.clientX);
setCompleted(old => old + 1);
},
[],
);
useEffect(() => {
//console.log(completed); /*Just to understand, what happens) */
if (completed === 49) {
return () => {
/*When completed value is 50, this log message appears, but mouse listener still works! */
console.log("Finish!");
document.removeEventListener("mousemove", updateMousePosition);
setKeyGenState(2);
}
}
});
But I still don't understand completely.. So, when I used useCallback with empty dependencies array, this function (updateMousePosition), will be unchanged during all "life" of my component? And in useEffect I remove mouseListener. It is magic for me: why does useEffect ignore removing without useCallback?
Try to use React.useCallback for updateMousePosition. Every change in your component creates new function (reference). So React.useEffect remove last updateMousePosition but doesn't remove added in handleChange.
How to delay 300ms and take only last event from mouseLeave event stream Observable in RxJS? I wanted to take latest event only and delay it to 300 milli seconds then fire a method.
class MouseOverComponent extends React.Component {
state = {menuIsOpen: false}
componentDidMount() {
this.mouseLeave$ = Rx.Observable.fromEvent(this.mouseDiv, "mouseleave")
.delay(300)
.throttleTime(300)
.subscribe(() => /* here I want to hide the div */);
}
componentWillUnmount() {
this.mouseLeave$.unsubscribe();
}
menuToggle = e => {
e && e.preventDefault()
let {menuIsOpen} = this.state
menuIsOpen = !menuIsOpen
this.setState({menuIsOpen, forceState: true})
}
render() {
// const menuStateClass = ... resolving className with state
return (
<div ref={(ref) => this.mouseDiv = ref}>
Move the mouse and look at the console...
</div>
);
}
}
but its not working its firing previous events also. Its hiding and showing uncontrollable while i do fast mouse leave.
I want mouseDiv when mouse leaves from the div and wait for 300ms then hide.
Add a first() and repeat() will reset your stream from clean state and it probably can solve your issue.
Rx.Observable.fromEvent(block, "mouseleave")
.delay(300)
.throttleTime(300)
.first()
.repeat()
.subscribe(console.log);
fiddle: http://jsfiddle.net/cy0nbs3x/1384/
I think by "take latest event only" you mean you want to get the last value from fromEvent(this.mouseDiv, "mouseleave") when you call this.mouseLeave$.unsubscribe();.
By calling .unsubscribe() you dispose the chain which is not what you want in this case. Instead you can use takeUntil and takeLast(1) operators like the following to complete the chain that triggers takeLast(1) that passes the last value it received:
componentDidMount() {
this.mouseLeave$ = Rx.Observable.fromEvent(this.mouseDiv, "mouseleave")
.takeUntil(this.unsub$)
.takeLast(1)
.delay(300)
.subscribe(() => /* here I want to hide the div */);
}
componentWillUnmount() {
this.unsub$.next();
}
How can I add a className when the page scrolls? I have ready many other articles and answers to this (may be a duplicate) but none have helped me understand what is wrong with my code below.
If the code is not the issue I believe that it stems from a perspective wrapper around the app that may disallow the registration of scroll. How can I add the event listener to register scroll on id=container
constructor(props) {
super(props)
this.state = {
isStuck: true,
}
this.handleHeaderStuck = this.handleHeaderStuck.bind(this)
}
componentDidMount () {
window.addEventListener('scroll', this.handleHeaderStuck);
}
componentWillUnmount () {
window.removeEventListener('scroll', this.handleHeaderStuck);
}
handleHeaderStuck() {
if (window.scrollY === 0 && this.state.isStuck === true) {
this.setState({isStuck: false});
}
else if (window.scrollY !== 0 && this.state.isStuck !== true) {
this.setState({isStuck: true});
}
}
render() {
return (
<main className={this.state.isStuck ? 'header-stuck' : ''}>
...
</main>
This screenshot reassures me that the issue is with the registering of onScroll listener
Be sure your component have enough height for scroll. Your code works.
Add some height to main and check it.
main {
height: 2000px;
}
https://jsfiddle.net/69z2wepo/156204/
You code has an issue, onScroll is attached a listener function handleScroll whereas the function is handleHeaderStuck in your case. Change the listener to execute the correct function.
componentDidMount () {
window.addEventListener('scroll', this.handleHeaderStuck);
}
componentWillUnmount () {
window.removeEventListener('scroll', this.handleHeaderStuck);
}
I am trying to listen to scroll event in vue component, but when I have tried to set it up like this:
<div id="app"
:class="{'show-backdrop': isLoading || showBackdrop}"
#scroll="handleScroll">
And then in the methods:
handleScroll () {
const header = document.querySelector('#header');
const content = document.querySelector('#content');
const rect = header.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const headerTop = rect.top + scrollTop;
if (rect.top <= 0) {
header.classList.add('fixed');
content.classList.add('content-margin');
} else {
header.classList.remove('fixed');
content.classList.remove('content-margin');
}
}
That is not working.
I had to do a workaround like this:
beforeMount () {
window.addEventListener('scroll', this.handleScroll);
},
beforeDestroy () {
window.removeEventListener('scroll', this.handleScroll);
}
Why is the first approach not working, that should be the Vue way of doing things?
What you were initially setting up:
<div id="app"
:class="{'show-backdrop': isLoading || showBackdrop}"
#scroll="handleScroll">
Will only detect scroll on that div. That div would need to first be scrollable with overflow:scroll; or something similar, then when you scroll inside that div, the handleScroll will be triggered.
What you setup in javascript is listening to scroll on the window element. This will get triggered any time you scroll the page. What you did is correct for detecting scroll events on the window, however, keep in mind that these events will only be registered as long as this component is alive.
Is there a js listener for when a user scrolls in a certain textbox that can be used? Kinda like onclick except for scrolling. I saw HTML5 event listener for number input scroll - Chrome only but that seems to be for chrome only. I'm looking for something cross-browser.
For those who found this question hoping to find an answer that doesn't involve jQuery, you hook into the window "scroll" event using normal event listening. Say we want to add scroll listening to a number of CSS-selector-able elements:
// what should we do when scrolling occurs
function runOnScroll(element) {
// not the most exciting thing, but a thing nonetheless
console.log(element);
};
// grab elements as array, rather than as NodeList
const elements = document.querySelectorAll(`...`);
// and then make each element do something on scroll
elements.forEach(element => {
window.addEventListener(
"scroll",
() => runOnScroll(element),
{ passive: true }
);
});
Or alternatively, bind a single scroll listener, with evt => runOnScroll(evt) as handler and then figure out what to do with everything in elements inside the runOnScroll function instead.
Note that we're using the passive attribute to tell the browser that this event won't interfere with scrolling itself. This is important if we want smooth scrolling behaviour (and also means we shouldn't do perform reflow-triggering DOM updates during scroll).
For bonus points, you can give the scroll handler a lock mechanism so that it doesn't run if we're already scrolling:
// global lock, so put this code in a closure of some sort so you're not polluting.
let locked = false;
let lastCall = false;
function runOnScroll(element) {
if(locked) return;
if (lastCall) clearTimeout(lastCall);
lastCall = setTimeout(() => {
runOnScroll(element);
// you do this because you want to handle the last
// scroll event, even if it occurred while another
// event was being processed.
}, 200);
// ...your code goes here...
locked = false;
};
I was looking a lot to find a solution for sticy menue with old school JS (without JQuery). So I build small test to play with it. I think it can be helpfull to those looking for solution in js.
It needs improvments of unsticking the menue back, and making it more smooth.
Also I find a nice solution with JQuery that clones the original div instead of position fixed, its better since the rest of page element dont need to be replaced after fixing. Anyone know how to that with JS ?
Please remark, correct and improve.
<!DOCTYPE html>
<html>
<head>
<script>
// addEvent function by John Resig:
// http://ejohn.org/projects/flexible-javascript-events/
function addEvent( obj, type, fn ) {
if ( obj.attachEvent ) {
obj['e'+type+fn] = fn;
obj[type+fn] = function(){obj['e'+type+fn]( window.event );};
obj.attachEvent( 'on'+type, obj[type+fn] );
} else {
obj.addEventListener( type, fn, false );
}
}
function getScrollY() {
var scrOfY = 0;
if( typeof( window.pageYOffset ) == 'number' ) {
//Netscape compliant
scrOfY = window.pageYOffset;
} else if( document.body && document.body.scrollTop ) {
//DOM compliant
scrOfY = document.body.scrollTop;
}
return scrOfY;
}
</script>
<style>
#mydiv {
height:100px;
width:100%;
}
#fdiv {
height:100px;
width:100%;
}
</style>
</head>
<body>
<!-- HTML for example event goes here -->
<div id="fdiv" style="background-color:red;position:fix">
</div>
<div id="mydiv" style="background-color:yellow">
</div>
<div id="fdiv" style="background-color:green">
</div>
<script>
// Script for example event goes here
addEvent(window, 'scroll', function(event) {
var x = document.getElementById("mydiv");
var y = getScrollY();
if (y >= 100) {
x.style.position = "fixed";
x.style.top= "0";
}
});
</script>
</body>
</html>
Wont the below basic approach doesn't suffice your requirements?
HTML Code having a div
<div id="mydiv" onscroll='myMethod();'>
JS will have below code
function myMethod(){ alert(1); }
If scroll event is not working for some reason then try wheel event instead:
document.addEventListener('wheel', (event) => {console.log('scrolled')});
Is there a js listener for when a user scrolls in a certain textbox that can be used?
DOM L3 UI Events spec gave the initial definition but is considered obsolete.
To add a single handler you can do:
let isTicking;
const debounce = (callback, evt) => {
if (isTicking) return;
requestAnimationFrame(() => {
callback(evt);
isTicking = false;
});
isTicking = true;
};
const handleScroll = evt => console.log(evt, window.scrollX, window.scrollY);
document.defaultView.onscroll = evt => debounce(handleScroll, evt);
For multiple handlers or, if preferable for style reasons, you may use addEventListener as opposed to assigning your handler to onscroll as shown above.
If using something like _.debounce from lodash you could probably get away with:
const handleScroll = evt => console.log(evt, window.scrollX, window.scrollY);
document.defaultView.onscroll = evt => _.debounce(() => handleScroll(evt));
Review browser compatibility and be sure to test on some actual devices before calling it done.
let lastKnownScrollPosition = 0;
let ticking = false;
function doSomething(scrollPos) {
// Do something with the scroll position
}
document.addEventListener('scroll', function(e) {
lastKnownScrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(lastKnownScrollPosition);
ticking = false;
});
ticking = true;
}
});