javascriptbackbone.jsmarionettejquery-blockui

Why does jquery.blockUI swallow onClick event?


Summary Using jquery.blockUI seems to hide / swallow / mask button click event.

Tech Stach

  1. backbone and marionette
  2. backbone.radio
  3. underscore
  4. jquery
  5. jquery.blockUI

(all latest version)


The App

The app consists of a text input and a button.

enter image description here

In terms of backbone/marionette terminology, there is

  1. a top view which has 2 regions
  2. container view which as the text input
  3. footer view which has the button
  4. The container view is backed by a model.
  5. The footer has a button, Clicking the button sends a backbone.radio event.
  6. This event is picked up in the top view.

enter image description here

When the user leaves the text input, an API (server / backend) is called. In the example a Promise / setTimeout is used to simulate the call.

In the example the button calls console.log.


Code

Here is the JSFiddle Example on JSFiddle and below the Javascript code

// ------------------------------------------------------------------
var Model = Backbone.Model.extend({

  defaults: {
    "SearchCriteria": {
      "Min": { "value": "abc123", "ReadOnly": true }
    }
  },


  async callBackend() {

    //$.blockUI(); //<----- uncomment this and the button click is swallowed

    await new Promise(resolve => setTimeout(resolve, 3000));

    $.unblockUI();
  }

});
// ------------------------------------------------------------------

// ------------------------------------------------------------------
var ContainerView = Marionette.View.extend({
  template: _.template('<div><label>Container</label></div><div><input id = "min" name = "min" type = "text"/></div>'),

  events: {
    'change': 'onChangeData',
  },

  async onChangeData(data) {
    console.log('start onChangeData');    
    await this.model.callBackend();
    this.render();
    console.log('end onChangeData');    
  }

});
// ------------------------------------------------------------------

// ------------------------------------------------------------------
var FooterView = Marionette.View.extend({
  template: _.template('<div><button class="btn-footer-test">Footer</button></div>'),

  events: {
    "click .btn-footer-test": () => {
      console.log('click test ...');
      Backbone.Radio.channel("maske").trigger("select:test");
    }
  },

});
// ------------------------------------------------------------------

// ------------------------------------------------------------------
var TopView = Marionette.View.extend({
  template: _.template("<div id='container'></div><div id='footer'></div>"),

  regions: {
    container: '#container',
    footer: '#footer'
  },

  events: {
    'change': 'onChangeData',
  },

  initialize() {
    this.listenTo(Backbone.Radio.channel("maske"), "select:test", this.onTest, this);
  },

  onRender() {
    this.showChildView('container', new ContainerView({
      model: new Model()
    }));
    this.showChildView('footer', new FooterView());
  },

  onChangeData(data) {
  },

  onTest() {
    //NOT called if jquery.blockUI present ******

    console.log('onTest');
  }
});
// ------------------------------------------------------------------

$(document).ready(function () {
  console.log('Start');
  const topView = new TopView();

  topView.render();
  $('body').append(topView.$el);
});

Use

The user uses the app like so. The user

  1. changes the text input
  2. and directly clicks the button (without tabbing out of the field first!)

Expected Behavior

  1. the change to the text input triggers a change event.
    1. jquery.blockUI
    2. async call
    3. jquery unblockUI
  2. the click event to the button is executed

Actual Behavior

When the jquery.blockUI function is present the click event to the button is not executed. Commenting jquery.blockUI the button click event occurs, however before the await returns.


Questions

  1. What am I doing wrong?
  2. Why is the click event swallowed?

Solution

  • What am I doing wrong?

    Your expectations are wrong. There is no implicit mechanism in JavaScript that serializes asynchronous events one after the other has completed. You (developer) are responsible for synchronization of asynchronous events.

    Why is the click event swallowed?

    Click event fires when a mousedown and mouseup event occur on the same element. And this is not your case. The order of events is as follows:

    1. mousedown on <button>
    2. change on <input>; causes displaying overlay <div> via blockUI
    3. mouseup on overlay <div>
    4. click on closest common parent element of elements that triggered mousedown and mouseup, which is <body>

    Technically it seems to be impossible to click the button after the input changed, because overlay is displayed before mouseup, however there is one way. If you click and hold the mouse button while the overlay is being displayed and release afterwards, the click event will be triggered on the button, but this isn't something that you want anyway.

    Try to play with this snippet. It logs every mousedown, mouseup, click and change event. It registers asynchronous change event handler on <input> thet does nothing for the first second, then displays an overlay, then sleeps for 3 seconds and finally hides the overlay. You can observe various behaviours based on how long you kept the button mouse depressed.

    Change input text and quickly click the button

    button.mousedown
    input.change
    button.mouseup
    button.click

    Change input text, click the button and hold for 1 sec, then release

    button.mousedown
    input.change
    div.mouseup
    body.click

    Change input text, click the button and hold for 4 sec (until overlay disappears), then release

    button.mousedown
    input.change
    button.mouseup
    button.click

    $(document).on('mousedown mouseup click change', e => {
      console.log(e.target.tagName.toLowerCase() + '.' + e.type);
    });
    
    $('input').change(async _ => {
      await new Promise(resolve => setTimeout(resolve, 1000));
      const $div = $('<div style="position:fixed; left:0; top:0; right:0; bottom:0; background-color:rgba(0,0,0,0.5); z-index:999">').appendTo('body');
      await new Promise(resolve => setTimeout(resolve, 3000));
      $div.remove();
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <input type="text" placeholder="type something, then click the button" style="width:250px">
    <button>Button</button>

    You will have pretty hard time getting around this, because in normal situations the click event won't be fired on the button and therefore I'd suggest you to rethink your user interface. Why do you even block user interface when you use async/promises? It was invented to avoid blocking.