Summary Using jquery.blockUI seems to hide / swallow / mask button click event.
Tech Stach
(all latest version)
The App
The app consists of a text input and a button.
In terms of backbone/marionette terminology, there is
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
Expected Behavior
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
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:
mousedown
on <button>
change
on <input>
; causes displaying overlay <div>
via blockUI
mouseup
on overlay <div>
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.