This question already has answers here:
Prevent onClick event by clicking on a child div
(2 answers)
How to call stopPropagation in reactjs?
(2 answers)
Closed 5 years ago.
Can someone help explain how to stop event propagation on a click? I've read many other posts, but I still can't figure it out.
I have a grid of 40x50 boxes, and when I click one I'd like to see that box's id. Currently, when I click it bubbles up and returns Board as what's been clicked. So I need to stop the propagation, right? Where/how do I do that? I've tried passing i.stopPropagation(); in the handleClick() method, but it tells me that i.stopPropagation(); isn't a function.
function Square(props) {
return (
<div className="square" id={props.id} onClick={props.onClick}/>
);
}
class Board extends Component {
rowsByColumns(rows, columns) {
let arr=[];
let k=0;
let m=0
for (let i=0;i<rows;i++) {
arr.push([])
for (let j=0;j<columns;j++) {
arr[i].push(<Square key={"square"+m++} id={"square"+m} living={false} onClick={() => this.props.onClick(this)}/>)
}
}
let newArr = arr.map(row => <Row key={"row"+k++}>{row}</Row>);
return (newArr);
}
render() {
return (
<div id="board">
{this.rowsByColumns(40,50)}
</div>
);
}
}
class App extends Component {
constructor() {
super();
this.state = {
alive: ["square1"],
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(i) {
console.log(i);
this.setState({
alive: this.state.alive.concat([i])
})
}
render() {
return (
<div className="App container-fluid">
<main className="row justify-content-center">
<Board alive={this.state.alive} onClick={i => this.handleClick()}/>
</div>
</main>
</div>
);
}
}
Your click handler receives an event object. Use stopPropagation on it:
handleClick(e) {
e.stopPropagation();
}
then in your onClick:
onClick={this.handleClick}
Live example — the child stops every other click it sees:
class Parent extends React.Component {
constructor(...args) {
super(...args);
this.handleClick = () => {
console.log("Parent got the click");
};
}
render() {
return <div onClick={this.handleClick}>
Click here to see parent handle it.
<Child />
</div>;
}
}
class Child extends React.Component {
constructor(...args) {
super(...args);
this.stopClick = true;
this.handleClick = e => {
if (this.stopClick) {
e.stopPropagation();
console.log("Child got the click and stopped it");
} else {
console.log("Child got the click and didn't stop it");
}
this.stopClick = !this.stopClick;
};
}
render() {
return <div onClick={this.handleClick}>I'm the child</div>;
}
}
ReactDOM.render(
<Parent />,
document.getElementById("root")
);
<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>
Related
It is my understanding that refs are not defined outside the react lifecycle (source). The problem I am trying to solve is to capture a key press at the document level (i.e. trigger the event no matter what element is in focus), and then interact with a react ref. Below is a simplified example of what I am trying to do:
export default class Console extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
text: "",
};
}
print(output: string) {
this.setState({
text: this.state.text + output + "\n"
})
}
toggleVisible()
{
this.setState({visible: !this.state.visible});
}
render() {
const footer_style = {
display: this.state.visible ? "inline" : "none",
};
return (
<footer id="console-footer" className="footer container-fluid fixed-bottom" style={footer_style}>
<div className="row">
<textarea id="console" className="form-control" rows={5} readOnly={true}>{this.state.text}</textarea>
</div>
</footer>
);
}
}
class App extends React.Component {
private console: Console;
constructor() {
super({});
this.console = React.createRef();
}
keyDown = (e) =>
{
this.console.current.toggleVisible(); // <-- this is undefined
}
componentDidMount(){
document.addEventListener("keydown", this.keyDown);
},
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDown);
},
render() {
return (
<div className="App" onKeyDown={this.keyDown}> // <-- this only works when this element is in focus
// other that has access to this.console that will call console.print(...)
<Console ref={this.console} />
</div>
);
}
}
My question is: is there a way to have this sort of document level key press within the lifesycle of react so that the ref is not undefined inside the event handler keyDown? I've seen a lot of solutions that involve setting the tabIndex and hacking to make sure the proper element has focus at the right time, but these do not seem like robust solutions to me.
I'm just learning React so maybe this is a design limitation of React or I am not designing my components properly. But this sort of functionality seems quite basice to me, having the ability to pass components from one to the other and call methods on eachother.
You're calling the onKeyDown callback twice, once on document and once on App.
Events bubble up the tree.
When the textarea is not in focus, only document.onkeydown is called.
When it is in focus, both document.onkeydown and App's div.onkeydown are called, effectively cancelling the effect(toggling state off and back on).
Here's a working example: https://codesandbox.io/s/icy-hooks-8zuy7?file=/src/App.js
import React from "react";
class Console extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
text: ""
};
}
print(output: string) {
this.setState({
text: this.state.text + output + "\n"
});
}
toggleVisible() {
this.setState({ visible: !this.state.visible });
}
render() {
const footer_style = {
display: this.state.visible ? "inline" : "none"
};
return (
<footer
id="console-footer"
className="footer container-fluid fixed-bottom"
style={footer_style}
>
<div className="row">
<textarea id="console" className="form-control" rows={5} readOnly>
{this.state.text}
</textarea>
</div>
</footer>
);
}
}
export default class App extends React.Component {
constructor(props) {
super(props);
this.console = React.createRef();
}
keyDown = (e) => {
this.console.current.toggleVisible(); // <-- this is undefined
};
componentDidMount() {
document.addEventListener("keydown", this.keyDown);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDown);
}
render() {
return (
<div className="App" style={{ backgroundColor: "blueviolet" }}>
enter key to toggle console
<Console ref={this.console} />
</div>
);
}
}
Also, I recommend using react's hooks:
export default App = () => {
const console = React.createRef();
const keyDown = (e) => {
console.current.toggleVisible(); // <-- this is undefined
};
React.useEffect(() => {
// bind onComponentDidMount
document.addEventListener("keydown", keyDown);
// unbind onComponentDidUnmount
return () => document.removeEventListener("keydown", keyDown);
});
return (
<div className="App" style={{ backgroundColor: "blueviolet" }}>
press key to toggle console
<Console ref={console} />
</div>
);
};
I have an onclick handler:
render() {
return (
<div>
<div className={className} onClick={this.target.bind(this,id)}>{name}</div>
</div>
)
}
and here is the function:
target(test, event) {
event.target.className="addClasss";
}
I was trying the above way to addClass on event target, however is there a better way to do this?
Thanks
Instead of adding a class manually to a DOM element, you can e.g. add an additional state variable that keeps track of the element that has been clicked. You can then use this in the render method to choose which element that should get the class added to it.
Example
class App extends React.Component {
state = {
arr: [{ id: 1, name: "foo" }, { id: 2, name: "bar" }],
clicked: null
};
target(id) {
this.setState({ clicked: id });
}
render() {
const { arr, clicked } = this.state;
return (
<div>
{arr.map(element => (
<div
className={clicked === element.id && "addClass"}
onClick={this.target.bind(this, element.id)}
>
{element.name} {clicked === element.id && "clicked!"}
</div>
))}
</div>
);
}
}
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>
class App extends React.Component {
state = {
clicked: false,
};
handleClick = () => {
this.setState({ clicked: true });
}
render() {
const { clicked } = this.state;
const className = clicked ? "newClass" : "";
const name = "add newClass";
return (
<div>
<div className={className} onClick={this.handleClick}>{name}</div>
</div>
);
}
}
A little late but for future searchers: use jquery:
target(test, event) {
var element = $(event.target)
element.addClass('your class');
}
OBS: I agree partially with the answers above, react was made to work with states but there are some cases that we don't want to care about controlling every single aspect of our code with states, specially when there are a repetitive class, so I found easier to target these elements with jquery.
What I have
I have a class that's not being exported but being used internally by a file. (The classes are all in the same file)
class SearchResults extends Component {
constructor()
{
super();
this.fill_name = this.fill_name.bind(this);
}
fill_name(event, name)
{
console.log("search results", event.target);
this.props.fill_name(name, event.target);
}
render()
{
return (
<div className="search-results-item" onClick={ () => this.fill_name(event, this.props.name)}>
<div className="highlight">
{this.props.name}
</div>
</div>
)
}
}
I'm trying to get the <div> element to be sent back to the parent, which is defined below (skipping irrelevant stuff):
class PublishComponent extends Component {
fill_name(name, me)
{
console.log(me);
$("#company_input").val(name);
this.setState({ list: { ...this.state.list, company: [] } });
}
}
me is the event.
What I'm getting
The console posts the following:
search results <react></react>
undefined
so the event.target is <react></react> for some reason, while the me is getting undefined.
Expected behaviour
It should return the element i.e. <div className="search-results-item"...></div>
You are not passing the event object
Change This
<div
className="search-results-item"
onClick={() => this.fill_name(event, this.props.name)}
/>
To This
<div
className="search-results-item"
// pass the event here
onClick={event => this.fill_name(event, this.props.name)}
/>
This should work for you:
class SearchResults extends Component {
constructor() {
super();
this.fill_name = this.fill_name.bind(this);
}
fill_name() {
this.props.fill_name(this.props.name, this.ref)
}
render() {
return (
<div className="search-results-item" ref={ref => { this.ref = ref }} onClick={this.fill_name}>
<div className="highlight">
{this.props.name}
</div>
</div>
)
}
}
I have a volume element that shows the volume bar when the user hovers over it. This all works great in desktop. However to get the same functionality on mobile, the user has clicks on the volume element which also toggles the mute click event.
I am wanting to stop that mute event when the user clicks (i.e taps) on that element on mobile.
I don't want to modify the Mute or VolumeBar classes to fix this because these are generic classes in my library that the developer uses.
https://jsfiddle.net/jwm6k66c/2145/
Actual: The mute click event gets fired and the volume bar opens.
Expected: The mute click event doesn't gets fired and the volume bar opens.
Open the console -> go into mobile view (CTRL + SHIFT + M on chrome) -> click the volume button and observe the console logs.
What I have tried:
Using volumeControlOnClick to stop propogation when the volume bar's height is 0 (i.e not visible), this does not cancel the onClick though.
What I want:
To cancel the mute click event if the user clicks on the volume icon for the first time in mobile. Instead it should only show the volume bar.
const volumeControlOnClick = (e) => {
const volumeBarContainer =
e.currentTarget.nextElementSibling.querySelector('.jp-volume-bar-container');
/* Stop propogation is for mobiles to stop
triggering mute when showing volume bar */
if (volumeBarContainer.clientHeight === 0) {
e.stopPropagation();
console.log("stop propogation")
}
};
class Volume extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="jp-volume-container">
<Mute onTouchStart={volumeControlOnClick}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onClick={() => console.log("mute toggled")} onTouchStart={this.props.onTouchStart}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
React.render(<Volume />, document.getElementById('container'));
Here's what I ended up with. It works because onTouchStart is always calld before onClick if it's a touch event and if not's then the custom logic gets called anyway. It also fires before the hover has happened. This preserves the :hover event. e.preventDefault() did not.
let isVolumeBarVisible;
const onTouchStartMute = e => (
isVolumeBarVisible = e.currentTarget.nextElementSibling
.querySelector('.jp-volume-bar-container').clientHeight > 0
);
const onClickMute = () => () => {
if (isVolumeBarVisible !== false) {
// Do custom mute logic
}
isVolumeBarVisible = undefined;
};
<Mute
aria-haspopup onTouchStart={onTouchStartMute}
onClick={onClickMute}
>
<i className="fa">{/* Icon set in css*/}</i>
</Mute>
What you can do is use a flag to indicate you were in a touch event before being in your mouse event, as long as you are using bubble phase. So attach a listener to your container element like this:
let isTouch = false;
const handleContainerClick = () => isTouch = false;
const handleMuteClick = () => {
if (isTouch == false) {
console.log("mute toggled");
}
};
const volumeControlOnClick = () => {
isTouch = true;
};
class Volume extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="jp-volume-container" onClick={handleContainerClick}>
<Mute onTouchStart={volumeControlOnClick} onClick={handleMuteClick}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onTouchStart={this.props.onTouchStart} onClick={this.props.onClick}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
render(<Volume />, document.getElementById('container'));
If you are not using bubble phase so you can register a timeout of 100ms with the same logic above, where after 100ms you make your flag variable false again. Just add to your touchStart handler:
setTimeout(() => {isTouch = false}, 100);
EDIT: Even though touch events are supposed to be passive by default in Chrome 56, you call preventDefault() from a touchEnd event to prevent the click handler from firing. So, if you can't modify the click handler of your Mute class in any way, but you can add a touchEnd event, than you could do:
const handleTouchEnd = (e) => e.preventDefault();
const volumeControlOnClick = () => console.log("volumeControlOnClick");
class Volume extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="jp-volume-container">
<Mute onTouchStart={volumeControlOnClick} onTouchEnd={handleTouchEnd}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onTouchStart={this.props.onTouchStart} onTouchEnd={this.props.onTouchEnd} onClick={() => console.log("mute toggled")}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
render(<Volume />, document.getElementById('container'));
Try this
render() {
return (
<div className="jp-volume-container" onClick={handleContainerClick}>
<Mute onTouchStart={volumeControlOnClick} onClick={handleMuteClick}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onTouchStart={this.props.onTouchStart} onClick={this.props.onClick}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
render(<Volume />, document.getElementById('container'));
I am building a menubar using React JS. On clicking a menu item, respective megamenu should open. On clicking outside of the megamenu, it should close itself. I have made upto this by toggling the state of the megamenu. But I also want to close the megamenu, when the menu-item is clicked second-time(i.e. toggle close and open of megamenu by clicking menu-item). I am stuck here, on clicking the menu-item second time, the state is not toggling back.
class Menubar extends React.Component {
constructor() {
super();
this.state = {
clicked: false
};
this.handleClick = this.handleClick.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
}
componentWillMount() {
document.addEventListener('click', this.handleOutsideClick, false);
}
componentWillUnmount(){
document.removeEventListener('click', this.handleOutsideClick, false);
}
handleClick() {
this.setState({clicked: !this.state.clicked});
}
handleOutsideClick(){
if (this.refs.megaMenu.contains(event.target)) {
} else {
this.setState({
clicked: false
});
}
}
render() {
return (
<div className="container">
<div className="menu-bar">
{/* Menu*/}
<div className="menu-bar-item">
<a className="menu-bar-link" href="#" onClick={this.handleClick}>Points</a>
<div className={"mega-menu"+" "+this.state.clicked} ref="megaMenu">
<div className="mega-menu-content">
<p>Points Menu</p>
</div>
</div>
</div>
</div>
</div>
);
}
}
ReactDOM.render(
<Menubar />,
document.getElementById('example')
);
Codepen Demo
You need to move ref="megaMenu" so it includes button as well, otherwise when you click on button handleOutsideClick is also triggered and you flip this.state.clicked twice. Also, you forgot to pass event in handleOutsideClick handler.
class Menubar extends React.Component {
constructor() {
super();
this.state = {
clicked: false
};
this.handleClick = this.handleClick.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
}
componentWillMount() {
document.addEventListener('click', this.handleOutsideClick, false);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleOutsideClick, false);
}
handleClick() {
this.setState({clicked: !this.state.clicked});
}
handleOutsideClick(event) {
if (!this.menu.contains(event.target)) {
this.setState({
clicked: false
});
}
}
render() {
return (
<div className="container">
<div className="menu-bar">
{/* Menu*/}
<div className="menu-bar-item" ref={el => this.menu = el}>
<a className="menu-bar-link" href="#" onClick={this.handleClick}>Points</a>
<div className={"mega-menu" + " " + this.state.clicked}>
<div className="mega-menu-content">
<p>Points Menu</p>
</div>
</div>
</div>
</div>
</div>
);
}
}
ReactDOM.render(
<Menubar />,
document.getElementById('example')
);
I've fixed your Codepen Demo
Also, consider using callback refs like ref={el => this.menu = el}
It's a better way to do it.