Karl_S
Karl_S

Reputation: 3564

Google Apps Script Web App - crossing server side calls(?)

I have a web app I am writing and wanted updates to write back to the server real time. On the client side I have this function running and capturing each input. The inputs are currently all checkboxes. The problem is that I can check the boxes faster than the script works on them and therefore end up with unexpected results. So I need to slow the user down between selections or make sure each server call completes before the next one begins. How do I do this?

This is the start of the client side script section. There are other functions such as the success and failure handlers.

<script>
$("form").change(function(e) {

if (e.target.type && e.target.type === 'checkbox') {
  //A checkbox was changed, so act on it
  var name = e.target.value.substr(0,e.target.value.lastIndexOf("_"));
  var position = e.target.value.substr(e.target.value.lastIndexOf("_") + 1);
  var passArray = [name, position];

  if (e.target.checked) {
    //Add the value to the person's Assigned Position
    google.script.run.withSuccessHandler(editChangeReturnedFromServer).withFailureHandler(editFailed).setWSAssignedPosition(passArray);
  } else {
    //Remove the value from the peson's Assigned Position
    google.script.run.withSuccessHandler(editChangeReturnedFromServer).withFailureHandler(editFailed).clearWSAssignedPosition(passArray);
  }
  return;
}

 ...(other functions)...
 </script>

If the user checks boxes quickly, the calls to the server side setWSAssignedPosition() function seem to cross or even duplicate, as all the correct values are not added to the spreadsheet OR multiple copies of the same are added. If I slow the end user down with alert('I am changing that'); right before the google.script.run... line then all works fine. I actually watched as one line in the spreadsheet was replaced with an entry that came later. But that was intrusive and quickly became annoying.

I am not interested in a Submit or Apply button. I really want this to happen on the fly.

The server side function is:

//Set the Assigned Position for the person passed to the function
function setWSAssignedPosition(passedArray){

  var name = passedArray[0];
  var position = passedArray[1];

  //Get the entries from the filled out Position Requests in the appropriate sheet
  var validWSRequests = getValidWSRequests();
  var foundWSRequest = false;

  for (i in validWSRequests) {
    if (validWSRequests[i].participantName === name){
      ws_sheet.getRange(validWSRequests[i].sheetrow + 2 , ws_headers[0].indexOf("Assigned Position") + 1).setValue(position);
      foundWSRequest = true;
      break;

    }
  }

  if (!foundWSRequest) {
    var WSOthersAssigned = getRowsData(ws_norequests_sheet);
    var WSOthersAssigned_headers = ws_norequests_sheet.getRange(1, 1, 1, ws_norequests_sheet.getLastColumn()).getValues();
    //Get the first empty row on the sheet for those who didn't fill out the form in case we need it.
    var firstEmptyRow = getFirstEmptyRowWholeRow(ws_norequests_sheet);

    if (WSOthersAssigned.length < 1){
        //No records exist at all, so add the first
          ws_norequests_sheet.getRange(firstEmptyRow, WSOthersAssigned_headers[0].indexOf("Assigned Position") + 1).setValue(position); 
          ws_norequests_sheet.getRange(firstEmptyRow, WSOthersAssigned_headers[0].indexOf("PARTICIPANT NAME") + 1).setValue(name); 
     }else {

      for (i in WSOthersAssigned){
        var seeme = i;
        if (WSOthersAssigned[i].participantName === name) {
          //Found a record so edit it
          ws_norequests_sheet.getRange(WSOthersAssigned[i].sheetrow + 2 , WSOthersAssigned_headers[0].indexOf("Assigned Position") + 1).setValue(position);
          break;

        } else {
          //No record found, so append it
          ws_norequests_sheet.getRange(firstEmptyRow, WSOthersAssigned_headers[0].indexOf("Assigned Position") + 1).setValue(position); 
          ws_norequests_sheet.getRange(firstEmptyRow, WSOthersAssigned_headers[0].indexOf("PARTICIPANT NAME") + 1).setValue(name);
          break;

        }
      }
  }
  }
// This didn't help  
//  SpreadsheetApp.flush();
return [name, true, position];

}

ws_norequests_sheet and ws_norequests_sheet are defined globally to get the appropriate sheet in the spreadsheet where these items are stored. Depending on the initial source, the data is saved in one of two sheets. I am currently testing where all the data is going into the second sheet as these checkboxes are all stacked on top of each other and therefore quickly accessed

Upvotes: 0

Views: 174

Answers (1)

Marcin
Marcin

Reputation: 41

That's the nature of asynchronous server calls. You always need to be mindful that the calls may not return in the same order in which you sent them. You always need to take care and use the success and failure callbacks to properly respond.

There are several ways to deal with your issue (in the way that you want) and most involve preventing the user from triggering the change action on the form's elements and then allowing them to do so only after the server calls return.

One of the simpler ways would be to disable the form elements on change and the re-enable them in your callbacks.

So first disable them on change:

$("form").change(function(e) {
  if (e.target.type && e.target.type === 'checkbox') {
    $("input[type=checkbox]", "form").attr("disabled", "disabled");
  //Your change code here  
  }     
}

Then in both your success and fail handlers, re-enable them:

function editChangeReturnedFromServer(response) {
  //your code here to work with server data
  $("input[type=checkbox]", "form").removeAttr("disabled");
}
function editFailed(error) {
  //your code here to handle error
  $("input[type=checkbox]", "form").removeAttr("disabled");
}

Other options would be to use something like jQuery UI to create a modal dialog that would prevent the user from clicking options, which you could destroy on success. This is nice as it very clearly indicates to the user that something is happening where disabling inputs may not be as obvious to them.

Basically, you need to ensure that the user cannot trigger another change until the previous one has completed. Alternatively, you could in theory user a global variable to track the unprocessed checkbox values and another to track if a server call is in progress, but this is a lot of complexity for such a simple thing.

To be frank though, I don't really agree with your approach from a UI perspective. The expected behaviour of a checkbox is that it collects a user's input and only sends that to a server when a submit button is clicked. As an end user, I would expect to wait when 'submitting' something, but not when checking a box. Having to wait any amount of time (even fractions of a second) would personally annoy me. I would much rather check several options and wait for a longer submit.

Depending on what's actually being done with the return values, a more elegant solution might be to take care of the actions on the client side immediately without involving the server. Only sending it data once the user has finished their selections.

For example, if checking one box should show the user 3 inputs, you could:

$("#check1").click(function(){
  $(".showOnCheck1").toggle(this.checked);
});
.showOnCheck1 {display:none;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>
  <input type="checkbox" id="check1" value="check1"><label for="check1">Check box 1</label>
</p>
<p class="showOnCheck1">
  <label for="text1">Text Box 1</label><input type="text" id="text1">
</p>
<p class="showOnCheck1">
  <label for="text2">Text Box 2</label><input type="text" id="text2">
</p>
<p class="showOnCheck1">
  <label for="text3">Text Box 3</label><input type="text" id="text3">
</p>

As you can see with the above, there's no delay at all and no need to slow the user down or wait for the server, but it does depend on what you're doing with the response. The more logic you can put on the client side, the less the user has to wait for the server.

Upvotes: 1

Related Questions