REACT: How could we detect user click on canvas? - javascript

Hello and thank you for reading this question.
I had an use case in which I needed to detect mouse coordinates on canvas, and thank you to #Carlos Martinez : getting mouse coordinates in React it works as expected.
I tryed to go further and detect if user clicks on canvas and then put on a h2 it, using state, and log it; and here is the code I have tryed:
import React from 'react';
class Canvas extends React.Component {
//A canvas to display images with a title
constructor(props) {
super(props);
this.state = {x: 0, y: 0, inside: ''};
this.handleClick = this.handleClick.bind(this);
}
_onMouseMove(e) {
this.setState({x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY});
}
componentDidMount() {
document.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClick);
}
handleClick(e) {
console.log('INSIDE');
this.setState({inside: 'inside'});
}
render() {
const {x, y, inside} = this.state;
return (
<div className="previewComponent">
<div className="imgPreview">
{this.props.title}
<img src={this.props.image} alt="" onMouseMove={this._onMouseMove.bind(this)}
onClick={this.handleClick.bind(this)}/>
<h1>Mouse coordinates: {x} {y}</h1>
<h2>Inside?: {inside}</h2>
</div>
</div>
)
}
}
export {Canvas};
I expected that it would log it just once, and also put it on h2 tag.
However, unexpectedly it logs INSIDE, and then in addition logs two INSIDE:
INSIDE
2 Canvas.js:29 INSIDE
Could you explain me this behaviour? Also, it would be appreciated some tips to fix it and just print one log per click!
EDIT:
I have tried #Or B answer and I understood it however it looks like it stills showing the same behaviour, 3 logs of INSIDE instead of one:
The code is:
handleClick(e) {
e.stopPropagation();
console.log('INSIDE');
this.setState({inside: 'inside'});
}

This happens due to event propagation and the fact that you're listening to click events on the entire document.
Clicking on the <img> not only generates a click event for the image, but also for the two wrapping <div> elements. These two are captured by the document, which is why it's being logged two more times. If you log e.target you could see which element triggered the event.
In order to prevent it from propagating, use event.stopPropagation():
handleClick(e) {
e.stopPropagation();
console.log('INSIDE');
this.setState({inside: 'inside'});
}

This is because click handler is being called twice. Once by the canvas onClick component and the second time by the click handler inside the componentWillMount function. Also, the componentWillMount function listens to the clicks on the entire document because of the document.addEventListener part. I would simply use the componentWillMount to set states.
Remove the document.addEventListener part and it should be good to go.

Related

React onMouseEnter and onMouseLeave not behaving consistently

I have a div that is simply supposed to display 'HOVERING' if the cursor is hovering over it, and 'NOT HOVERING' otherwise. For some reason, it behaves as expected if I slowly hover each div on the page; however, if I quickly move my cursor across the screen, some of the divs become switched. Meaning, they will display "NOT HOVERING" when my cursor moves over the div, and "HOVERING" when my cursor is not over the div.
This error occurs in both Chrome and Safari.
Sandbox:
https://codesandbox.io/s/aged-butterfly-r2g6x?file=/src/Geo.js
Move your cursor quickly over the boxes to see the issue.
Issue
I think the main issue with your implementation is with the way asynchronous event callbacks are queued up and processed in the event loop. I can't find any hard details about the latency of processing event callbacks but the docs here and here may shed some more light on the matter if you care to do a deep dive.
Basically the issue is two-fold:
There is a minute duration a single event loop takes to process, i.e. detect an event and add it to the queue. I suspect the mouse is moving fast enough off/out the screen or into another div it isn't detected. The divs "jumping"/"moving" when hovering also doesn't help much.
The component logic assumes all events can and will be detected and simply toggled the previous existing state. As soon as an event is missed though the toggling is inverted, thus the issue you see. Even in the updated sandbox this latency can cause one of the elements to get "stuck" hovered
Proposed Solution
Add a mouse move event listener to the window object and check if the mouse move event target is contained by one of your elements. If not currently hovered and element contains event target, set isHovered true, and if currently hovered and the element does not contain event target, set isHovered false.
This isn't a full replacement for the enter/leave|over/out event listeners attached to the containing div as I was still able to reproduce an edge-case. I noticed your UI is most susceptible to this issue when moving the mouse quickly and leaving the window.
Combining the window and div event listeners gives a pretty good resolution (though I was still able to reproduce edge-case it is much more difficult to do). What also seems to have helped a bit is not defining anonymous callback functions for the div.
import React, { createRef } from "react";
export default class Geo extends React.Component {
state = {
isHovering: false
};
mouseMoveRef = createRef();
componentDidMount() {
window.addEventListener("mousemove", this.checkHover, true);
}
componentWillUnmount() {
window.removeEventListener("mousemove", this.checkHover, true);
}
setHover = () => this.setState({ isHovering: true });
setUnhover = () => this.setState({ isHovering: false });
checkHover = e => {
if (this.mouseMoveRef.current) {
const { isHovering } = this.state;
const mouseOver = this.mouseMoveRef.current.contains(e.target);
if (!isHovering && mouseOver) {
this.setHover();
}
if (isHovering && !mouseOver) {
this.setUnhover();
}
}
};
render() {
var textDisplay;
if (this.state.isHovering) {
textDisplay = <span>HOVERING</span>;
} else {
textDisplay = <h1>NOT HOVERING</h1>;
}
return (
<div
ref={this.mouseMoveRef}
onMouseEnter={this.setHover}
onMouseLeave={this.setUnhover}
style={{ width: 300, height: 100, background: "green" }}
>
{textDisplay}
</div>
);
}
}
As far as I can see, you have a problem with the way you update the state. Bear in mind that React may update the state asynchronously.
Changing toggleHoverState function will solve the issue
toggleHoverState() {
this.setState(state => ({isHovering: !state.isHovering}));
}
Go to this section in React docs for more info

Pure Javascript mouseenter and mouseleave event delegation with React issues

I would like to achieve something like this on my React project (https://creativesfeed.com/code/custom-cursors/index2.html)
I want to have a custom mouse cursor (a dot in this case which is in a "div") which follows the coordinates the mouse and that's fine, I can make it work.
The other thing that I want to achieve is that when hovering on ANY tag, a custom class will be added to the custom mouse cursor and removed when the custom cursor leave the link. The problem is that whenever I hover on the tag, the class appear and disappear immediately. I know that eventListener in Javascript are different from jQuery and I even tried with jQuery (which I hate to have in React) and I have the same issue. So what am I doing wrong? Could it be because I run the function on componentDidMount() ?
import React, { Component } from 'react';
import Homepage from './components/Homepage';
class App extends Component {
componentDidMount() {
let mouse__follower = document.getElementById('mouse__follower');
let links = document.getElementsByTagName('a');
document.addEventListener("mousemove", (e) => {
var x = e.clientX;
var y = e.clientY;
mouse__follower.style.top = `${y}px`;
mouse__follower.style.left = `${x}px`;
});
let addClassToLink = (e) => {
mouse__follower.classList.add('active');
};
let removeClassToLink = (e) => {
mouse__follower.classList.remove('active');
};
// ALSO, IS THIS VERY BAD IN TERMS OF MEMORY CONSUMING?
for(var i = 0, len = links.length; i < len; i++) {
links[i].addEventListener('mouseenter', addClassToLink, false);
links[i].addEventListener('mouseleave', removeClassToLink, false);
};
}
render() {
return (
<div className="nu-creative">
<div id="mouse__follower">
<div id="mouse__follower__circle"></div>
</div>
<Homepage />
</div>
);
}
}
export default App;
The link is in another file, but I think that's not the issue.
EDIT: I have found the issue. If I remove
mouse__follower.style.top = `${y}px`;
mouse__follower.style.left = `${x}px`;
from the document.addEventListener("mousemove", ...) the class is added and removed correctly, so there's must something there that I can do. Maybe is because two eventListener are triggered together.
I will investigate, thanks for the answers so far :)
Looks like the react life cycle is messing around with you. The thing is that react re-renders the html whenever it decides it should be done, and in your render method you are removing the inline classes.
So, the way to do this react-wise would be to set a flag in the state of your component which you change with setState, and check for this flag in your render method to decide wether or not add the class to the cursor.
Something like this:
<div id="mouse__follower" className={this.state.cursorActive? "active":""}>
Edit:
btw, this is not related but, remember to unbind all your event listeners that were bound in componentDidMount inside the componentWillUnmount.
ISSUES SOLVED :)
The z-index of the div containing the "dot" wasn't set and this was causing the event to trigger too many times. By setting the "z-index" to -1 the issues was solved.

Is there any way to call a component method on clicking a "non react" dom element

Here's the scenario. I have created tables using jQuery Datable plug-in. In the last column, there's a button (an non react HTML element) for all rows. Since all HTML for table is automatically created by the plug-in, we can't use JSX component for the button and hence can't use onClick react listener.
Here's what I'm doing currently:
In my regular script file (non react):
$(document).on("click", ".my-button", function(){
//show a popup and add content in it using ajax
});
Here's what I want to do the same in react code (i.e., in the main component class)
class LoginForm extends React.Component {
constructor(props) {
super(props);
}
//following method is to be called on onClick
showAPopupAndAddContentAjax() {
//code
}
//other stuff
}
So is there any way to call any react listener method? Or is there any different approach to achieve this?
PS: I just can't remove datatable code for now as it's already written and can't be replaced right now. Just need the listeners like onClick
Normally, you can't do that. Because React works on virtual-dom concept and you want to interact with the core dom.
A tricky way to do that is to add class and trigger the popup open:
$(document).on("click", ".my-button", function(){
$('#your_instance_of_component').addClass('open-popup');
});
// in your react app
const openPopup = ReactDOM.findDOMNode(LoginForm)
.getElementsByClassName('open-popup')//[0]
// implement the logic
if(openPopup.length) {
// do the stuff
}
Hope, this helps!
Update
I just got another idea which will work fine:
In the jQuery listener, add the query parameter. And in the react app, you may call the dom listener on route change. You must give effort for this with some research. Hope, this helps now!
Setup an event listener which React listens for and update the state based on that.
This code snippet should give you the general idea.
$(document).on("click", ".my-button", function() {
//show a popup and add content in it using ajax
$(document).trigger( "show-my-react-popup" );
});
class Popup extends React.Component {
state = { open: false };
showAPopupAndAddContentAjax = () => {
this.setState({ open: true });
};
closePopup = () => {
this.setState({ open: false });
};
componentDidMount() {
// jQuery code, should be refactored and removed in the future
$(document).on("show-my-react-popup", () => {
this.showAPopupAndAddContentAjax();
});
}
componentWillUnmount() {
// jQuery code, should be refactored and removed in the future
$(document).off("show-my-react-popup");
}
render() {
if (!this.state.open) return null;
return (
<div className="popup">
<p>popup</p>
<button onClick={this.closePopup}>close</button>
</div>
);
}
}
onmousedown onmouseup
Source - HTML DOM Events
There is alternative ways aswell, if using jQuery:
$('#SomeButton').click(function() {
functionTOevent();
});
*Use same idea as this, to get something in react.
It's not advised to use frameworks that use virtual DOM (React) with frameworks that use physical DOM (jQuery).
However, we can render React in only part of the application which avoids a lot of the risk between using both physical / virtual DOM.
I think something like this may work for your use case.
HTML
<button class="my-button">Render Component</button>
<div id="root"></div>
Javascript:
class App extends React.Component {
static getDerivedStateFromProps(props, state) {
// load data with ajax
}
render() {
return (
<div>
<h1>Hello World</h1>
<p>This is a react component</p>
</div>
);
}
}
function renderReact() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
$(document).on('click', '.my-button', function() {
renderReact();
});
You can see it working in this codepen.

How do I set up an 'onKeyPress' handler on a <canvas> in react?

I am familiar with React and its event system, but I can't seem to get the onKeyPress event to fire on a <canvas> element. In fact, I can't get it to fire on a <div> either.
Here is the relevant code
class MyComponent extends React.Component {
render() {
return (
<canvas onKeyPress={() => console.log('fired')} />
)
}
}
It works fine if I change the <canvas> to be an <input>, but does not work for <div>. Does this mean that react simply does not support keyPress events on canvas elements? What am I overlooking?
Just assign tabIndex to the element for getting the focus.
<canvas tabIndex="0" onKeyPress={ () => console.log( 'fired' ) } />
canvas.addEventListener('keydown', function(event) {
alert('keydown');
}, false);
Check if you can fire the above event :)

ReactJS - Handling onClick events for an entire section except for a few child elements

This has been asked a few times with regards to using just normal JavaScript or jQuery, which I've successfully accomplished (in the BatmanJS framework). I'm rewriting my app with React and I'm having an issue accomplishing this.
So I have a parent div, with many child elements of various nested elements inside those. I basically want the entire parent div and every child element to handle a click event, except when a few of the child elements are clicked; I want those to trigger another event.
Normally I would do this:
$(parentDiv).on('click',
'.subParent *:not(a):not(button):not(.ignore-click)',
handleClick);
That basically watches for the click on all child elements except any with the class .ignore-click. This worked fine outside of React.
In React, I did this:
$(this.getDOMNode()).on('click',
'.subParent *:not(a):not(button):not(.ignore-click)',
this.handleClick);
And I can't get it to work properly no matter what I try. The CSS selector in the query works fine. For example, it doesn't trigger for any buttons, but will still trigger for the parent div surrounding the button, which I want, but not when the actual button is clicked.
I'm using e.stopPropagation in the handler.
If there's a better way to solve this without jQuery, I'd be up for that as well. I even thought about adding onClick="" to every element except the ones I wanted, but I would still run into the issue where a parent had the event and would still trigger.
Here's my component code:
var MyComponent = React.createClass({
componentDidMount: function() {
$(this.getDOMNode()).on('click','.subParent *:not(a):not(button):not(.ignore-click)',this.handleClick);
},
handleClick: function(e) {
e.stopPropagation();
var el = e.currentTarget;
console.log('event1: ' + el);
},
handleButtonClick: function(e) {
e.stopPropagation();
console.log('event2: ' + e.currentTarget);
},
render: function() {
return (
<div className="parentDiv">
<div className="subParent">
<div className="anotherDiv">
<p>Some text</p>
</div>
<div className="anotherDiv">
<button className="ignore-click" onClick={this.handleButtonClick}>Trigger event2</button>
</div>
</div>
</div>
)
}
});
Here's a jsfiddle.
You should pass the event handler that you want each component to use (pass by setting it as a prop in the parent call to it). For example:
var React = require('react');
var Parent = React.createClass({
onClickA: function(e) {
// do stuff
},
onClickB: function(e) {
// do stuff
},
render: function() {
return (
<div>
<ChildA onClick={this.onClickA} />
<ChildB onClick={this.onClickB} />
</div>
);
}
});
Ideally with React, you are never changing the DOM, and if you do need to select from the DOM, you should use the React methods to do so (React.findDOMNode);

Categories

Resources