How to block the UI for a long running Javascript loop - javascript

I need to do the opposite of this post, "Best way to iterate over an array without blocking the UI"
I have to loop through hundreds of rows and set a value for each. But that job has to complete before I allow the users to do the next step and submit the updated rows to the database.
The javascript is below.
// toolbar events/actions
changeZeroDiscountButton.click(function (event) {
var value = discountComboBox.jqxComboBox('val');
if ((value != null) && (value != "")) {
value = value / 100;
// get all the rows (this may have to be changed if we have paging
var datainformations = $('#jqxgrid').jqxGrid('getdatainformation');
var rowscounts = datainformations.rowscount;
for (var i = 0; i < rowscounts; i++) {
var preSetDiscount = $("#jqxgrid").jqxGrid('getcellvalue', i, "discount");
if (preSetDiscount == .0000) {
$("#jqxgrid").jqxGrid('setcellvalue', i, "discount", value);
}
}
}
});

JavaScript is designed so it does not block the UI in any way, and this is one of its most important features for the browsers. The only exceptions are the popup message boxes (i.e. alert(), confirm(), and propmpt()). Even if it is possible, it's highly not recommended to block the UI.
There are many alternative ways to prevent the user from firing actions that shouldn't be fired until something else happens. Examples:
Disable the action's button until your processing ends then enable it back.
Set a flag (e.g. var processing = true) and check that flag in the click event of the action's button so it displays a message (e.g. "still processing, please wait...") when flag is true and execute the action when flag is false. Remember not to use alert() for the message otherwise you'll block the processing. Use a popup div instead.
Set the event handler at the beginning of the processing to a function that displays a message (e.g. "still processing, please wait...") and at the end of the processing, set the event handler to the function that will do the action. Remember not to use alert() for the message otherwise you'll block the processing. Use a popup div instead.
Show a modal popup div at the beginning of the processing with a message (e.g. "still processing, please wait..."), or progress bar, or some animation. The modal popup prevents the user from interacting with the page so they cannot click anything. For that to work, the modal popup must not have a close button or any other way to close it. At the end of processing, close the modal popup so the user can now continue.
Important Note: You mentioned in your comment to the other answer that the overlay (which is similar to the modal popup in my last point) is not displayed until the end of processing. That's because your processing is occupying the processor and preventing it from handling the UI thread. When you can do is delay your processing. So first display the modal popup (or overlay), then use setTimeout() to start processing 1 second later (maybe 500 millisecond or even less is enough). This gives the processor enough time to handle the UI thread before it starts your long processing.
Edit Here is an example of the last method:
function start() {
disableUI();
setTimeout(function() {
process();
}, 1000);
}
function process() {
var s = (new Date()).getTime();
var x = {};
for (var i = 0; i < 99999; i++) {
x["x" + i] = i * i + i;
}
var e = new Date().getTime();
$("#results").text("Execution time: " + (e - s));
enableUI();
}
function disableUI() {
$("#uiOverlay").dialog({
modal: true,
closeOnEscape: false,
dialogClass: "dialog-no-close",
});
}
function enableUI() {
$("#uiOverlay").dialog("close");
}
.dialog-no-close .ui-dialog-titlebar {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<button type="button" onclick="start()">Start</button>
<div id="results"></div>
<div id="uiOverlay" style="display: none;">Processing... Please wait...</div>
Edit 2 Here is an example of the third method:
$("#StartButton").on("click", start);
function start() {
//remove all previous click events
$("#StartButton").off("click");
//set the click event to show the message
$("#StartButton").on("click", showProcessingMsg);
//clear the previous results
$("#results").text("");
setTimeout(function() {
process();
}, 1000);
}
function process() {
var s = (new Date()).getTime();
var x = {};
for (var i = 0; i < 99999; i++) {
x["x" + i] = i * i + i;
}
var e = new Date().getTime();
$("#results").text("Execution time: " + (e - s));
//remove all previous click events
$("#StartButton").off("click");
//set the click event back to original
$("#StartButton").on("click", start);
}
function showProcessingMsg() {
$("#results").text("Still processing, please wait...");
}
.dialog-no-close .ui-dialog-titlebar {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button type="button" id="StartButton">Start</button>
<div id="results"></div>

If it is a long loop browser by itself will block all other events. You may just experience a freeze browser.
That wont be a good experience.
You can look cover the UI with an Overlay like this and inform user about the operation

Related

JQuery order of execution, synchronization issue

this seems like a pretty basic issue but I could figure out how to make sure the jquery gets executed before I check for HTML elements.
I have a loop to enter the search bar and read results and check if the search terms returns anythings.
This does what I wants except the if structures check for element even before the search button gets clicked. The website I run the extension on is https://www.avnet.com/wps/portal/us
var searchTerms = ["internal", "null?","random", "asdfsadfa", "relay"];
var i;
for (i = 2; i < searchTerms.length; i++) {
var curTerm = searchTerms[i];
$('#searchInput').val(curTerm);
$('.input-group-addon').click();
if($(".avn-noresultspage-main-section")[0]){
$(".avn-noresultspage-main-section").css( "border", "10px solid red" );
alert('This is a Null!');
}else{
alert('Not a Null!');
}
}
So with some googling I see that the chaining/ call back function is the way to go.
Callback function example
So I call the check elements function after the click in jquery. But now nothing happens. I just run through the for loop and last click was not even executed?
How should I properly chain my actions?
var searchTerms = ["internal", "null?","random", "asdfsadfa", "relay"];
var i;
for (i = 2; i < searchTerms.length; i++) {
var curTerm = searchTerms[i];
$('#searchInput').val(curTerm);
$('.input-group-addon').click(function() {
if($(".avn-noresultspage-main-section")[0]){
$(".avn-noresultspage-main-section").css( "border", "10px solid red" );
alert('This is a Null!');
}else{
alert('Not a Null!');
}
} );
}
Update: I did try to use the time out function but I seems to freeze the execution of click as well.
var searchTerms = ["internal", "null?","random", "asdfsadfa", "relay"];
var i;
for (i = 2; i < searchTerms.length; i++) {
var curTerm = searchTerms[i];
setTimeout(function() {
$('#searchInput').val(curTerm);
$('.input-group-addon').click();
}, (3 * 1000));
if($(".avn-noresultspage-main-section")[0]){
$(".avn-noresultspage-main-section").css( "border", "10px solid red" );
alert('This is a Null!');
}else{
alert('Not a Null!');
}
}
Is there a way to delay after the click?
Your modified code is definitely different in code logic. The syntax .click(function) is used to adding/modifying an event handler to "click" event. To make it work like the first version one, you need to add a chain trigger after it, like click(function(){...}).click();
And the reason why checking code's executing before the click event trigger is, the search event is probably a AJAX event and when you trigger click, Javascript will only "click" and execute the checking code without waiting for search event to return a result. You should use:
$(document).ajaxSuccess(function(){
//checking code here
});
But it apply for ALL the ajax events on this site, so the best solution imo is you should add a timeout waiting for checking code, to make sure we have the search result returned.

Disabled button raises click event after enabling

I'm trying to solve a quite simple task but stuck with JQuery behavior.
I have a HTML button which I disable (add disabled attribute) right after it get clicked to prevent multiple clicks, do something long running (i.e. update DOM with a lot of elements) and enable the button back.
Problem is that even the button is disabled jquery queues all clicks on it and raise my click handler right after it became enabled.
According to JQuery docs it should not raise events for a disabled element.
Bellow is my code. Open JS console, click two times on the button, notice couple 'START --' messages in the console.
<div>
<button id="mybtn" type="button">Find</button>
</div>
var btnElement = $('#mybtn');
btnElement.click(doClick);
function doClick() {
var btnElement = $('#mybtn');
btnElement.attr('disabled', true);
console.log('START --' + Date());
for (var i = 0; i < 70000; i++) {
var el = $('#mybtn');
var w = el.width();
w += 10;
}
console.log('STOP --' + Date());
el.attr('disabled', false);
}
Here is my solution http://jsfiddle.net/DRyxd/8/
var btnElement = $('#mybtn');
var buttonIsBusy = false;
function doHeavyJob () {
console.log('START --' + Date());
for (var i = 0; i < 70000; i++) {
var el = $('#mybtn');
var w = el.width();
w += 10;
}
var timeoutId = setTimeout (unblockTheButton, 0);
console.log('STOP --' + Date());
}
function unblockTheButton () {
console.log('unblockTheButton');
btnElement.attr('disabled', false);
buttonIsBusy = false;
}
function doClick() {
console.log('click', buttonIsBusy);
if (buttonIsBusy) {
return;
}
btnElement.attr('disabled', true);
buttonIsBusy = true;
var timeoutId = setTimeout (doHeavyJob, 0);
}
btnElement.click(doClick);
The issue here is that click-handler function has not finished and browser has not refreshed the DOM. That means that block was not yet applied to the button. You can try pushing your heavy code out of the current context like this:
function someHeavyCode () {
/* do some magic */
}
var timeoutId = setTimeout(someHeavyCode, 0);
This will push your heavy code out of the current context.Letting browser to update the DOM first and only after execute the heavy code.
While the heavy code is executed, browser (at least Chrome) kept the user input queue somewhere in other place (or most-likely other thread). And as soon as heavy code completes - it feeds the DOM with all that queued events. We need to ignore that events somehow. And I use the setTimeout with 0-time again. Letting the browser do what was queued before unblocking the button.
WARNING But be extremely careful with this technique. Browser will still be blocked and if you spawn a lot of such blocks it may hang.
See also this Why is setTimeout(fn, 0) sometimes useful? and consider using webworkers.
P.S. Blocking a user input in such a way is not a good approach, try to rethink what you are going to do, probably there is a better solution for that.

Showing warning with timeout when opening external links

I want that when a user clicks on any external link (identified by either particular id or class) on my site then he should get a popup with a counter of 10 seconds, after 10 seconds the popup should close and the user should be able to access the external URL. How can this be done? I'm able to show a warning like below but I don't know how to add timeout to it, also this is a confirm box, not a popup where I can add some div and more stuff for user to see until the counter stops.
$(document).ready(function(){
var root = new RegExp(location.host);
$('a').each(function(){
if(root.test($(this).attr('href'))){
$(this).addClass('local');
}
else{
// a link that does not contain the current host
var url = $(this).attr('href');
if(url.length > 1)
{
$(this).addClass('external');
}
}
});
$('a.external').live('click', function(e){
e.preventDefault();
var answer = confirm("You are about to leave the website and view the content of an external website. We cannot be held responsible for the content of external websites.");
if (answer){
window.location = $(this).attr('href');
}
});
});
PS: Is there any free plugin for this?
I've put together a little demo to help you out. First thing to be aware of is your going to need to make use of the setTimeout function in JavaScript. Secondly, the confirmation boxes and alert windows will not give you the flexibility you need. So here's my HTML first I show a simple link and then created a popup div that will be hidden from the users view.
<a href='http://www.google.com'>Google</a>
<div id='popUp' style='display:none; border:1px solid black;'>
<span>You will be redirected in</span>
<span class='counter'>10</span>
<span>Seconds</span>
<button class='cancel'>Cancel</button>
</div>
Next I created an object that controls how the popup is displayed, and related events are handled within your popup. This mostly is done to keep my popup code in one place and all events centrally located within the object.
$('a').live('click', function(e){
e.preventDefault();
popUp.start(this);
});
$('.cancel').click(function()
{
popUp.cancel();
});
var popUp = (function()
{
var count = 10; //number of seconds to pause
var cancelled = false;
var start = function(caller)
{
$('#popUp').show();
timer(caller);
};
var timer = function(caller)
{
if(cancelled != true)
{
if(count == 0)
{
finished(caller);
}
else
{
count--;
$('.counter').html(count);
setTimeout(function()
{
timer(caller);
}, 1000);
}
}
};
var cancel = function()
{
cancelled = true;
$('#popUp').hide();
}
var finished = function(caller)
{
alert('Open window to ' + caller.href);
};
return {
start : start,
cancel: cancel
};
}());
If you run, you will see the popup is displayed and the countdown is properly counting down. There's still some tweaks of course that it needs, but you should be able to see the overall idea of whats being accomplished. Hope it helps!
JS Fiddle Sample: http://jsfiddle.net/u39cV/
You cannot using a confirm native dialog box as this kind of dialog, as alert(), is blocking all script execution. You have to use a cutomized dialog box non-blocking.
You can use for example: jquery UI dialog
Even this has modal option, this is not UI blocking.
Consdier using the javascript setTimeout function to execute an action after a given delay
if (answer){
setTimeOut(function(){
//action executed after the delay
window.location = $(this).attr('href');
}, 10000); //delay in ms
}

JQuery Mobile Slider Not ReEnabling

So I think I am doing everything correct with my jquery mobile slider, but the control is not being re-enabled. I've made a pretty decent jsFiddle with it, in hopes someone will spot the error quickly.
On the fiddle you will see the jQuery moblie control. If you click and move the slider position the event will fire that the control value changed. If you end up changing the value more than 5 times within 20 seconds the control will lock up. You can think of this as being a cooldown period. Well after the control cools down it should be re-enabled for more mashing.
Problem is, the control never comes back from being disabled!
http://jsfiddle.net/Narq6/
Sample Javascript:
var sent = 0;
var disabled = false;
$('#slider-fill').on( 'slidestop', function()
{
send();
writeConsole(sent);
})
function send()
{
setTimeout(decrease, 4000);
sent +=1;
if(sent > 5)
{
$('#slider-fill').prop('disabled', 'disabled');
disabled = true;
}
}
function decrease()
{
if(sent > 0)
sent -= 1;
writeConsole('decrease');
writeConsole(sent);
if(sent === 0)
{
//CODE TO DISABLE HERE!!!
//LOOK HERE THIS IS WHERE I REMOVE THE DISABLE!!!
writeConsole('no longer disabled!');
$('#slider-fill').prop('disabled', '');
///YOU LOOKED TOO FAR GO BACK A LITTLE BIT :D
}
}
function writeConsole(message)
{
var miniconsole = $('#miniConsole');
var contents = miniconsole.html();
miniconsole.html(contents + message + '<br/>' );
miniconsole.scrollTop(10000);
}
You were using incorrect enable/disable syntax.
This one is a coorect syntax:
$('#slider-fill').slider('disable');
and
$('#slider-fill').slider('enable');
Here's am working example made from your jsFiddle: http://jsfiddle.net/Gajotres/djDDr/

How to smooth a jQuery script that takes time to execute?

I have a jQuery script that searches in the DOM and shows the results in a list.
There is a simplified version of the script here: http://jsfiddle.net/FuJta/1/
There is usually a large number of results, so the script can take a while to execute. (In the example above, this is simulated with a function that delays the script). So if you type too fast in the searchbox, the script prevents you from typing, and it feels bad.
How could I change my script so that you can type freely, and the results show up when they are ready. I want something like the facebook search : if you type too fast, the results are just delayed, but you can still type.
Html
<p>Type in foo, bar or baz for searching. It works, but it is quite slow.</p><br/>
<input type="text" id="search"/>
<div id="container" style="display:none">
<div class="element">foo</div>
<div class="element">bar</div>
<div class="element">baz</div>
</div>
<div id="results">
</div>​
Javascript
$(function() {
function refreshResults() {
var search = $('#search').val();
var $filtered = $('#container .element').clone().filter(function() {
var info = $(this).text();
return info.toLowerCase().indexOf(search) >= 0;
});
$('#results').empty();
$filtered.each(function() {
$('#results').append($(this));
});
}
// simulating script delay
function pausecomp(millis) {
var date = new Date();
var curDate = null;
do {
curDate = new Date();
}
while (curDate - date < millis);
}
$('#search').keyup(function() {
pausecomp(700);
refreshResults();
});
});​
One solution could to refresh the results only when pressing enter. This way, the delay for searching the results feels ok. But I would prefer if I just delay the results and let the user freely type.
You should perform a search like this using asynchronous techniques. No doubt Facebook uses some sort of AJAX to request search results - which means getting the results from the server. This will help prevent the UI 'freeze' that you are currently experiencing.
Here is a very simple example of what you can try (it uses JQuery for the AJAX requests):
var searchInProgress = false;//used to work out if a search is in progress
var searchInQueue = false;//used to flag if the input data has changed
function getSearchResults(searchText){
if (searchInProgress ) {
searchInQueue = true;
return;
}
searchInProgress = true;
searchInQueue = false;
$.getJSON("URL",//URL to handle AJAX query
{ searchText: searchText},//URL parameters can go here
function (data) {
//handle your returned data here
searchInProgress = false;
if (searchInQueue){//text has changed, so search again
getSearchResults();
}
});
}
$('#search').keyup(function() {
getSearchResults($(this).val());
});
A few things to note: It is probably a good idea to handle failed AJAX requests to ensure you can reset the searchInProgress flag as needed. Also, you can add delays after the keyup as desired, but this all depends on how you want it too work.
From How to delay KeyPress function when user is typing, so it doesn't fire a request for each keystroke? :
var timeoutId = 0;
$('#search').keyup(function () {
clearTimeout(timeoutId); // doesn't matter if it's 0
timeoutId = setTimeout(refreshResults, 100);
});
It does what I want indeed.
Here's a solution that divides the search process into steps, returning flow to the browser during the process to allow the UI to respond.
$(function() {
function searchFunc($element,search) {
var info = $element.text();
return info.toLowerCase().indexOf(search) >= 0;
}
var searchProcessor = null;
function restartSearch() {
console.log('Restarting...');
// Clear previous
if (searchProcessor != null) {
clearInterval(searchProcessor);
}
$('#results').empty();
// Values for the processor
var search = $('#search').val();
var elements = $('#container .element').get();
console.log('l:',elements,elements.length);
// Start processing
searchProcessor = setInterval(function() {
if (elements.length == 0) {
// Finished searching all elements
clearInterval(searchProcessor);
searchProcessor = null;
console.log('Finished.');
} else {
console.log('Checking element...');
var $checkElement = $(elements.shift());
if (searchFunc($checkElement, search)) {
$('#results').append($checkElement.clone());
}
}
}, 10);
}
$('#search').keyup(function() {
restartSearch()
});
});
It only processes one element each time. That should probably be increased so it handles perhaps 10 or 100 each time around, but the important point is that the work is divided into chunks.
This solution should also be faster than the original because it doesn't clone() everything, only the elements that were matched.

Categories

Resources