what is the best way to catch and handle a click event on "anything except specific DOM-node(s)" in a React app?
The handler of the click event is the easy part: this can be any method.
The registration of the event, and the trigger to invoke the handler, is the hard part.
There is no clean way to capture a "clicks outside ...." event.
There are however various (HTML/ CSS/ Javascript) tricks you could apply:
If it is a modal page/ popup, you could also render a full page background rectangle (e.g. slightly transparent grey), which is in front of the whole page, but behind the popup. Add a click-event-listener to this background to remove the modal + the grey background.
Another method is to use the focusout javascript event on your top-react component:
the top HTML component rendered by react should be able to get focus (needs to be an <a> or similar HTML, or - somewhat less clean - needs a tabindex=... to work)
give the element focus as soon as it is rendered (inside componentDidMount()`)
add a focusout event listener, which triggers the handler to do something with the click outside.
The focusout event is fired as soon as the component no longer has focus:
- if a child of the component gets focus (e.g. you click something inside the component) focusout is also fired: usually no problem for menu's, but undesired for popups with forms
- the focusout is also fired if the user presses TAB.
There's no React-specific way to do this; all React event handlers are tied to the component they're set on. The best way to accomplish this depends on the details of what you need to get done, but a fairly straightforward way to address this would be to add a delegated click handler to the body element, or the closest ancestor element that includes the area you want to capture clicks from. You'd attach this event handler either on the component's componentDidMount() or whenever it becomes relevant, for example, after toggling the component's state so that it shows a dropdown menu.
Attach the event handler however you normally would – element.addEventListener or jQuery's $().on or what-have-you - and evaluate the event target when it fires to determine whether you need to execute your custom logic.
Simple example, without jQuery:
componentDidMount() {
document.body.addEventListener('click', (e) => {
if (e.target !== [your dom node]) {
// do something
}
}
}
Attaching a single event handler on the body element shouldn't pose any performance issues, but best practice for most cases where you'd use something like this would be to remove the event handler when it's no longer needed.
Related
As said in the title, I am trying to customize the contextmenu event. The situation is this: I want to catch the event, preventing it from firing on some elements (I'm ok here, all good), then I want to call it targeting another element (this is not working). At first I just tried dispatching it by creating a custom event and using myTargetElement.dispatchEvent(), the custom element does fire, but context menu won't open.
The reason I need this is that I want to open the contenteditable context menu when the user clicks anywhere. I've tried something similar to the last example on this MDN page, and I logged the event type, it is firing. Here's some example code of what I'm doing.
HTML
<div id="prevent">This div will prevent default event behaviour.</div>
<div id="my-target" contenteditable>Fire here the event and open context menu</div>
For instance, I cannot put one div inside the other.
JS
function showMenu(){
const preventer = document.getElementById('prevent');
const myTarget = document.getElementById('my-target');
const myEvent = new Event('contextmenu', {
bubbles:false //I had to use this, as setting it true was logging an error on Firefox
});
myTarget.dispatchEvent(myEvent);
console.log(myEvent.type); //it does log the event name
}
The listener that prevents default is not important, as when I just run the showMenu() (even when removing every other bit of js) on console it still has not the intended effect. I'm also able to listen to the 'contextmenu' event when I add a listener and run showMenu().
I'm beginning to think that there is not a direct solution to this, but workarounds and ideas would be really appreciated.
Some code that looks like the following is firing the click event via the Enter key, but is not responding to the mouse click.
//a is an anchor element
a.addEventListener('click', function (e)
{
//Do Stuff...
});
This page demonstrates the problem. The relevant bit of code is at line 176. This is in the middle of development and currently only (sort of) works in Chrome.
Also, I just verified that it works if I use mousedown, so it's not just the case of an invisible element sitting in front of the anchor.
Any ideas?
Edit: Now that you've shown us the actual code you're using, the problem is related to the fact that the autoSuggest() function has it's own click handler and in that click handler, it is clearing the container which removes all <a> elements in the container so your link object gets destroyed (probably before your click event gets to process). So, you can get events that happen before the click (like mousedown), but after a click, the element is removed from the DOM.
If you tell us what you're trying to actually do when an auto-suggest item is clicked that is different than the default behavior of the autoSuggest() function and you point to any documentation for that function, then perhaps we could offer a better way to solve your issue.
The link may be firing and taking you off to a new page (or reloading the current page), thus preventing you from seeing the click code run. Usually when you process a click event on a link element, you need to prevent the default behavior:
//a is an anchor element
a.addEventListener('click', function (e) {
e.preventDefault();
//Do Stuff...
});
Another possibility is that you are trying to install the event handler too soon either before the DOM has been loaded or before this particular link has been created and thus no actual click event handler is attached to the DOM object. You can verify whether the event handler is even getting called by temporarily putting an alert("Click handler called"); in the event handler and see if that pops up or not.
I've got an event handler set on document, listening for submit and change events, delegated to any form elements on the page. The idea is to update the server when an input is altered (since the event bubbles up to the form), while also allowing for submitting an entire form. I have a few overrides in place for inputs that need special handling, and this works great (since they're placed higher up in the code, I can block the main event handler with .stopImmediatePropagation() - all these handlers are on document.)
What I'm trying to do now is intercept the change event on a specific input, then instead of preventing, I want to re-trigger the primary handler, passing in some additional data (using jQuery's .trigger(event, [data]) syntax.)
To give a simplified example: http://jsfiddle.net/rrEmq/3/
My goals are to:
Maintain delegated events (the forms are loaded in dynamically, the JS is loaded once)
Avoid duplicating code
Avoid breaking out the main handler's functionality into separate functions
That third item is my initial approach, and I understand that it will get the job done, but I'd love to find a way to solve this without rewriting the main handler. Obviously, the real kicker is that, since everything's delegated to the same element, and since I'm using e.target, I can't simply trigger a handler higher up in the DOM tree.
Create a custom event and trigger with the extra data
Instead of
$(this).trigger("change", data);
Call
$(this).trigger("changemodified", data);
And create a handler for this event.
You can reuse handlers
var handleChange = function(e) { };
$("something").on("change", handleChange);
$("something").on("changemodified", handleChange);
I have a div on a page that shows some info about a particular category (Image, Name etc).
When I click on the edit image it puts the category into edit mode which allows me to update the name. As you can see from the image below it shows that "Soup" is currently in edit mode, the others are in normal view mode. This all works as expected with the cancel / save buttons doing everything right. (I tried adding an image but wouldn't let me, need more love)
However once in edit mode if I click anywhere else on the page (Outside of the div) the expected result would be that the soup category would go back to view mode. Upon an event firing of some sort, this should also allow me to ask if they wanted to save changes.
So what I then decided to do is create an blur event on the "soups" parent div. This works as expected if I click anywhere on the page, however if I click on the inner element of the div it also causes the parents blur event to be fired, thus causing the category to go back to view mode.
So, is there a way to prevent the parent div from firing the blur event if any one of its children receive focus?
<div tabindex="-1" onblur="alert('outer')">
<input type="text" value="Soup" />
</div>
I just wrote the code without a compiler so not sure if that even works but with that hopefully you get the idea.
I'm using Knockout.js to update the GUI on the fly but that shouldn't effect this answer I wouldn't have thought.
I faced the same issue. This what worked for me.
handleBlur(event) {
// if the blur was because of outside focus
// currentTarget is the parent element, relatedTarget is the clicked element
if (!event.currentTarget.contains(event.relatedTarget)) {
.....
}
}
Enjoy :)
I've had to tackle this problem before. I am not sure if it is the best solution, but it is what I ended up using.
Since the click event fires after the blur, there is no (cross-browser, reliable) way to tell what element is gaining focus.
Mousedown, however, fires before blur. This means that you can set some flag in the mousedown of your children elements, and interrogate that flag in the blur of your parent.
Working example: http://jsfiddle.net/L5Cts/
Note that you will also have to handle keydown (and check for tab/shift-tab) if you want to also catch blurs caused by the keyboard.
I don't think there is any guarantee mousedown will happen before the focus events in all browsers, so a better way to handle this might be to use evt.relatedTarget. For the focusin event, the eventTarget property is a reference to the element that is currently losing focus. You can check if that element is a descendant of the parent, and if its not, you know focus is entering the parent from the outside. For the focusout event, relatedTarget is a reference to the element that is currently receiving focus. Use the same logic to determine if focus is fully leaving the parent:
const parent = document.getElementById('parent');
parent.addEventListener('focusin', e => {
const enteringParent = !parent.contains(e.relatedTarget);
if (enteringParent) {
// do things in response to focus on any child of the parent or the parent itself
}
});
parent.addEventListener('focusout', e => {
const leavingParent = !parent.contains(e.relatedTarget);
if (leavingParent) {
// do things in response to fully leaving the parent element and all of its children
}
});
Heres my link:
http://tinyurl.com/6j727e
If you click on the link in test.php, it opens in a modal box which is using the jquery 'facebox' script.
I'm trying to act upon a click event in this box, and if you view source of test.php you'll see where I'm trying to loacte the link within the modal box.
$('#facebox .hero-link').click(alert('click!'));
However, it doesn't detect a click and oddly enough the click event runs when the page loads.
The close button DOES however have a click event built in that closes the box, and I suspect my home-grown click event is being prevented somehow, but I can't figure it out.
Can anyone help? Typically its the very last part of a project and its holding me up, as is always the way ;)
First, the reason you're getting the alert on document load is because the #click method takes a function as an argument. Instead, you passed it the return value of alert, which immediately shows the alert dialog and returns null.
The reason the event binding isn't working is because at the time of document load, #facebox .hero-link does not yet exist. I think you have two options that will help you fix this.
Option 1) Bind the click event only after the facebox is revealed. Something like:
$(document).bind('reveal.facebox', function() {
$('#facebox .hero-link').click(function() { alert('click!'); });
});
Option 2) Look into using the jQuery Live Query Plugin
Live Query utilizes the power of jQuery selectors by binding events or firing callbacks for matched elements auto-magically, even after the page has been loaded and the DOM updated.
jQuery Live Query will automatically bind the click event when it recognizes that Facebox modified the DOM. You should then only need to write this:
$('#facebox .hero-link').click(function() { alert('click!'); });
Alternatively use event delegation
This basically hooks events to containers rather than every element and queries the event.target in the container event.
It has multiple benefits in that you reduce the code noise (no need to rebind) it also is easier on browser memory (less events bound in the dom)
Quick example here
jQuery plugin for easy event delegation
P.S event delegation is pencilled to be in the next release (1.3) coming very soon.