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
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);
const ProductScreen = () => {
const [qty, setQty] = useState(0);
const handleAddtoCart = () => {
console.log(qty);
};
return (
<div className="productScreen">
{product.countInStock > 0 && (
<div className="productScreen__details__qty">
<span>Qty : </span>
<select
id="qty"
name="qty"
value={qty}
onChange={(e) => setQty(e.target.value)}
>
{[...Array(product.countInStock).keys()].map((x) => (
<option key={x + 1} value={x + 1}>
{x + 1}
</option>
))}
</select>
</div>
)}
{product.countInStock > 0 ? (
<div className="productScreen__details__price__details__cart">
<button
className="productScreen__details__price__details__toCart"
onClick={handleAddtoCart()}
>
Add to Cart
</button>
</div>
</div>
</div>
);
};
Here handleAddtoCart gets triggered when selecting options but doesnt trigger when button is pressed(handleAddtoCart is added to button), when I change handleAddtoCart() to handleAddtoCart in onClick attribute of button it works properly.
Why when handleAddtoCart() is given as onclick attribute it is getting triggered by adjacent select option and is not getting triggered when button is pressed
You need to make a callback to that function, because every render of the component, will literally execute handleAddtoCart() and not as you expect it to happen only of the onClick trigger.
as react's Offical documents explained:
To save typing and avoid the confusing behavior of this, we will use the arrow function syntax for event handlers here and further below:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => console.log('click')}>
{this.props.value}
</button>
);
}
}
Notice how with onClick={() => console.log('click')}, we’re passing a function as the onClick prop. React will only call this function after a click. Forgetting () => and writing onClick={console.log('click')} is a common mistake, and would fire every time the component re-renders.
for more details:
https://reactjs.org/tutorial/tutorial.html
Change
onClick={handleAddtoCart()}
by
onClick={handleAddtoCart}
Also try with :
onChange={(e) => setQty(e.currentTarget.value)}
instead of :
onChange={(e) => setQty(e.target.value)}
The currentTarget read-only property of the Event interface identifies
the current target for the event, as the event traverses the DOM. It
always refers to the element to which the event handler has been
attached, as opposed to Event.target, which identifies the element on
which the event occurred and which may be its descendant.
https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget
function Example() {
const containerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const targetElement = containerRef?.current
const stopPropagation = (e: MouseEvent) => {
console.log('propagation stopped at parent div')
e.stopPropagation()
}
targetElement?.addEventListener("click", stopPropagation);
return () =>
targetElement?.removeEventListener('click', stopPropagation)
}, []);
return (
<div ref={containerRef}>
<div onClick={() => console.log('child')}>
<div onClick={() => console.log('grandchild')}>
<button>click</button>
</div>
</div>
</div>
);
}
In the above snippet, 'grandchild' and 'child' should appear in the console first, respectively, after clicking the button. However, stopPropagation() handler gets invoked before both onClick inline-handlers (which are no longer executed after .stopPropagation().
In my understanding, the click event should bubble from:
button -> grandchild -> child -> containerRef (event stops bubbling from here)
Does the .addEventListener behave differently in contrast to onClick attributes? It seems that in the snippet above, the .addEventListener handlers get invoked first before onClick handlers do.
For a demo: https://codesandbox.io/s/kind-leaf-9vnrq
I've tried doing something similar with HTML: https://codesandbox.io/s/inspiring-wildflower-cf0jx
<div id="parent">
<div id="child" onclick="window.alert('child');">
<div id="grandchild" onclick="window.alert('grandchild');">
<button onclick="window.alert('button')">
click
</button>
</div>
</div>
</div>
<script>
const parent = document.getElementById("parent");
parent.addEventListener("click", function(e) {
e.stopPropagation();
window.alert("parent");
});
</script>
In the above snippet, it works just as intended, click bubbles from:
button -> grandchild -> child -> parent (event stops bubbling from here)
React uses SyntheticEvent mechanism handled by React itself. Which means that the real event has already propagated by the time you interact with it in React. Therefore, you cannot mix SyntheticEvents with regular JS events, because it leads to unexpected behaviors.
What you can do is stop the propagation of SyntheticEvent in your component
...
// onClick={(e) => e.stopPropagation()} instead of addEventListener
return (
<div ref={containerRef} onClick={(e) => e.stopPropagation()}>
<div onClick={() => console.log('child')}>
<div onClick={() => console.log('grandchild')}>
<button>click</button>
</div>
</div>
</div>
);
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
}