How to call google apps script server-side functions synchronously? - google-apps-script

I wrote a google apps script code, it will open a google spread sheet, and list the values by row, but there are 2 problems:
1. The output by random order.
2. The div text which id "loding" change to "Finished!" before list all of values.
I thought the script will wait for server-side function return when I run it by "withSuccessHandler()", but it's not.
How can I correct it?
index.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function jsListValue() {
// Get count.
google.script.run.withSuccessHandler(function(count) {
// List all values.
for( count; count>0; count=count-1) {
// Get a value.
google.script.run.withSuccessHandler(function(content) {
// Shows in "output".
var new_div = document.createElement("div");
new_div.appendChild(document.createTextNode(content));
document.getElementById("output").appendChild(new_div);
}).gsGetValue(count);
}
// Change loding notice.
document.getElementById("loding").innerHTML = "Finished!";
}).gsGetCount();
}
</script>
</head>
<body onload="jsListValue()">
<div id="output"></div>
<div id="loding">Loding now...</div>
</body>
</html>
code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function gsOpenSheet() {
// Return sheet of the note data.
return (SpreadsheetApp.openById("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getSheetByName("sheet1"));
}
function gsGetCount() {
// Return last row index in this sheet.
return (gsOpenSheet().getLastRow());
}
function gsGetValue(index) {
// Return value in the (index,1).
return (gsOpenSheet().getRange(index,1).getValue());
}

GAS is very similar to Javascript, and all calls to Google's server side functions are asynchronous. You cannot change this (at least I haven't seen any doc reg. that).
What you can do, is, use a callback function on the client side which polls the server for a "success" return value. It'll keep polling it say for 1 minute, or else exit. Let it set a client flag to "true" if the success value is returned by the server. Nothing should proceed on the client side, unless the flag is true. In this way, you can control what happens on the client side.

You want to use withSuccessHandler Docs
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function onSuccess(numUnread) {
var div = document.getElementById('output');
div.innerHTML = 'You have ' + numUnread
+ ' unread messages in your Gmail inbox.';
}
google.script.run.withSuccessHandler(onSuccess)
.getUnreadEmails();
</script>
</head>
<body>
<div id="output"></div>
</body>
</html>

google.script.run is asynchronous, which means that is impossible to predict in what order gsGetValue(count) will return. When you're using withSuccessHandler you must perform the next action inside the callback function.
My suggestion is to get all the range you want and put it on an array. You can create a serverside function to do this. The code would look like this:
//Serverside function
function getDataForSearch() {
const ws = SpreadsheetApp.openById("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getSheetByName("sheet1");
return ws.getRange(1,1,ws.getLastRow(),1).getValues();
}
getValues() will return an array of arrays that should contain all values from the range specified. More information about here
On your client-side, the script should be like this:
//array to get the data from getRange()
var data;
function jsListValue() {
google.script.run.withSuccessHandler(function(dataReturned){
data = dataReturned;
var new_div;
data.forEach(function(r){
new_div = document.createElement("div");
new_div.appendChild(document.createTextNode(r[0));
document.getElementById("output").appendChild(new_div);
});
document.getElementById("loding").innerHTML = "Finished!";
}).getDataForSearch();
}

As hinted by #Iuri Pereira If you return a parameter from the google script code, and then use it in the page javascript, then the procedure runs synchronously.
HTML:
<button onclick="google.script.run.withSuccessHandler(js_function).gs_code();">action</button>
GS:
function gs_code() {
// do Something;
return true;
}
HTML javascript:
function js_function(gs_return_value) {
console.log(gs_return_value);
}

Related

Apps-script function with dynamic parameter

I am testing the app script platform and I have a doubt when using this code called from HTML file:
JSON.parse(<?= JSON.stringify(getDataFromSheet("tyreUse", "valueSearched")); ?>);
If I set the string value directly it works.
If I try to pass a variable that is declared in it does not recognize it. How can I pass a JS variable to the app script function like next example?
let value_searched = "cars";
JSON.parse(<?= JSON.stringify(getDataFromSheet("tyreUse", value_searched)); ?>);
Scriptlets like <?= ?> are used in html templates to load data from the server into html pages prior to rendering. If you want to pass data back to a server side function then you can use google.script.run and there are restrictions on the data types that you can pass.
google.script.run
Here is an example of getting data from spreadsheet dynamically. I typically build my page and then use an anonymous function of the form (function () {}()); to get the data from spreadsheet and populate the HTML elements with the values.
Create an HTML file HTML_Demo:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<input id="A8SBwf" type="text">
<input id="gNO89b" type="button" value="Click Me" onclick="buttonOnClick()">
<script>
function buttonOnClick() {
try {
google.script.run.withSuccessHandler(
function(response) {
document.getElementById("A8SBwf").value = response;
}
).getCellA1();
}
catch(err) {
alert(err);
}
}
</script>
</body>
</html>
Then in Code.gs create the getCellA1:
function getCellA1() {
try {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var range = sheet.getRange("A1");
return range.getValue();
}
catch(err) {
return err.message;
}
}

Spreadsheet Alarm Toggle

At the moment I am searching for a method to toggle an alarm for all Users who are looking into the spreadsheet:
// creates a custom menu when the spreadsheet is opened
function onOpen() {
var ui = SpreadsheetApp.getUi()
.createMenu('SWAT Pager')
.addItem('Open SWAT Pager', 'openCallNotifier')
.addToUi();
// you could also open the call notifier sidebar when the spreadsheet opens
// if you find that more convenient
// openCallNotifier();
}
// opens the sidebar app
function openCallNotifier() {
// get the html from the file called "Page.html"
var html = HtmlService.createHtmlOutputFromFile('Page')
.setTitle("SWAT Pager");
// open the sidebar
SpreadsheetApp.getUi()
.showSidebar(html);
}
// returns a list of values in column H
function getColumnE() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Operationen");
// get the values in column H and turn the rows into a single values
return sheet.getRange(1, 8, sheet.getLastRow(), 1).getValues().map(function (row) { return row[0]; });
}
Thats the code.gs
Now what I would like to do is to set a dropdown menu where i can choose:
Warning / No Warning.
When i choose 'warning' I want an alarm sound playing, and on 'no warning' no sound.
Ive tried to change this script into it:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="message">Keine Alarmierung </p>
<audio id="Alarmierung">
<source src="http://banhammer.bplaced.net/audio/spaceship_alarm.mp3" type="audio/mp3">
Your browser does not support the audio element.
</audio>
<script>
var lastTime = []; // store the last result to track changes
function checkCalls() {
// This calls the "getColumnE" function on the server
// Then it waits for the results
// When it gets the results back from the server,
// it calls the callback function passed into withSuccessHandler
google.script.run.withSuccessHandler(function (columnE) {
for (var i = 0; i < columnE.length; i++) {
// if there's a difference and it's a call, notify the user
if (lastTime[i] !== columnE[i] && columnE[i] === "Alarmierung") {
notify();
}
}
// store results for next time
lastTime = columnE;
console.log(lastTime);
// poll again in x miliseconds
var x = 1000; // 1 second
window.setTimeout(checkCalls, x);
}).getColumnE();
}
function notify() {
document.getElementById("Alarmierung").play();
}
window.onload = function () {
checkCalls();
}
</script>
</body>
</html>
But unfortunately it would not work. I would appreciate any suggestions from the community.
Proposed Solution
Your code was generally in the right direction, but it seemed to be doing some necessarily complicated things like:
return sheet.getRange(1, 8, sheet.getLastRow(), 1).getValues().map(function (row) { return row[0]; });
To get a value from E2, which can be done like this:
return sheet.getRange("E2").getValue()
Though maybe you mean to aggregate the values like this for some reason, though it seemed to me that you were tying to get too much working too quickly and were getting confused with what goes where.
In any case, I took your code and simplified it to make an alarm work, which is what I believe your question was really directed towards:
Code.gs
// This function creates a pop up called "Alarm"
function alarm(){
var html = HtmlService.createHtmlOutputFromFile('Page')
.setWidth(400)
.setHeight(300);
SpreadsheetApp.getUi()
.showModalDialog(html, "Alarm");
}
// This is the function called by the client-side js in "Page.html"
// It returns a true or false value depending on the contents of E2
function checkAlarm() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Operationen");
return sheet.getRange("E2").getValue() == "ALARM"
}
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="message">Keine Alarmierung </p>
<script>
// This is the call to the checkAlarm function in Code.gs.
// It returns a true or false value which is passes into the function
// playAlarm
google.script.run.withSuccessHandler(playAlarm).checkAlarm();
function playAlarm(bool){
// If the value returned from "checkAlarm" is true
// It created the HTML audio element and makes it play.
if (bool == true){
let audio = document.createElement("audio");
audio.setAttribute("controls", "controls");
audio.setAttribute("autoplay", "autoplay"); // So it plays automatically
audio.setAttribute("style", "display:none"); // So the player is hidden
let source = document.createElement("source");
source.setAttribute("src","https://onlineclock.net/audio/options/police-car.mp3");
source.setAttribute("type","audio/mpeg");
audio.appendChild(source);
document.appendChild(audio);
}
}
</script>
</body>
</html>
Steps to get working.
Paste in the code.
Make sure it is not called onOpen - for an onOpen function to have access to the UI, it needs to be installed not a simple trigger. So go to the triggers in your project and install an onOpen one using the alarm function.
Make sure the value of E2 in Operationen is ALARM.
Refresh the page and the alarm should play!
Notes
Your sound source http://banhammer.bplaced.net/audio/spaceship_alarm.mp3 unfortunately does not work within Apps Script. I believe this is because it is an http origin and not a https origin. This is why I replaced this with another sound https://onlineclock.net/audio/options/police-car.mp3 to get it working.
The script only checks for a match with ALARM in cell E2 in Operationen. Any other value will result in no alarm.
References
ModalDialog
Installable Triggers
Client to Server Communication

How to handle errors and success at the same time on a Google Apps Script Webapp

I am wondering how the client-server communication with Google Apps Script Webapps works. In the html page I call my functions with
google.script.run.doSomething();
and I can add a
withFailureHandler(onFailure);
OR
withSuccessHandler(onSuccess) but I can't add both..
So when I want to call a server-side function I normally want to handle the response in the UI ?differently if it is a success than a failure, right? But I want to handle both right? So why do I have to choose between one of them?
Also another problem is that I could not find any information about the actual errors you can have on the server side so that your withFailureHandler(onFailure); can handle them. Can I just do a throw new Error("everything is broken - tell that the user")? If there are permission errors, are they handled? How do I generate an error on the server side so that I can handle it on the client side properly?
It is true that the documentation does not specify it clearly, but you can implement both a success and error handler simultaneously
Sample:
<html>
<head>
<base target="_top">
<script>
google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onFailure).getUnreadEmails();
function onSuccess(numUnread) {
var div = document.getElementById('output');
div.innerHTML = 'You have ' + numUnread + ' unread messages in your Gmail inbox.';
}
function onFailure(error) {
var div = document.getElementById('output');
div.innerHTML = "ERROR: " + error.message;
}
</script>
</head>
<body>
<div id="output"></div>
</body>
</html>
UPDATE
To simulate a failure based on the documentaiton sample, change the working code.gs part
from
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index');
}
function getUnreadEmails() {
return GmailApp.gotInboxUnreadCount();
}
to
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index');
}
function getUnreadEmails() {
return GMailApp.gotInboxUnreadCount();
}
After deploying the WebApp, the html will output:
As written in the official documentation,
You can use any combination and any order of withSuccessHandler(), withFailureHandler(), and withUserObject(). You can also call any of the modifying functions on a script runner that already has a value set. The new value simply overrides the previous value.
Sample:
const gsr = google.script.run;
const runnerF = gsr.withFailureHandler(onFailure);
const runnerS1 = runnerF.withSuccessHandler(onSuccess1);
const runnerS2 = runnerF.withSuccessHandler(onSuccess2);
/*Both runners have the same failure handlers, but different success handlers*/
runnerS1.callServer();//failure => onFailure;success => onSuccess;
runnerS2.callServer2();//failure => onFailure;success => onSuccess2;

Automated download of file from Drive in Web App?

I'm trying to write a polling web app that checks Google Drive and automatically downloads files without user interaction.
Using ContentService I have managed to get things working when I place the code in the doGet function.
However this only works once and there does not appear to be a way to refresh or reload the page automatically on a timer event.
Using a SetTimeout on the client side javascript I can get a function on the server side to automatically trigger at certain intervals but then I am stuck with what to do with the output from ContentService.
The on Success call back will not accept the output from createTextOutput.
My solution does not not need to be deployed and I'm happy to execute from the editor if that expands my choices.
So once I have the output from createTextOutput on my server side what am I supposed to do with it to get it back to the client in order to cause the file download?
I have included the code if that helps.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
setTimeout(
function ()
{
document.getElementById('results').innerHTML = 'Event Timer';
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onFailure)
.fetchFromGoogleDrive();
}, 60000);
function onSuccess(sHTML)
{
document.getElementById('results').innerHTML = 'File Downloaded ' + sHTML;
}
function onFailure(error)
{
document.getElementById('results').innerHTML = error.message;
}
</script>
</head>
<body>
<div id="results">Waiting to DownLoad!</div>
id="Fetch">Fetch!</button>
</body>
</html>
function doGet() {
Logger.log('doGet');
return HtmlService.createHtmlOutputFromFile('form.html');
}
function fetchFromGoogleDrive() {
//Logger.Log('fetchFromGoogleDrive');
var fileslist = DriveApp.searchFiles("Title contains 'Expected File'");
if (fileslist.hasNext()) {
//Logger.Log('File found');
var afile = fileslist.next();
var aname = afile.getName();
var acontent = afile.getAs('text/plain').getDataAsString();
var output = ContentService.createTextOutput();
output.setMimeType(ContentService.MimeType.CSV);
output.setContent(acontent);
output.downloadAsFile(aname);
return afile.getDownloadUrl();
}
else
{
//Logger.Log('No File Found');
return 'Nothing to download';
}
//Logger.log('All files processed.');
}
EDIT: Different answer after clarification.
If this is intended to run automated as a webapp what I would do is return the getDownloadUrl and create a new iFrame using that that as the source.
Apps Script
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function getDownloadLink(){
//slice removes last parameter gd=true. This needs to be removed. slice is a hack you should do something better
return DriveApp.getFileById("0B_j9_-NbJQQDckwxMHBzeVVuMHc").getDownloadUrl().slice(0,-8);
}
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="dlBox"></p>
</body>
<script>
function buildLink(res){
var dlBox = document.createElement("iframe");
dlBox.src = res;
document.getElementById("dlBox").appendChild(dlBox)
}
//automate this as you need
google.script.run
.withSuccessHandler(buildLink)
.getDownloadLink();
</script>
</html>

Pass array from server side function using google script and html

I have an html page that will be served to a google sheet app to be used as a UI. I would like to access an array from a server side function within the html file. I am having trouble accessing a returned array. Here is what I have:
in html file:
<div id="id1">
Starting 1
</div>
<div id= "id2">
Starting 2
</div>
<script type="text/javascript">
document.getElementById("id1").innerHTML = "A change";
</script>
<script type="text/javascript">
function onSuccess(numUnread) {
alert('You have ' + numUnread[0]
+ ' unread messages in your Gmail inbox.');
document.getElementById("id2").innerHTML = numUnread[0];
}
google.script.run.withSuccessHandler(onSuccess)
.getPermits();
</script>
In code.gs:
function getPermits()
{
var permits = [];
for(var i = 0; i < 10; i++)
{
permits.push('Element ' + i);
}
return permits;
}
Right now I am just trying to figure out why the div with id = "id2"
does not get changed to the first element from the passed array. Instead, it is not changed. Also, there is no alert. If I change the return of the gePermits() function to a string, both the div and the alert work as I would expect.
Thanks in advance!
Some types are not passed trough HTMLService, but you can always STRINGFY and PARSE it, try:
return JSON.stringify(permits);
and in the html:
function onSuccess(numUnread) {
numUnread = JSON.parse(numUnread);