Why does preventDefault() on a parent element's click 'disable' a checkbox?
Asked Answered
L

2

10

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?

Linwoodlinz answered 2/4, 2013 at 14:42 Comment(2)
This is the intended behavior of preventDefault(). It doesn't matter what node the eventObject is bubbling through, it's still the same object.Via
Funny thing is that you actually provided the right answer :-) For people more acknowledged in event handling, this is a quick one :-)Electroencephalograph
S
13

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?

  1. You click on the checkbox
  2. It gets checked
  3. The event is dispatched on the document root
  4. (Not in IE): capture phase, nothing happens with your handlers
  5. The event arrives at the <input>
  6. …and begins to bubble
  7. On the <div>, it is handled. Your event listener calls the preventDefault method, setting an internal cancelled flag.
  8. It bubbles on, but nothing happens any more.
  9. 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:

  1. The event is handled on the <input>. Your listener calls the stopPropagation method
  2. …which leads to skipping the bubbling phase. (The div-listener will never be called)
  3. The default action was not prevented, and the checkbox stays checked.
Sympetalous answered 2/4, 2013 at 15:5 Comment(7)
Thank you for providing such a simple explanation. "It doesn't need to arrive at the document root, but it must not have been default-prevented" nailed itFateful
Would you call Explosion Pills' fiddle as a Google Chrome bug?Wie
+1 though Chrome fails to prevent the default action for the radio and IE8 fails both checkbox and radio here.Wie
To expand a bit more on what Bergi is saying, the reason why the span and button events work is because that is during the pre-activation phase and the event listeners must be handled. Default behavior is still canceled: jsfiddle.net/P8Fap/4Dorinda
@FabrícioMatté: Yes, this looks like a bug to me. Both FF and Opera do not check the radio button. Notice that Chrome does it correctly if at least one (other) radio button is checked.Sympetalous
Thanks. And I just noticed that the fiddle would never work in IE8 as IE<9 does not support addEventListener, but by adding the old IE feature detection the default action is prevented as expected in IE8. jsfiddle.net/R7dgW/8/showWie
I was hoping to end up with some reading material when I asked the question and OP delivered. Thanks for an authoritative answer! (Has anyone rewritten the HTML spec as a metaphorical sci-fi novel yet?!?)Linwoodlinz
C
5

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).

Contentious answered 2/4, 2013 at 14:47 Comment(13)
This doesn't explain why preventing the default behavior of a parent element prevents children behaviors. The OP clearly understands what preventDefault does.Fateful
I thought it was pretty clear. Added a bit of clarification; don't know if you'll think it makes any difference, tho.Contentious
@Fateful it seemed pretty clear to me - saw you copied & pasted your comment from another answer, did you read this answer completely before commenting?Theme
@Theme All this answer (and the other answer) does is explain what preventDefault and stopPropagation do. The OP has no problem understanding their purpose. The question is in regards to a parent/ancestor element preventing the default behavior of an event which was triggered by a child/descendent element. The point is that the event object that is passed between these handlers is the same, so when a parent prevents the default behavior of an event, it doesn't matter where it originated - it will prevent its default behavior.Fateful
What @Fateful said; how do you explain the fact that the checkbox cannot be checked but the radio button can? jsfiddle.net/R7dgWDorinda
@DavidHedlund And that's why I'm upvoting, because I think that's exactly what the OP needs. I just wanted to explain more of what I meant in the first placeFateful
I think an important thing to note is the e.target. Since the event bubbles up, you will see events pass through the <div> that were triggered by the <input> (or any other descendents) and will be the target. Since it bubbled and passes through the <div>, calling preventDefault on it isn't anything special compared to a handler where it originated. It will still prevent the default behavior of that specific event.Fateful
@ExplosionPills The radio cannot be checked on my Firefox.Wie
@FabrícioMatté I think they meant something more like jsfiddle.net/R7dgW/1 - since the event wasn't propagated, the parent listener wasn't able to prevent the default behaviorFateful
@Fateful that's not what I meant, but he is right; FFX does not select the radio button, but Chrome does. This indicates to me that this is not necessarily intended.Dorinda
@ExplosionPills Ahh okay. That's interesting...even if I change it to be jQuery events, the same thing happens in FF/Chrome. jsfiddle.net/R7dgW/2 - I thought maybe that would make things more consistent/"correct"Fateful
@Fateful I think that Chrome does not use click as the activation event for radio buttons, which is weird because I think that's what it should be, but if there is another event that does trigger it I can't find itDorinda
Nice concise answer. The precise purposes of preventDefault and stopPropagation needed clarifying in my head - I had always looked at them both crudely as "things to try when you run into ancestor/descendent problems". Many thanks!Linwoodlinz

© 2022 - 2024 — McMap. All rights reserved.