Search code examples
javascriptjqueryhtmlcsstransitionend

Toggle function fires twice on transitionend


I have a toggle whose checked status I am calling with jQuery. Every time I click the toggle from unchecked to checked, the console logs "do this!" twice. Why does this function fire twice and what is the workaround to get it to fire once?

$("#my-toggle").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function() {
  if (document.getElementById('my-checkbox').checked) {
    console.log("do this!")
  }
});
.switch {
  position: absolute;
  top: 15%;
  display: inline-block;
  width: 60px;
  height: 34px;
}

.switch input {
  display: none;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  -webkit-transition: .4s;
  transition: .4s;
}

.slider:before,
.slider .number {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: white;
  -webkit-transition: .4s;
  transition: .4s;
}

.slider .number {
  background: none;
  font-size: 14px;
  left: 9px;
  top: 9px;
}

input:checked+.slider {
  background-color: #2196F3;
}

input:focus+.slider {
  box-shadow: 0 0 1px #2196F3;
}

input:checked+.slider:before,
input:checked+.slider .number {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}


/* Rounded sliders */

.slider.round {
  border-radius: 34px;
}

.slider.round:before {
  border-radius: 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<label class="switch">
  <input type="checkbox" id="my-checkbox">
  <span class="slider round" id="my-toggle"></span>
</label>

My final HTML looks like this, for which the event fires three times:

<label class="switch">
  <input type="checkbox" id="my-checkbox">
  <span class="slider round" id="my-toggle">
    <span class="number">00</span>
  </span>
</label>

Solution

  • There are two transitionend events firing: one from your #my-toggle <span>, one from your ::before pseudo element. You need to differentiate those two events. You’d need the event argument e for that.

    The only difference is then found in e.originalEvent.propertyName (for the corresponding CSS property) and e.originalEvent.pseudoElement.

    So let’s check for that last property in the if condition.

    Also put another && e.target == this in there, since the transitionend event is also firing for children because the event bubbles up. Since you’re only listening to the event on the parent <span>, you can’t simply call e.stopPropagation which would only be useful to prevent the parent(s) from firing the event.

    $("#my-toggle").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(e) {
      if (document.getElementById("my-checkbox").checked && !e.originalEvent.pseudoElement && e.target == this) {
        console.log("do this!");
      }
    });
    .switch {
      position: absolute;
      top: 15%;
      display: inline-block;
      width: 60px;
      height: 34px;
    }
    
    .switch input {display:none;}
    
    .slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      -webkit-transition: .4s;
      transition: .4s;
    }
    
    .slider:before,
    .slider .number {
      position: absolute;
      content: "";
      height: 26px;
      width: 26px;
      left: 4px;
      bottom: 4px;
      background-color: white;
      -webkit-transition: .4s;
      transition: .4s;
    }
    .slider .number {
      background: none;
      font-size: 14px;
      left: 9px;
        top: 9px;
    }
    
    input:checked + .slider {
      background-color: #2196F3;
    }
    
    input:focus + .slider {
      box-shadow: 0 0 1px #2196F3;
    }
    
    input:checked + .slider:before,
    input:checked + .slider .number {
      -webkit-transform: translateX(26px);
      -ms-transform: translateX(26px);
      transform: translateX(26px);
    }
    
    /* Rounded sliders */
    .slider.round {
      border-radius: 34px;
    }
    
    .slider.round:before {
      border-radius: 50%;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <label class="switch">
      <input type="checkbox" id = "my-checkbox">
      <span class="slider round" id = "my-toggle">
         <span class="number">00</span>
      </span>
    </label>