Search code examples
javascripthtmljquerycssjquery-events

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


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');
});

Solution

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