Search code examples
jqueryclickevent-delegationmouseup

mouseup, click events and DOM manipulations


Stackers,

I've got a curious case. There is a container element, that contains some other element with the class "clickable". The event listener to clickable 'click' is attached to the container, using delegation.

<input type="text"/>
<div class="container">
    <div class="clickable">Click</div>
</div>

$container.on('click', '.clickable', incrementCounter);

There is also an input element outside the container. On 'change' event the content of the container is replaced with the identical one (using $container.html()) on the next event loop turn (to be realistic :-).

$input.on('change', function() {
    setTimeout(function() {
        $container.html('<div class="clickable">Click</div>');
    }, 0);
});

Now the case: enter something in the input field (do not click anywhere or press Enter). Then immediately click on the clickable.

What happens: 'change' event causes the re-rendering of the container on the next event loop turn and the 'click' event is never generated.

It can be solved with replacement of the 'click' event with the 'mouseup'.

Question: why such a difference?

See plunk.


Solution

  • You are running into a race condition between the blur and the click events.
    A click event is actually a combined mousedown and mouseup events on the same element.

    If you change the input and then click on your container, the events you have are:

    mousedown (on row)
    blur (on input)
    mouse up (on row)
    

    Since the mouse down and mouse up event were interfered (by the blur) your don't get the click event.

    I added a console.log for the relevant events, you can check this snippet:

    $(function() {
      var rowHtml = '<div class="row">Click</div>';
      
      var $rowsContainer = $('.rows-container');
      var $counter = $('.counter');
      var $blinker = $('.blinker');
      var $input = $('input[type=text]');
      
      $input.on('change', blink);
      $rowsContainer.on('mousedown', function(e) {
        console.log('mousedown', e.target);
      });
      $rowsContainer.on('mouseup', function(e) {
        console.log('mouseup', e.target);
      });
      $rowsContainer.on('click', function(e) {
        console.log('click', e.target);
      });
      $input.on('blur', function(e) {
        console.log('blur', e.target);
      });
      
      $rowsContainer.on('click', '.row', incrementCounter);
      
      // this works as expected
      //$rowsContainer.on('mouseup', '.row', incrementCounter);
      
      function incrementCounter() {
        var oldValue = parseInt($counter.text(), 10) || 0;
        $counter.text(1 + oldValue);
      }
      
      function blink() {
        setTimeout(function() {
          $rowsContainer.html(rowHtml);
        }, 0)
    
        $blinker.html('☼');
        
        setTimeout(function() {
          $blinker.html('&nbsp;');
        }, 300);
      }
    });
    .counter,
    .blinker {
      display: inline-block;
      width: 1.2em;
    }
    .rows-container {
      border: 1px dotted #ccc;
      margin: 1em;padding: 2em;
    }
    .row {
      padding: 1em;
      background-color: #eef;
    }
    p {
      font-size: 12px;
      line-height: 12px;
    }
    <script data-require="[email protected]" data-semver="2.1.4" src="https://code.jquery.com/jquery-2.1.4.js"></script>
    <h1>Click vs. mouseup whith DOM manipulation</h1>
    <p>On the input field change event a little sun shortly appears on the right
      and the content of the "Click" parent is redrawn with the identical content.
    </p>
    <p>Clicking on "Click" increments the counter. It works by delegation of the
      event handling to the "Click" container.
    </p>
    <p>Try to enter something in the text field and trigger the "change" event
      by clicking on "Click". Sun appears but counter is unchanged.</p>
    <p>When listening to 'mouseup' event instead of the 'click' everything works.</p>
    
    <h2>Why?</h2>
    <span class="counter">0</span>
    <span class="blinker">&nbsp;</span>
    <input type="text" />
    <div class="rows-container">
      <div class="row">Click</div>
    </div>

    You can use this example to see how exactly the mousedown/mouseup/click events works.

    1. Click on the container (without leaving the mouse), drag your mouse outside the container, and leave your mouse. You will see that you have a mousedown on the container and mouseup on some other element (like body) but no click event.
    2. Click on the container (without leaving the mouse), drag your mouse inside the container, and leave your mouse. You will see that now you do have a click event.