I am trying to create a javascript plugin that allows you to hook into the save operation. I want some logic wrapped around the logic though:
Show a spinner
Execute save hook-in
Execute save logic
Remove Spinner
here is an example of my code:
function save(){
var settings = getSettings();
showSpinner();
// Hook-in call
if(settings.onSave)
settings.onSave();
saveState();
removeSpinner();
}
showSpinner() just hides save/cancel buttons and shows a spinner in its place.
hideSpinner() does the opposite.
saveState() saves the data from input fields to their data attributes.
settings.onSave() can be whatever code is passed in to the config by a user of the plugin.
When I run the process the spinner never shows up even though the hook-in I have defined takes ~1.5 seconds to return. If I pause the code in the debugger, the spinner does have a chance to show and it does disappear after the operation is complete.
What is the best approach to resolve this? I am not at all familiar with asynchronous design patterns within javascript. I know there is a single thread that the javascript can run on but I don't know why the save operation is getting in the way of showing the spinner.
Your browser won't update the interface until Javascript gives back control to the browser, meaning you need to return from function before the browser will show the spinner. Use setTimeout to give control back to the browser briefly.
function save(){
showSpinner();
setTimeout(function(){
var settings = getSettings();
// Hook-in call
if(settings.onSave)
settings.onSave();
saveState();
removeSpinner();
}, 0 );
}
Related
Some of the scripts that I run take a long time and users might get concerned that a script stopped working if they can't see the status/step. I have added a spinner to the Sidebar to at least indicate that the script started running, but I would like to do more than that.
Ideally, I would be able to directly update the Sidebar contents from the GAS, but I gather than is not possible because of sandboxing. I have seen other questions and answers that discuss using success handlers in a daisy chain like this:
function uploadActivities(){
google.script.run.withSuccessHandler(onSuccess).activities_upload();
}
function onSuccess(lastStatus){
$('#codestatus').text(lastStatus);
google.script.run.step_two();
}
It is a hack and it would require me to split the code into smaller steps and pass values to the UI, which don't belong in the UI, and back to the code. I really don't like that approach and maintenance could be a bear.
I have tried creating a var in GAS and updating that value as the code progresses. However, I can't find a way to get the UI to periodically check until the code execution is complete AND to successfully update the UI after each step.
Here is the code I have created:
function uploadActivities(){
google.script.run.activities_upload();
getStatus();
}
function getStatus(){
var isActive = true;
while(isActive){
var lastStatus = google.script.run.getStatus();
$('#codestatus').text(lastStatus);
if(lastStatus === 'Complete'){ isActive = false; }
}
}
In GAS I use this code:
var codeStatus = 'start';
function getStatus(){
return codeStatus;
}
function activities_upload(){
codeStatus = 'Started Execution';
...
codeStatus = 'Extracting Values';
...
codeStatus = 'Uploading Activities';
...
codeStatus = 'Complete';
}
It runs the required code, and even updates the #codestatus div with the first value, but it doesn't get any values beyond the first value. Additionally, it creates a continuous loop if there is an error in the code execution, so that isn't good either.
Is there a good, efficient, and safe way to complete this approach? Or, is there a better way to notify the user of the code execution status so they don't get worried if it takes a while, and can tell if there has been an issue?
I have struggled with this for some time. Unfortunately, I don't have a good fix for your approach, but I can show what I finally did and it seems to be working.
First, create an easy way to send a toast to your users.
function updateStatus_(alert,title){
var ui = SpreadsheetApp.getActiveSpreadsheet();
var title_ = title!=""?title:"";
ui.toast(alert,title_);
}
Second, as required, use the toast to update the user.
function activities_upload(){
updateStatus_('Started Execution');
...
updateStatus_('Extracting Values');
...
updateStatus_('Uploading Activities');
...
updateStatus_('Complete');
}
This will alert the user with a temporary message as the code progresses and not require the user to clear an alert.
Please note that if the steps progress rapidly the user will see the toast flash on the screen only to be quickly replaced by the next toast. So, make sure you don't have too many throughout your execution.
I want to display a spinner before some complicated function, i.e. dummyCounter(). The code looks like:
function add1() {
msg.html('start counting~<br \>');
document.body.appendChild(div);
spinner.spin(div);
// display spinner before doing stuff
dummyCounter();
}
jsfiddle: http://jsfiddle.net/eGB5t/
However the spinner shows after the dummyCounter() function is finished.
I try to use callback to force spinner display earlier but still no good. Can anybody help? Thanks.
jsfiddle: http://jsfiddle.net/eGB5t/2/
You have a thinking failure. Spinners are usually used for asynchronous tasks, so you can see that there is something in progress. A callback is then used to remove the spin when the async action has finished, since you cannot tell before it starts when it will finish.
I made up a quick example to show you, how such an async function would work in this case, and you can clearly see how the spinner appears slightly before "google finished" appears.
http://jsfiddle.net/eGB5t/4/
I added the following instead of your counting method:
$.ajax("http://google.de").always(function() {
msg.append("google finished");
});
You add the spin before you count, then it counts, then you could remove the spinner. This is perfecty fine. Thing is, if you would count to let's say 9999999999999 (so it would take some seconds), a normal for loop like you're doing is completely blocking the browser, so you won't have any repaints (and therefore no spinner) at all, while the loop is running.
What you would have to do (in this case) is to introduce a worker to have multithreading functionality in javascript.
var x;
function add1() {
msg.html('start counting~<br \>');
spinner.spin(div);
x= setTimeout(document.body.appendChild(div),500);
}
I use Ajax load icons all the time for when I do ajax requests.
I want to know if it's possible to accomplish the same thing with just regular javascript code. For example:
$('button').on('click', function(){
showLoadingBar();
longProcess();
hideLoadingBar();
});
longProcess() is just a function that can take 1-3 seconds to process (it does a lot of calculations and manipulates the DOM but doesn't do any ajax requests).
Right now, the browser halts/freezes during these 1-3 seconds, so what I would rather do is show a loading icon during this time. Is this possible? The code I have above pretty much ignores showLoadingBar().
The DOM won't be updated until the current Javascript process ends, so you can't do it just like that. You can however use setTimeout to get around that:
showLoadingBar();
setTimeout(function() {longProcess(); hideLoadingBar(); }, 1);
What happens above, in case it isn't clear, is that showLoadingBar is executed, then a timeout is set up. The current process will then end and allow the DOM to update with the loading bar, before the timeout is invoked, very shortly after. The handler executes your heavy function and when it's done, hides the loading bar again.
The following will give you control over the action on click. What this means is you can disable the clicking ability till it has finished running. But also i've including setTimeout which returns control to the browser (thus removing that annoying "lockup" feeling) and in the timeout function we preform our long process then re-enable the button! VIOLA!
function startProc() {
var $this = $(this);
$this.off("click", startProc);
showLoadingBar();
setTimeout(function() {
longProcess();
hideLoadingBar();
$('button').on('click', startProc);
});
}
$('button').on('click', startProc);
Dude,
Use the .bind method, which in this case is performed so:
$('button').bind('click', function(){
showLoadingBar();
longProcess();
hideLoadingBar();
});
I have a function running on document load that copies the contents of a select object to other select boxes (to conserve network bandwidth).
The function is taking a few seconds to complete, so I wanted to mask the main div (to give the user the idea that something is happening).
Unfortunately, the mask is not showing up until after the function completes:
// I want the mask to show immediately here, but never gets shown
$('#unassignedPunchResults').mask('Getting results');
$('.employeeList').each(function (i) {
// this is freezing the browser for a few seconds, the masking is not showing
$('#employeeList option').clone().appendTo(this);
});
$('#unassignedPunchResults').unmask();
How can I interrupt the javascript after the mask() call to flush that event and continue, so the user can see the mask while the longer processing (the each()) processes?
Put the rest of the code in a setTimeout(function() { ... }, 0) call.
I've been thinking a while about this.
The first solution is to use the settimeout function.
However it could be a little 'dirty' because you add an arbitrary delay. A more proper logic would be to execute the $('.employeeList').each(function (i)... function after the mask function has benne executed and rendered.
Jquery allows us to do that with the deferred functions like then which xecutes after a deferred condition has been satisfied.
So try with:
// I want the mask to show immediately here, but never gets shown
$('#unassignedPunchResults').mask('Getting results').then(function(){
$('.employeeList').each(function (i) {
// this is freezing the browser for a few seconds, the masking is not showing
$('#employeeList option').clone().appendTo(this);
});
});
In general, using settimeout with an arbitrary number of ms is a solution which works for simple cases, but if you have multiple settimouts in a complex code then you could have synchronizaton problems.
or use a worker, but then you need to discard msie < 10
or break up your calculations in segments that run for less than 500 ms and use setinterval to sread the loading over 5 seconds.
google simulate threading in javascript for code examples.
I have a function called save(), this function gathers up all the inputs on the page, and performs an AJAX call to the server to save the state of the user's work.
save() is currently called when a user clicks the save button, or performs some other action which requires us to have the most current state on the server (generate a document from the page for example).
I am adding in the ability to auto save the user's work every so often. First I would like to prevent an AutoSave and a User generated save from running at the same time. So we have the following code (I am cutting most of the code and this is not a 1:1 but should be enough to get the idea across):
var isSaving=false;
var timeoutId;
var timeoutInterval=300000;
function save(showMsg)
{
//Don't save if we are already saving.
if (isSaving)
{
return;
}
isSaving=true;
//disables the autoSave timer so if we are saving via some other method
//we won't kick off the timer.
disableAutoSave();
if (showMsg) { //show a saving popup}
params=CollectParams();
PerformCallBack(params,endSave,endSaveError);
}
function endSave()
{
isSaving=false;
//hides popup if it's visible
//Turns auto saving back on so we save x milliseconds after the last save.
enableAutoSave();
}
function endSaveError()
{
alert("Ooops");
endSave();
}
function enableAutoSave()
{
timeoutId=setTimeOut(function(){save(false);},timeoutInterval);
}
function disableAutoSave()
{
cancelTimeOut(timeoutId);
}
My question is if this code is safe? Do the major browsers allow only a single thread to execute at a time?
One thought I had is it would be worse for the user to click save and get no response because we are autosaving (And I know how to modify the code to handle this). Anyone see any other issues here?
JavaScript in browsers is single threaded. You will only ever be in one function at any point in time. Functions will complete before the next one is entered. You can count on this behavior, so if you are in your save() function, you will never enter it again until the current one has finished.
Where this sometimes gets confusing (and yet remains true) is when you have asynchronous server requests (or setTimeouts or setIntervals), because then it feels like your functions are being interleaved. They're not.
In your case, while two save() calls will not overlap each other, your auto-save and user save could occur back-to-back.
If you just want a save to happen at least every x seconds, you can do a setInterval on your save function and forget about it. I don't see a need for the isSaving flag.
I think your code could be simplified a lot:
var intervalTime = 300000;
var intervalId = setInterval("save('my message')", intervalTime);
function save(showMsg)
{
if (showMsg) { //show a saving popup}
params=CollectParams();
PerformCallBack(params, endSave, endSaveError);
// You could even reset your interval now that you know we just saved.
// Of course, you'll need to know it was a successful save.
// Doing this will prevent the user clicking save only to have another
// save bump them in the face right away because an interval comes up.
clearInterval(intervalId);
intervalId = setInterval("save('my message')", intervalTime);
}
function endSave()
{
// no need for this method
alert("I'm done saving!");
}
function endSaveError()
{
alert("Ooops");
endSave();
}
All major browsers only support one javascript thread (unless you use web workers) on a page.
XHR requests can be asynchronous, though. But as long as you disable the ability to save until the current request to save returns, everything should work out just fine.
My only suggestion, is to make sure you indicate to the user somehow when an autosave occurs (disable the save button, etc).
All the major browsers currently single-thread javascript execution (just don't use web workers since a few browsers support this technique!), so this approach is safe.
For a bunch of references, see Is JavaScript Multithreaded?
Looks safe to me. Javascript is single threaded (unless you are using webworkers)
Its not quite on topic but this post by John Resig covers javascript threading and timers:
http://ejohn.org/blog/how-javascript-timers-work/
I think the way you're handling it is best for your situation. By using the flag you're guaranteeing that the asynchronous calls aren't overlapping. I've had to deal with asynchronous calls to the server as well and also used some sort of flag to prevent overlap.
As others have already pointed out JavaScript is single threaded, but asynchronous calls can be tricky if you're expecting things to say the same or not happen during the round trip to the server.
One thing, though, is that I don't think you actually need to disable the auto-save. If the auto-save tries to happen when a user is saving then the save method will simply return and nothing will happen. On the other hand you're needlessly disabling and reenabling the autosave every time autosave is activated. I'd recommend changing to setInterval and then forgetting about it.
Also, I'm a stickler for minimizing global variables. I'd probably refactor your code like this:
var saveWork = (function() {
var isSaving=false;
var timeoutId;
var timeoutInterval=300000;
function endSave() {
isSaving=false;
//hides popup if it's visible
}
function endSaveError() {
alert("Ooops");
endSave();
}
function _save(showMsg) {
//Don't save if we are already saving.
if (isSaving)
{
return;
}
isSaving=true;
if (showMsg) { //show a saving popup}
params=CollectParams();
PerformCallBack(params,endSave,endSaveError);
}
return {
save: function(showMsg) { _save(showMsg); },
enableAutoSave: function() {
timeoutId=setInterval(function(){_save(false);},timeoutInterval);
},
disableAutoSave: function() {
cancelTimeOut(timeoutId);
}
};
})();
You don't have to refactor it like that, of course, but like I said, I like to minimize globals. The important thing is that the whole thing should work without disabling and reenabling autosave every time you save.
Edit: Forgot had to create a private save function to be able to reference from enableAutoSave