I have a button with onClick event attached to it:
<button id="my-button" onClick={myMethod}>
My button
</button>
I have also added an event listener to this button:
const listener = (e) => {
// Do something here (or elsewhere) to prevent `myMethod` from being invoked
console.log('Hello, world!');
}
const options = { capture: true, once: true };
document.getElementById('my-button')
.addEventListener('click', listener, options);
Is it possible to add some method inside the listener, so the myMethod is stopped from being invoked?
Combining React event handling and raw DOM event handling usually indicates a larger design issue. Having the one conflict with the other even more so. :-)
Having said that, React's event handlers use delegation, so the standard e.stopPropagation should do it:
const listener = (e) => {
e.stopPropagation();
console.log('Hello, world!');
};
Example:
function myMethod() {
console.log("myMethod");
}
const Example = () => <button id="my-button" onClick={myMethod}>
My button
</button>;
ReactDOM.render(
<Example />,
document.getElementById("root")
);
const listener = (e) => {
// Do something here (or elsewhere) to prevent `myMethod` from being invoked
e.stopPropagation();
console.log('Hello, world!');
}
const options = { capture: true, once: true };
document.getElementById('my-button')
.addEventListener('click', listener, options);
<div id="root"></div>
<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>
Note that you'll need to re-attach your event handler every time React re-renders the component. This is part of why mixing these two systems is generally not your best approach.
Related
I'm working with Lit Element and I'm trying add an event listener on 'Click' that will a variable state that will set the dropdown to be expand or not. But once the drop down is 'closed' I want to remove that event to avoid unnecessary event calls on 'Click.
Adding the event works great but I cannot remove it.
Here is the idea:
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("_tenantsExpanded")) {
document.removeEventListener("click", (ev) => this._eventLogic(ev, this));
if (this._tenantsExpanded)
document.addEventListener("click", (ev) => this._eventLogic(ev, this));
}
}
The fct logic:
private _eventLogic(e: MouseEvent, component: this) {
const targets = e.composedPath() as Element[];
if (!targets.some((target) => target.className?.includes("tenant"))) {
component._tenantsExpanded = false;
}
}
Code in my render's function:
${this._tenantsExpanded
? html` <div class="tenants-content">${this._tenantsContent()}</div> `
: html``}
Important note: I want the click event to be listened on all the window, not just the component itself. The same for removing the event.
PS: I don't know why e.currentTaget.className doesn't give me the actual className, but results to an undefined.
When you use removeEventListener you have to pass a reference to the same function you used when adding the listener.
In this example the function is stored in fn.
(You might have to change the this reference here, it depends a bit on your whole component).
const fn = (ev) => this._eventLogic(ev, this);
document.addEventListener("click", fn);
document.removeEventListener("click", fn);
I have a cell renderer that returns the name property and objects on a row:
const nameRenderer = ({ value, data }) => {
const { id: queueId } = data;
return (
<Box>
<div className="row-hidden-menu">
<IconButton
icon="menu"
onClick={({ event }) => {
event.preventDefault();
event.stopPropagation();
onMenuClick();
}}
/>
</div>
</Box>
);
};
The issue I have is that I have an onRowClick function but I don't want that function to be called when I click the icon from the nameRenderer. Right now when the menu opens, the onRowClicked event navigates to a new page.
See this answer for more in-depth explanation, but the gist is that the event that you receive from the onClick callback is React's synthetic event which is a wrapper of the native event. Calling stopPropagation() from a synthetic event will not stop the real event from bubbling up and it is a quirk of the React framework for a long time.
Solution: attach your onClick event handler to the real DOM element instead.
function ButtonCellRenderer() {
return (
<button
ref={(ref) => {
if (!ref) return;
ref.onclick = (e) => {
console.log("native event handler");
e.stopPropagation(); // this works
// put your logic here instead because e.stopPropagation() will
// stop React's synthetic event
};
}}
onClick={(e) => {
e.stopPropagation(); // this doesn't work
}}
>
Click me
</button>
);
}
Live Demo
In javascript, I have a class that has a scroll event that works well. Multiple console.log('OK scroll') displayed.
const PaginatorInfiniteScroll = class {
constructor(paginationElement) {
...
window.addEventListener('scroll', () => {
this.scroll()
})
}
scroll()
{
console.log('OK scroll')
...
}
endScroll()
{
window.removeEventListener('scroll', () => {
console.log('OK remove')
this.scroll()
}, true)
}
...
But when I no longer need this event, I want to delete it.
When I execute this.endScroll() I have good console.log('OK remove') displayed but I still have the console.log('OK scroll') that appears when I scroll.
I think I am performing removeEventListener('scroll') badly but don't know how to do it
You just need to pass the name of the event for which you need to remove the listener. This will remove all the event listeners for scroll event.
window.removeEventListener('scroll');
Edit:
If you only want to remove specific listener for the scroll event, you need to save the callback function in some variable and pass that variable to removeEventListener function
const callback = () => {
console.log('OK Scroll');
}
window.addEventListener('scroll', callback); // add scroll event listener
window.removeEventListener('scroll', callback); // remove scroll event listener
Since dispatchEvent, as per the docs, will apply the:
normal event processing rules (including the capturing and optional
bubbling phase)
I'm looking for something similar but with a way to skip this process and trigger the event directly on the element. To trigger the default element event behavior while bypassing the processing stage.
As in, to capture the event at window level (before it reaches the other capture triggers) and pass it straight to the component (text area) invoking it directly.
(For example to trigger the default keydown of a text area without going through the hierarchy)
I've been trying to do it like this but if there is another event at window level this will not work:
window.addEventListener("keydown", this.keyDown, true);
keyDown = (event) => {
event.preventDefault();
event.nativeEvent && event.nativeEvent.stopImmediatePropagation();
event.stopImmediatePropagation && event.stopImmediatePropagation();
event.stopPropagation();
// Pass event straight to the element
return false;
};
I'm looking to trigger the default element event behavior while bypassing the processing
There may well be a more elegant way to do this, but one option is to remove the element from the DOM first, dispatch the event to it, then put it back into the DOM:
document.body.addEventListener('keydown', () => {
console.log('body keydown capturing');
}, true);
document.body.addEventListener('keydown', () => {
console.log('body keydown bubbling');
});
const input = document.querySelector('input');
input.addEventListener('keydown', () => {
console.log('input keydown');
});
const node = document.createTextNode('');
input.replaceWith(node);
input.dispatchEvent(new Event('keydown'));
node.replaceWith(input);
<input>
Since the element isn't in the DOM when the event is dispatched, the elements which used to be its ancestors won't see the event.
Note that events dispatched to detached elements do not capture/bubble regardless, not even to parents or children of element the event was dispatched to.
Without removing the element from the DOM entirely beforehand, if the input can exist in a shadow DOM, you can also dispatch an event to it there, and the event won't capture down or bubble up (though user input, not being custom events, will propagate through):
document.body.addEventListener('keydown', () => {
console.log('body keydown capturing');
}, true);
document.body.addEventListener('keydown', () => {
console.log('body keydown bubbling');
});
outer.attachShadow({mode: 'open'});
const input = document.createElement('input');
outer.shadowRoot.append(input);
input.addEventListener('keydown', () => {
console.log('input keydown');
});
input.dispatchEvent(new Event('keydown'));
<div id="outer"></div>
Another approach would be to call stopPropagation and stopImmediatePropagation in the capturing phase, at the very beginning, when the event is at the window, and then manually call the listener function you want. Make sure to attach your window listener before any other scripts on the page run, to make sure none of the page's listeners can see the event first:
// Your script:
const input = document.body.appendChild(document.createElement('input'));
input.className = 'custom-extension-element';
const handler = (e) => {
setTimeout(() => {
console.log(e.target.value);
});
};
window.addEventListener(
'keydown',
(e) => {
if (e.target.closest('.custom-extension-element')) {
e.stopImmediatePropagation(); // Stop other capturing listeners on window from seeing event
e.stopPropagation(); // Stop all other listeners
handler(e);
}
},
true // add listener in capturing phase
);
// Example page script
// which tries to attach listener to window in capturing phase:
window.addEventListener(
'keydown',
(e) => {
console.log('page sees keydown');
},
true
);
document.body.addEventListener('keydown', () => {
console.log('body keydown capturing');
}, true);
document.body.addEventListener('keydown', () => {
console.log('body keydown bubbling');
});
The best way around propagation issues is to just execute the function:
function tester(){
console.log("Just fire the function! Don't dispatchEvent!");
}
tester();
document.getElementById('test').onkeyup = function(e){
e.stopPropagation();
console.log(this.id); console.log(e.target); tester();
}
document.body.onkeyup = ()=>{
console.log("This shouldn't fire when on #test");
}
<input id='test' type='text' value='' />
I have an onchange event for a field that needs to be debounced, I'm using underscore for that, however when I use the debouncer the event that is passed to the React handler appears to be out of date.
<div className='input-field'>
<input onChange={_.debounce(this.uriChangeHandler.bind(this), 500)} id='source_uri' type='text' name='source_uri' autofocus required />
<label htmlFor='source_uri'>Website Link</label>
</div>
uriChangeHandler(event) {
event.preventDefault();
let uriField = $(event.target);
let uri = uriField.val();
this.setState({
itemCreateError: null,
loading: true
});
this.loadUriMetaData(uri, uriField);
}
I'm getting this error:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're calling preventDefault on a released/nullified synthetic event. This is a no-op. See https://fb.me/react-event-pooling for more information.
Using the onchange without the debouncer works fine.
I ended up with a solution I saw on github which worked well for me. Basically you wrap the debounce function in a custom function debounceEventHandler which will persist the event before returning the debounced function.
function debounceEventHandler(...args) {
const debounced = _.debounce(...args)
return function(e) {
e.persist()
return debounced(e)
}
}
<Input onChange={debounceEventHandler(this.handleInputChange, 150)}/>
This got rid of the synthetic event warning
in yout case it might help
class HelloWorldComponent extends React.Component {
uriChangeHandler(target) {
console.log(target)
}
render() {
var myHandler = _.flowRight(
_.debounce(this.uriChangeHandler.bind(this), 5e2),
_.property('target')
);
return (
<input onChange={myHandler} />
);
}
}
React.render(
<HelloWorldComponent/>,
document.getElementById('react_example')
);
JSBin
Also you can use _.clone instead of _.property('target') if you want to get the complete event object.
EDITED
To prevent React nullifies the event you must call event.persist() as stated on React doc:
If you want to access the event properties in an asynchronous way, you should call event.persist() on the event, which will remove the synthetic event from the pool and allow references to the event to be retained by user code.
And hence you could use e => e.persist() || e instead of _.clone
JSBin
I went with a combination of xiaolin's answer and useMemo:
const MyComponent = () => {
const handleChange = useMemo(() => {
const debounced = _.debounce(e => console.log(e.target.value), 1000);
return e => {
e.persist();
return debounced(e);
};
}, []);
return <input onChange={handleChange} />;
};
What I think is happening is that the event is being nullified in the time in between the actual event and when your method gets called. Looking at the _.debounce source code (and using what we know about debouncing functions) will tell you that your method isn't called until 500 milliseconds until after the event fires. So you've got something like this going on:
Event fires
_.debounce() sets a 500 millisecond timeout
React nullifies the event object
The timer fires and calls your event handler
You call event.stopPropagation() on a nullified event.
I think you have two possible solutions: call event.stopPropagation() every time the event fires (outside of the debounce), or don't call it at all.
Side note: this would still be a problem even with native events. By the time your handler actually gets called, the event would have already propagated. React is just doing a better job at warning you that you've done something weird.
class HelloWorldComponent extends Component {
_handleInputSearchChange = (event) => {
event.persist();
_.debounce((event) => {
console.log(event.target.value);
}, 1000)(event);
};
render() {
return (
<input onChange={this._handleInputSearchChange} />
);
}
}
The idea here is that we want the onChange handler to persist the event first then immediately debounce our event handler, this can be simply achieved with the following code:
<input
onChange={_.flowRight(
_.debounce(this.handleOnChange.bind(this), 300),
this.persistEvent,
)}
</input>
persistEvent = e => {
e.persist();
e.preventDefault();
return e;
};
handleOnChange = e => {
console.log('event target', e.target);
console.log('state', this.state);
// here you can add you handler code
}