I encountered this situation recently (simplified here). Simply wrap a checkbox with an element and apply preventDefault() on it's click event and the checkbox becomes uncheckable.
See this fiddle, but here's a snip:
<div>
<input type="checkbox"/>
</div>
/* Wrapper could be any element (and any ancestor element will work) */
$('div').on('click', function(e){
e.preventDefault();
});
/* Uncomment to make the checkbox work again
$('input').on('click', function(e){
e.stopPropagation();
});
*/
The behavior occurs in Chrome and FF, so I assume it is intentional.
Why does the click event, which has already been triggered on the checkbox itself not result in the checkbox getting toggled? The preventDefault on the ancestor seems like it ought to be irrelevant to the child checkbox's behavior. It seems as if, for the checkbox change to occur, the click event needs to bubble freely all the way to the document root.
What's going on here?
The preventDefault on the ancestor seems like it ought to be irrelevant to the child checkbox's behavior.
No, not really. The event bubbles, and all handlers (including the ones on ancestors) may affect it.
It seems as if, for the checkbox change to occur, the click event needs to bubble freely all the way to the document root.
Not exactly. It doesn't need to arrive at the document root, but it must not have been default-prevented.
You might want to read the architecture notes in the DOM spec on event flow, default actions and cancelable events.
So what does happen step-by-step?
You click on the checkbox
It gets checked
The event is dispatched on the document root
(Not in IE): capture phase, nothing happens with your handlers
The event arrives at the <input>
…and begins to bubble
On the <div>, it is handled. Your event listener calls the preventDefault method, setting an internal cancelled flag.
It bubbles on, but nothing happens any more.
Since the event was cancelled, the default action should not occur and the checkbox is reset to its previous state.
If you uncomment the second part of your code, steps 6+ look different:
The event is handled on the <input>. Your listener calls the stopPropagation method
…which leads to skipping the bubbling phase. (The div-listener will never be called)
The default action was not prevented, and the checkbox stays checked.
preventDefault prevents the browser's default action from being fired. Changing the checked state of a checkbox or following the href of an anchor, submitting a form, etc., are default actions. preventDefault prevents them, while allowing event propagation.
This is distinct from event propagation, (which you may – as you've noted – stop by stopPropagation). The event will propagate throughout the entire hierarchy of listeners before invoking the browser's default behavior (unless prevented).
Related
When I was exploring StackOverflow's code, I get:
And I tried to get onclick event handler. There are the report:
> $0.onclick
< null
> $0.parentElement.onclick
< null
But $0.click() gives me some result (answer is upvoted).
How StackOverflow developers hid it and how to make it by hand, in pure JS?
While the .onclick property is one way to add a click event listener to elements, it's not the only way. Another way is via .addEventListener(). When an event listener is added to an element via .addEventListener(), the onclick attribute/property doesn't update to the event handler function. The .onclick property is only set on an element if you're using the onclick="" attribute or if the code has set the property elem.onclick = function() {...}, which nowdays isn't very often.
Chrome does provide you with a way to find the event handlers on different elements. When you're in your developer tools, you can select the element you're interested in, click "Event Listeners" in the right-hand pane, find the event you're intrested in, in this case that's the "click" event, and then look for your element:
Your element happens to have an event associated with it, but this might not always be the case. As the comments on your question have pointed out, you can sometimes run into cases where there is no click event handler on your specific element, but rather, it is added to a parent of that element. In this case, when you click on your element, the event propegates/"bubbles" up through the DOM, eventually reaching your parent element with the click event listener. The event handler on the parent can then see what element was originally clicked, and can then perform some actions based on that. This is known as event delegation, and is one of the reasons why you might not always find an event tied to your element even when looking in dev tools.
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.
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.
event.preventDefault() will override default event behavior of an element. How can I temporarily override all click bindings and not just default ones?
Or is there a way to save all the click bindings so I can unbind them and use them later?
Well this is not a proper answer but a workaround. We can push the required handler on top of the stack and then used return false to stop other bindings. https://github.com/private-face/jquery.bind-first
You can use jQuery.clone(true) what this does is return data for an element. The parameter that is set to true means to also copy over all the events as well.
So if you clone the element into a variable you can bring back the old click events by simply replacing your target element with its older clone (which has the old events)
So it goes as follows:
step 1:
clone the target element using jQuery.clone(true) into a variable
step 2:
remove all click events from the target element using jQuery.off('click')
step 3:
bind your event to the target element with jQuery.on('click' function etc...)
step 4:
when you're done replace the target element with its clone (which has the old events)
Here is a JSFiddle for your viewing pleasure
(Sorry for the simpleness of the JSFiddle I mocked it up quickly and I have no example situation where I would use this.)
EDIT: I forgot to explain jQuery.clone(true)
You may catch the click before it can bubble by using
element.addEventListener(type, listener[, useCapture]);
This way you can 'catch' the click before triggering the jQuery click handler, like this (which I took from this stackoverflow question:
document.addEventListener('click', function(e) {
e.stopPropagation();
}, true);
For more information (and some IE < 9 support), see developer.mozilla
Edit: details about useCapture from Mozilla:
If true, useCapture indicates that the user wishes to initiate capture. After initiating capture, all events of the specified type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. Events which are bubbling upward through the tree will not trigger a listener designated to use capture. See DOM Level 3 Events for a detailed explanation. If not specified, useCapture defaults to false.
If you have control over all of the JS code and can bind your own handler first and all other event handlers are bound with jQuery then you can do this:
var overrideClick = false;
$("#yourElementId").click(function(e) {
if (overrideClick) {
e.stopImmediatePropagation();
// e.preventDefault(); uncomment this if you want to prevent default action too
}
});
Where some other part of your code would set overrideClick = true when needed.
Demo: http://jsfiddle.net/NCa5X/
jQuery calls handlers in the order they are bound, so you can then use event.stopImmediatePropagation() to prevent the other handlers from being called.
I have an <input> element that can either have the focus set via code, or as the result of a mouse click.
If the user clicks on the input, then the click event handler will fire - all well and good. If the element receives the focus via some other way (e.g. via code) then I want to manually trigger the click event so that the handler will also fire.
I could do this:
$elem = $('input');
$elem
.on('focus', function() { $(this).trigger('click') })
.on('click', function() { alert('Clicked!') });
However, this will result in click handler being fired twice; once for the click event and once for the focus event.
Is there any way to selectively trigger the click handler only if the focus was not received as the result of a click event?
UPDATE
This is a very simplified version of my problem, so I can't do things like bind both handlers to the focus event etc. I'm trying to merge two third-party pieces of code.
The .trigger() function adds a property isTrigger in the event object to identify that the event was triggered by its usage. Although, it is not documented the property is still present in jQuery 1.8.3 but it seems to only be used internally.
Anyways, you can make use of the extraParameters parameter to add a custom property to the event object. For instance,
$(this).trigger('click', {
isTrigger: true
});
It will keep the compatibility with isTrigger even if it is gone in a future release.
After doing some more research it appears that there is no way of guaranteeing which event will fire first: click or focus. (There doesn't seem to be a standard that dictates the order of events.)
This means that when the focus event fires there's no way to determine if a click event will or will not be triggered by the browser shortly afterwards.
I managed to solve the issue by using setTimeout() to run a test about 100ms after the focus event fired to check if the click event had fired. The third-party code that I was using (bound to the click event) added an extra class to the <input>, so I was able to check for that.
You can tap into the mousedown event which fires before the focus event. When you click a focusable object the order of events is as follows... mousedown, focus, mouseup, click.
You could set a flag in the mousedown event and then check for it in the focus event to see if the focus came from a mouse click. Obviously make sure to clear the flag in the focus event handler. Every application is different, but tapping into the mousedown event allows you to figure out a solution.
Here is a JSFiddle demonstrating the order of events... http://jsfiddle.net/ek7v7/
$elem = $('input');
$elem
.on('focus', function() { alert("Focused!") })
Focus can be fired by focusing the input by using tab, clicking it, or by using .focus()
Is there a reason for on('click', ...)?