Why are two click events registered in this html/css/jquery

3.8k views Asked by At

I am trying to style a checkbox list. I've added my styles and they appear correctly when rendered. I want to add a class when the label for the checkbox is clicked. This is my markup and here is the same in a jsfiddle. You can see from my fiddle that two click events are registered with just one click. Why?

html:

<ul>
    <li>
        <label for="test_0" class="">
            <input id="test_0" name="offering_cycle" type="checkbox" value="1"> Fall
        </label>
    </li>
    <li>
        <label for="test_1" class="">
            <input id="test_1" name="offering_cycle" type="checkbox" value="2"> Spring
        </label>
    </li>
    <li>
        <label for="test_2" class="">
            <input id="test_2" name="offering_cycle" type="checkbox" value="3"> Summer
        </label>
    </li>
    <li>
        <label for="test_3" class="">
            <input id="test_3" name="offering_cycle" type="checkbox" value="4"> Other
        </label>
    </li>
</ul>

CSS:

ul {
    list-style-type:none;
}
label {
    position:relative;
    display:inline-block;
    padding-left:27px;
    height:25px;
}
label:before {
    display:block;
    position:absolute;
    top:-2px;
    margin-left:-28px;
    width:18px;
    height:18px;
    background-color:#fff;
    border-radius:5px;
    border:1px solid #ccc;
    text-align: center;
    color:#fff;
    font-size:18px;
    content:'a';
}
input {
    width:1px;
    height:1px;
    border:0;
    opacity:0;
    float:right;
}

jQuery:

$('label[for*=test_]').on('click',function(){
    $(this).toggleClass('testing');
});
10

There are 10 answers

0
John S On BEST ANSWER

Reason the label's click handler is called twice:

Clicking on a label that is associated with an input causes two click events to be triggered. The first click event is triggered for the label. The default handling of that click event causes a second click event to get triggered for the associated input. Since you have the input as a descendant of the label, the second click event bubbles up to the label. That is why your click event handler is called twice.


If you really want to handle a click event for the label (and have it execute only once for a click):

(1) If you are willing and able to modify the HTML, you could move the input so it is not a descendant of the label. There will still be two click events, but the second click event will not bubble up from the input to the label since the label is no longer an ancestor of the input.

When the input is not a descendant of the label, you must use the label's "for" attribute to associated it with the input. The value of the "for" attribute should be the "id" value of the input. (You are already including the "for" attribute with the proper value.)

<input id="test_0" name="offering_cycle" type="checkbox" value="1">
<label for="test_0" class="">Fall</label>

(2) Preventing the default handling of the first click event prevents the second click event from getting triggered, BUT doing this breaks the label. That is, the checkbox will not get checked/unchecked when the label is clicked.

$('label[for*=test_]').on('click', function(event) {
    event.preventDefault();
    $(this).toggleClass('testing');
    // returning false would be another way to prevent the default handling.
});

jsfiddle


(3) Instead, you could stop the second click event from bubbling up from the input.

$('input:checkbox').on('click', function(event) {
    event.stopPropagation();
});

$('label[for*=test_]').on('click', function() {
    $(this).toggleClass('testing');
});

jsfiddle

Note: If the input was not a child of the label, this would not be necessary.


(4) Or you could check the event target in the handler. It will be the label for the first click event and the input for the second. The following handler executes the code inside the if-statement only for the first click event.

$('label[for*=test_]').on('click', function(event) {
    if (event.target == this) {
        $(this).toggleClass('testing');
    }
});

jsfiddle

Note: If the input was not a child of the label, the code above would still work, but the if-statement would be unnecessary because the click event triggered for the input would not bubble up to the label.


Handling the click for the input instead:

In your case, you don't really need to register a click handler for the label element. You could register a click (or change) handler for the input instead. You could then use $(this).closest('label') to get the label element.

$('input[name=offering_cycle]').on('click', function() {
     $(this).closest('label').toggleClass('testing');
});

jsfiddle

Note: If the input was not a child of the label, the handler above would still get called when you click on the label, but $(this).closest('label') would not get the label. You would have to use something like $('label[for="' + this.id + '"]') instead.


Regarding the "for" attribute on the label elements:

Since you have the inputs inside the labels, it is not necessary to include the for attributes on the labels --- but it's not invalid.

You have set the "for" attribute values to the values of the "id" attributes of the input elements. That is the correct way to use the "for" attribute to associated a label with an input. If you were to include a "for" attribute with an invalid value, the label would not be associated with the input, even if the input is a descendant of the label.

From the HTML5 spec for the "for" attribute of a label element:

The for attribute may be specified to indicate a form control with which the caption is to be associated. If the attribute is specified, the attribute's value must be the ID of a labelable element in the same Document as the label element. If the attribute is specified and there is an element in the Document whose ID is equal to the value of the for attribute, and the first such element is a labelable element, then that element is the label element's labeled control.

If the for attribute is not specified, but the label element has a labelable element descendant, then the first such descendant in tree order is the label element's labeled control.

0
Harsh Makani On

Rather try this.

Add a common class to all Label's and fire event on that one. Like this :

<ul>
    <li>
        <label for="test_0" class="testClass">
            <input id="test_0" name="offering_cycle" type="checkbox" value="1"> Fall
        </label>
    </li>
    <li>
        <label for="test_1" class="testClass">
            <input id="test_1" name="offering_cycle" type="checkbox" value="2"> Spring
        </label>
    </li>
    <li>
        <label for="test_2" class="testClass">
            <input id="test_2" name="offering_cycle" type="checkbox" value="3"> Summer
        </label>
    </li>
    <li>
        <label for="test_3" class="testClass">
            <input id="test_3" name="offering_cycle" type="checkbox" value="4"> Other
        </label>
    </li>
</ul>

And than,

$(".testClass").on('click',function(){
      $(this).toggleClass('testing');
});
5
Tushar On

That's because you're binding event on label which contains checkbox and you're using for="chekbox_id". So, the event is fired for label as well as for checkbox.

<ul>
    <li>
        <label for="test_0" class="">Fall</label>
        <input id="test_0" name="offering_cycle" type="checkbox" value="1" />
    </li>
    <li>
        <label for="test_1" class="">Spring</label>
        <input id="test_1" name="offering_cycle" type="checkbox" value="2" />
    </li>
    <li>
        <label for="test_2" class="">Summer</label>
        <input id="test_2" name="offering_cycle" type="checkbox" value="3" />
    </li>
    <li>
        <label for="test_3" class="">Other</label>
        <input id="test_3" name="offering_cycle" type="checkbox" value="4" />
    </li>
</ul>
<div id="target"></div>

Take the input out of label.

Demo: http://jsfiddle.net/tusharj/fdm1pmj2/5/

0
Renzo Poddighe On

The click event is fired twice: once for the label, and once for the checkbox. You need to select only the checkbox.

$('label[for*=test_] > input').on('click',function(){
    $('div#target').append($(this).parent().clone());    
});

JSFiddle here

2
Roko C. Buljan On

To explain what happens:

  • Input is clicked, but since the LABEL is bound to that Input using for attribute and for simply being it's child element, here's your first registered event.
  • Than the input propagates the click (all elements propagate clicks) down to LABEL, and since the label is your delegated element - here's your second event being triggered.

Since you wrapped your Input Checkboxes inside a LABEL element,
you can actually listen the even on the input:

$('label[for*=test_] input').on('click',function(){
    $('div#target').append($(this).closest("label").clone());    
});

and than clone the parent label. jsFiddle


Another way to prevent that attributed connection between LABEL and the inner INPUT is to stop the input from propagating the event letting the bounding label's checkbox do its job: fiddle

$("label:input").on("click", function( evt ) {
    evt.stopPropagation();
});
$('label[for*=test_]').on('click',function(){
    $('div#target').append($(this).clone());
});

Otherwise you could prevent the default LABEL's behavior jsFiddle
that triggers back events from the bound INPUT and that's using e.preventDefault() but your (hidden) input checkbox will not get checked!

$('label[for*=test_]').on('click',function( e ){
    e.preventDefault();
    $('div#target').append($(this).clone()); 
});

https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault

0
tarun chine On

you can try this...

$("label").find("*").click(function( event ) {
     event.stopPropagation();
});

$('label[for*=test_]').on('click',function(){
     $(this).toggleClass('test');
});
4
bitsm On

You could also try:

$('label[for^="test_"]').click(function(){
    $('div#target').append($(this).clone());
    return false;    
});
2
Marcel Burkhard On

The for attribute on a given label should be linked to the name of the input not the id.

You can call the method stopPropagation on an event and you can inject the event into the click handler.

Example:

$('label[for*=test_]').on('click',function(e){
    e.stopPropagation();
});

Example html:

<div class="parent something">
    <div class="child something">
    </div>
</div>

Now if you have a click handler on div.something it won't fire twice but you can still use both.

Sources:

0
Michelangelo On

Ok so what I said in the comment. It generates two click events, do this instead and it works like a charm: Fiddle

$('input').on('click',function(){
   console.log("clicked");
    $('div#target').append($(this).parent().clone());    
});

You might want to rename your inputs.

0
Lukas735 On

Maybe your Form is sent two times. To block the second submit (which also is a security issue) you can use this function on the Form. In my Example the form has the Class .comment-form but you can add any form class Name you want.

blockSecondSubmit('.comment-form');

function blockSecondSubmit($form) {
    $($form).on('submit', function () {
        $($form).on('submit', function () {
            return false;
        });
    });
}