I'm detecting keypresses in a component that is aware of what dialog component is currently open elsewhere in the app via a prop, currentDialog. Normally I'd be able to access this prop in a nested function but it seems this isn't possible when using useCallback:
export const AllAreaNav = (props) => {
console.log('AllAreaNav / props.currentDialog: ', props.currentDialog); // displays correct dialog
const handleKeyPress = useCallback((event) => {
console.log('AllAreaNav / handleKeyPress / props.currentDialog: ', props.currentDialog); // displays undefined
if(event.keyCode === 70) {
//Do whatever when F is pressed
console.log("F key pressed");
if (props.currentDialog == "DialogSearchBar") {
// Take action based on DialogSearchBar being active
} else {
// Take action based on DialogSearchBar NOT being active
}
}
}, []);
useEffect(() => {
// Listener for keypresses
document.addEventListener("keydown", handleKeyPress, false);
return () => {
document.removeEventListener("keydown", handleKeyPress, false);
};
}, []);
return (
{jsxElements}
)
};
So now I'm a little unsure of a straightforward way of passing this as a parameter - assuming this would be the next step. From researching I believe it's fine to add another parameter alongside event? That this should work as I intend:
const handleKeyPress = useCallback((event, currentDialog) => {
However, I'm not entirely sure of how to initially pass this to the function. If I modify the listener to be:
document.addEventListener("keydown", handleKeyPress(event, props.currentDialog, false);
I'm unsure if this is correct, or where exactly to define event in this context, in the manner handleKeyPress defaults its as a parameter.
It seems that you were trying to resolve the problem by parametrizing the callback but you did not have an event in your context. In order to parametrize AND have the event in context, you must create a closure on the currentDialog parameter.
You can try this solution:
/**
* move callback definition outside the component
* and create a closure on currentDialog (function returning a function)
*/
const handleKeyPress = (currentDialog) => (event) => {
if (event.keyCode === 70) {
//Do whatever when F is pressed
console.log("F key pressed");
if (currentDialog == "DialogSearchBar") {
// Take action based on DialogSearchBar being active
} else {
// Take action based on DialogSearchBar NOT being active
}
}
};
export const AllAreaNav = (props) => {
console.log("AllAreaNav / props.currentDialog: ", props.currentDialog); // displays correct dialog
useEffect(() => {
// Listener for keypresses
const listener = handleKeyPress(props.currentDialog); // use closure to create a callback closured on currentDialog value
document.addEventListener(
"keydown",
listener,
false
);
return () => {
document.removeEventListener(
"keydown",
listener,
false
);
};
}, [handleKeyPress, props.currentDialog]); // pass callback and currentDialog value to dependency array
return { jsxElements };
};
Related
I'm trying to implement parallax with intersectionObserver API which calls eventListener scroll function with callback. The problem is what I want to pass parameter "entry.target" to add and removeEventListener functions, is it possible to remove event listener callback with parameter?.
Currently I have:
const parallaxScroll = trg => {
trg.style.top = `${-trg.getBoundingClientRect().top / 7.3}px`;
}
const addParallax = ([entry]) => {
if(entry.isIntersecting){
window.addEventListener('scroll', parallaxScroll);
//what I want to:
window.addEventListener('scroll', parallaxScroll(entry.target));
}else{
window.removeEventListener('scroll', parallaxScroll);
//what I want to:
window.removeEventListener('scroll', parallaxScroll(entry.target));
}
}
You would need to create a wrapper function (or call .bind to do the same) for each parameter. Assuming that you can have several entry objects that each could have an associated active scroll-listener, but not more than one per entry, you could dedicate a property of entry for storing that listener reference:
const addParallax = ([entry]) => {
if(entry.isIntersecting) {
if (entry.listener) return; // already have an active listener for this entry
entry.listener = () => parallaxScroll(entry.target);
window.addEventListener('scroll', entry.listener);
} else {
window.removeEventListener('scroll', entry.listener);
entry.listener = null;
}
}
Or with .bind:
const addParallax = ([entry]) => {
if(entry.isIntersecting) {
if (entry.listener) return; // already have an active listener for this entry
entry.listener = parallaxScroll.bind(null, entry.target);
window.addEventListener('scroll', entry.listener);
} else {
window.removeEventListener('scroll', entry.listener);
entry.listener = null;
}
}
I'm having a hard time to remove Event listener type currying function.
// I register first, at some time I want to remove using freezeHighlight but it doesn't working (without currying function it's working like a charm)
const privateMethods = {
highlighted (index) {
return function (event) {
event.target.setAttribute('filter', 'url(#glow)')
// need param index later
}
}
}
register (node, index) {
node.addEventListener('mouseover', privateMethods.highlighted(index))
}
freezeHighlight (node) {
node.removeEventListener('mouseover', privateMethods.highlighted)
}
Is it possible to remove event listener type currying function or should I procede with a workaround?
You need to memoize the handler you create so that you can remove it later.
const handlers = {};
const privateMethods = {
highlighted (index) {
// return the saved handler if we've been called before
// or create a new handler, save it, and return it.
return handlers[index] || (handlers[index] = function (event) {
event.target.setAttribute('filter', 'url(#glow)')
// need param index later
});
}
}
register (node, index) {
// add the handler
node.addEventListener('mouseover', privateMethods.highlighted(index))
}
freezeHighlight (node, index) {
// will remove the handler
node.removeEventListener('mouseover', privateMethods.highlighted(index))
}
var listener
register (node, index) {
listener = privateMethods.highlighted(index)
node.addEventListener('mouseover', listener)
}
freezeHighlight (node) {
node.removeEventListener('mouseover', listener)
}
You think privateMethods.highlighted is the listener. No it's not. The return value of privateMethods.highlighted(index) is the listener.
I have a list of event names which I need to listen to stored in an array, like so:
var events = ['A', 'B'];
Now, I'm unsure which event will be triggered first and it could be very inconsistent (depends on the HTTP requests that they await) so I can never safely listen to only one of them. So, I need to somehow "cross-listen" to all of them in order to trigger my original callback.
So my idea was to do the following:
Create a listener for A, which creates a listener for B. B's listener triggers the callback.
Create a listener for B, which creates a listener for A. A's listener triggers the callback.
So this would result in 4 listners, which would look something like this (pseudo-code):
var callback = function() { console.log('The callback'); };
Event.on('A', function () {
Event.on('B', callback);
});
Event.on('B', function () {
Event.on('A', callback);
});
So I believe this would solve my issue (there's probably another problem that I'm not seeing here though).
The issue is, I can make this work when there are only 2 events I need to listen to. But what about when I have 3-4 events I want to "cross-listen" to? Lets say we have ['A', 'B', 'C', 'D']. This would obviously require looping through the events. This part is what's confusing me and I'm not sure how to proceed. This would need to register a nice combination of events.
This needs to be done in JavaScript.
My imagination and logic is limited in this case.
I was thinking something like this:
var callback = function() { console.log('The callback'); };
var events = {
'click': false,
'mouseover': false,
'mouseout': false
};
for(prop in events) {
$('.evt-button').on(prop, function(evt) {
if(events[evt.type] === false) {
console.log('First ' + evt.type + ' event');
events[evt.type] = true;
checkAll();
}
});
}
function checkAll() {
var anyFalse = false;
for(prop in events) {
if(events.hasOwnProperty(prop)) {
if(events[prop] === false) {
anyFalse = true;
break;
}
}
}
if(!anyFalse) {
callback();
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<button class="evt-button">The button</button>
There's lots of ways to do what you're asking, but to keep it simple you could have an array of event names, as you already do, and simply remove them as they occur, checking to see if the array is empty each time. Like this...
var events = ['A', 'B'];
var callback = function() { console.log('The callback'); };
var eventOccured = function(eventName) {
// if the event name is in the array, remove it...
var idx = events.indexOf(eventName);
if (idx !== -1) {
events.splice(events.indexOf(idx), 1);
}
// if the event array is empty then we've handled everything...
calback();
};
Event.on('A', function () {
// do whatever you need in the event handler
eventOccured("A");
});
Event.on('B', function () {
// do whatever you need in the event handler
eventOccured("B");
});
A bit late, but you can make a function that wraps your callback and reuse it to attach multiple eventlisteners and count until all have occurred. This way you can also add events to cancel out others, say keyup cancels keydown.
const K = a => _b => a
const makeEventTrigger = (fn) => {
let lastTarget,
required = 0,
active = 0;
return (enable, qualifier = K(true)) => {
required = enable ? required + 1 : required;
return (event) => {
if(!qualifier(event)) {
return
}
const isLastTarget =
lastTarget && lastTarget.isEqualNode(event.currentTarget);
if (!enable) {
lastTarget = isLastTarget ? null : lastTarget;
active = Math.max(0, active - 1);
return;
}
if (!isLastTarget) {
lastTarget = event.currentTarget;
active = Math.min(required, active + 1);
return active === required && fn();
}
};
};
};
let changeTitleCol = () =>
(document.querySelector("h1").style.color =
"#" + Math.random().toString(16).slice(-6));
let addTriggerForColorChange = makeEventTrigger(changeTitleCol);
let isSpace = e => e.key === " " || e.code === "Space"
document
.querySelector("button")
.addEventListener("mousedown", addTriggerForColorChange(true));
document
.querySelector("button")
.addEventListener("mouseup", addTriggerForColorChange(false));
document.addEventListener("keydown", addTriggerForColorChange(true, isSpace));
document.addEventListener("keyup", addTriggerForColorChange(false));
<h1>Multiple Event sources!</h1>
<div>
<button id="trigger-A">klik me and press space</button>
</div>
I want to have a onkeydown event fire a function only once. for that function to fire again, the user has to release the key and press/hold again.
I know its fairly simple but I'm new at JS. Also I prefer to avoid using jQuery or other libs.
One more thing, this should work for both ie and firefox.
I'm surprised it's not mentioned, there's also event.repeat:
document.addEventListener('keydown', (e) => {
if (e.repeat) return;
console.log(e.key);
});
This will only fire once per each keypress, since event.repeat turns true after holding the key down.
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key#keyboardevent_sequence
You could set a flag:
var fired = false;
element.onkeydown = function() {
if(!fired) {
fired = true;
// do something
}
};
element.onkeyup = function() {
fired = false;
};
Or unbind and rebind the event handler (might be better):
function keyHandler() {
this.onkeydown = null;
// do something
}
element.onkeydown = keyHandler;
element.onkeyup = function() {
this.onkeydown = keyHandler;
};
More information about "traditional" event handling.
You might also want to use addEventListener and attachEvent to bind the event handlers. For more information about that, have a look at quirksmode.org - Advanced event registration models.
There's a "once" parameter you can use
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
Eg:
element.addEventListener('keydown', function(event) {
doSomething()
}, {once: true});
It'll remove it as soon as it's been called.
Alternatively you can use removeEventListener if it's a named function
Here is a method that uses addEventListener and removeEventListener
var textBox = document.getElementById("textBox");
function oneKeyDown(){
$("body").append("<h1>KeyDown<h1>"); //just to show the keypress
textBox.removeEventListener('keydown', oneKeyDown, false);
}
function bindKeyDown(){
textBox.addEventListener('keydown', oneKeyDown, false);
}
textBox.addEventListener('keyup', bindKeyDown, false)
bindKeyDown();
Code example on jsfiddle.
One note, for IE you will need to use attachEvent, detachEvent.
Here you go:
test.onkeydown = function() {
if ( this.className === 'hold' ) { return false; }
this.className = 'hold';
// call your function here
};
test.onkeyup = function() {
this.className = '';
};
Live demo: http://jsfiddle.net/simevidas/xAReL/2/
JQuery's one will help you.
What it does is, bind the eventHandler to event, and when event occurs, it runs the eventHandler and unbinds it, so that its not fired at next event.
as stated in the other answers, there is no 'onkeyfirstdown' or similar event to listen for.
the best solution is to keep track of which keys are already down in a js-object:
var keysdown = {};
element.addEventListener('keydown', function(evt) {
if(!(evt.key in keysdown)) {
keysdown[evt.key] = true;
// key first pressed
}
});
element.addEventListener('keyup', function(evt) {
delete keysdown[evt.key];
});
this way, you will not be skipping 'keyfirstpressed' events if more than one key is held down.
(many of the other solutions posted here will only fire when no other keys are down).
Here is my solution that will only run the function you pass it when a key is FIRST pressed on the target (eg window or some input field). If the user wants to trigger a key again, they'll have to release it and press it again.
Vanilla JS
const onKeyPress = (func, target = window) => {
// persistent "store" to track what keys are being pressed
let pressed = {};
// whenever a keydown event is fired ontarget element
const onKeyDown = (event) => {
// if key isn't already pressed, run func
if (!pressed[event.which])
func(event);
// add key to store
pressed = { ...pressed, [event.which]: true };
};
// whenever a keyup event is fired on the window element
const onKeyUp = (event) => {
const { [event.which]: id, ...rest } = pressed;
// remove key from store
pressed = rest;
};
// add listeners
target.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
// return a function that can be called to remove listeners
return () => {
target.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
};
And then to use it:
const removeListener = onKeyPress((event) => console.log(event.which + ' key pressed'))
removeListener(); // when you want to remove listeners later
React and React Hooks
import { useState } from 'react';
import { useEffect } from 'react';
import { useCallback } from 'react';
export const useKeyPress = (func, target = window) => {
// persistent "store" to track what keys are being pressed
const [pressed, setPressed] = useState({});
// whenever a keydown event is fired ontarget element
const onKeyDown = useCallback(
(event) => {
// if key isn't already pressed, run func
if (!pressed[event.which])
func(event);
// add key to store
setPressed({ ...pressed, [event.which]: true });
},
[func, pressed]
);
// whenever a keyup event is fired on the window element
const onKeyUp = useCallback((event) => {
// remove key from store
const { [event.which]: id, ...rest } = pressed;
setPressed(rest);
}, [pressed]);
useEffect(() => {
// add listeners when component mounts/changes
target.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
// cleanup/remove listeners when component unmounts/changes
return () => {
target.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
}, [target, onKeyDown, onKeyUp]);
};
And then to use it:
import { useKeyPress } from 'wherever';
useKeyPress((event) => console.log(event.which + ' key pressed'))
I want to have a onkeydown event fire a function only once. for that function to fire again, the user has to release the key and press/hold again.
I know its fairly simple but I'm new at JS. Also I prefer to avoid using jQuery or other libs.
One more thing, this should work for both ie and firefox.
I'm surprised it's not mentioned, there's also event.repeat:
document.addEventListener('keydown', (e) => {
if (e.repeat) return;
console.log(e.key);
});
This will only fire once per each keypress, since event.repeat turns true after holding the key down.
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key#keyboardevent_sequence
You could set a flag:
var fired = false;
element.onkeydown = function() {
if(!fired) {
fired = true;
// do something
}
};
element.onkeyup = function() {
fired = false;
};
Or unbind and rebind the event handler (might be better):
function keyHandler() {
this.onkeydown = null;
// do something
}
element.onkeydown = keyHandler;
element.onkeyup = function() {
this.onkeydown = keyHandler;
};
More information about "traditional" event handling.
You might also want to use addEventListener and attachEvent to bind the event handlers. For more information about that, have a look at quirksmode.org - Advanced event registration models.
There's a "once" parameter you can use
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
Eg:
element.addEventListener('keydown', function(event) {
doSomething()
}, {once: true});
It'll remove it as soon as it's been called.
Alternatively you can use removeEventListener if it's a named function
Here is a method that uses addEventListener and removeEventListener
var textBox = document.getElementById("textBox");
function oneKeyDown(){
$("body").append("<h1>KeyDown<h1>"); //just to show the keypress
textBox.removeEventListener('keydown', oneKeyDown, false);
}
function bindKeyDown(){
textBox.addEventListener('keydown', oneKeyDown, false);
}
textBox.addEventListener('keyup', bindKeyDown, false)
bindKeyDown();
Code example on jsfiddle.
One note, for IE you will need to use attachEvent, detachEvent.
Here you go:
test.onkeydown = function() {
if ( this.className === 'hold' ) { return false; }
this.className = 'hold';
// call your function here
};
test.onkeyup = function() {
this.className = '';
};
Live demo: http://jsfiddle.net/simevidas/xAReL/2/
JQuery's one will help you.
What it does is, bind the eventHandler to event, and when event occurs, it runs the eventHandler and unbinds it, so that its not fired at next event.
as stated in the other answers, there is no 'onkeyfirstdown' or similar event to listen for.
the best solution is to keep track of which keys are already down in a js-object:
var keysdown = {};
element.addEventListener('keydown', function(evt) {
if(!(evt.key in keysdown)) {
keysdown[evt.key] = true;
// key first pressed
}
});
element.addEventListener('keyup', function(evt) {
delete keysdown[evt.key];
});
this way, you will not be skipping 'keyfirstpressed' events if more than one key is held down.
(many of the other solutions posted here will only fire when no other keys are down).
Here is my solution that will only run the function you pass it when a key is FIRST pressed on the target (eg window or some input field). If the user wants to trigger a key again, they'll have to release it and press it again.
Vanilla JS
const onKeyPress = (func, target = window) => {
// persistent "store" to track what keys are being pressed
let pressed = {};
// whenever a keydown event is fired ontarget element
const onKeyDown = (event) => {
// if key isn't already pressed, run func
if (!pressed[event.which])
func(event);
// add key to store
pressed = { ...pressed, [event.which]: true };
};
// whenever a keyup event is fired on the window element
const onKeyUp = (event) => {
const { [event.which]: id, ...rest } = pressed;
// remove key from store
pressed = rest;
};
// add listeners
target.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
// return a function that can be called to remove listeners
return () => {
target.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
};
And then to use it:
const removeListener = onKeyPress((event) => console.log(event.which + ' key pressed'))
removeListener(); // when you want to remove listeners later
React and React Hooks
import { useState } from 'react';
import { useEffect } from 'react';
import { useCallback } from 'react';
export const useKeyPress = (func, target = window) => {
// persistent "store" to track what keys are being pressed
const [pressed, setPressed] = useState({});
// whenever a keydown event is fired ontarget element
const onKeyDown = useCallback(
(event) => {
// if key isn't already pressed, run func
if (!pressed[event.which])
func(event);
// add key to store
setPressed({ ...pressed, [event.which]: true });
},
[func, pressed]
);
// whenever a keyup event is fired on the window element
const onKeyUp = useCallback((event) => {
// remove key from store
const { [event.which]: id, ...rest } = pressed;
setPressed(rest);
}, [pressed]);
useEffect(() => {
// add listeners when component mounts/changes
target.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
// cleanup/remove listeners when component unmounts/changes
return () => {
target.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
}, [target, onKeyDown, onKeyUp]);
};
And then to use it:
import { useKeyPress } from 'wherever';
useKeyPress((event) => console.log(event.which + ' key pressed'))