using and modifying global variables within handler functions - google-apps-script

Hello everyone out there,
I can use global variables within a handler function, however I cannot modify them "globally" within than function.
In code below, after the first click it will show the number 1001 (the handler reads, increments and shows the right result).
But, any further clicks will always show 1001, so the handler keeps reading the original globalVar value: it doesn't get modified as I was expecting.
Anything I can do to fix this?
var globalVar = 1000;
function testingGlobals() {
var app = UiApp.createApplication();
var doc = SpreadsheetApp.getActiveSpreadsheet();
var panel = app.createVerticalPanel().setId('panel');
app.add(panel);
panel.add(app.createButton(globalVar).setId("globalVar").addClickHandler(app.createServerHandler("chgGlobal").addCallbackElement(panel)));
doc.show(app)
}
function chgGlobal(e) {
var app = UiApp.createApplication();
globalVar++;
app.getElementById("globalVar").setText(globalVar);
return app;
}

You can not increment Global Variables this way, as every time a handler executes, Global variable is initialized and then manipulated.
You can use hidden formfields to hold the a variable which you can change in handler, and it will be persistent as long as the app is open.
e.g
function testingGlobals() {
var app = UiApp.createApplication();
var doc = SpreadsheetApp.getActiveSpreadsheet();
var panel = app.createVerticalPanel().setId('panel');
var myVar = app.createHidden().setValue('0').setName('myVar').setId('myVar');
panel.add(myVar);
app.add(panel);
panel.add(app.createButton('0').setId("globalVar").addClickHandler(app.createServerHandler("chgGlobal").addCallbackElement(panel)));
doc.show(app)
}
function chgGlobal(e) {
var app = UiApp.createApplication();
gloabalVar = parseInt(e.parameter.myVar);
gloabalVar ++;
app.getElementById('myVar').setValue(gloabalVar.toString());
app.getElementById("globalVar").setText(gloabalVar);
return app;
}

You can persist data in CacheService (fast but not guaranteed) or ScriptProperties or ScriptDb (a little slower, but persisted) instead of global properties. ScriptDb in particular can hold objects without needing to use strings to store them.

Related

How to define global variable in Google Apps Script

I see most examples from Google is they use only functions in a single giant script.
e.g. https://developers.google.com/apps-script/quickstart/macros
But in our style, we usually write all functions under a single namespace, such as
MyCompany = (MyCompany || {});
MyCompany.init = function () {
Logger.log('init');
};
function onOpen() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var menus = [{
name: "Init",
functionName: MyCompany.init
}];
spreadsheet.addMenu("Test", menus);
};
However, when I run the code above, it return
"MyCompany is not defined."
How to solve?
You might be better off using the Properties Service as you can use these as a kind of persistent global variable.
click 'file > project properties > project properties' to set a key value, or you can use
PropertiesService.getScriptProperties().setProperty('mykey', 'myvalue');
The data can be retrieved with
var myvalue = PropertiesService.getScriptProperties().getProperty('mykey');
In GAS global variables are not what they are in other languages. They are not constants nor variables available in all routines.
I thought I could use global variables for consistency amongst functions and efficiency as well. But I was wrong as pointed out by some people here at SO.
Global variable will be evaluated at each execution of a script, so not just once every time you run your application.
Global variables CAN be changed in a script (so they are not constants that cannot be changed by accident), but will be reinitialized when another script will be invoked.
There is also a speed penalty on using global variables. If within a function you use the same global variable two or more times, it will be faster to assign a local variable and use that instead.
If you want to preserve variables between all functions in your application, it might be using a cacheService will be best.
I found out that looping through all files and folders on a drive takes a LOT of time. But you can store info about files and folders within cache (or even properties) and speed up at least 100 times.
The only way I use global variables now is for some prefixes and for naming widgets.
I'm using a workaround by returning a function with an object of my global variables:
function globalVariables(){
var variables = {
sheetName: 'Sheet1',
variable1: 1,
variable2: 2
};
return variables;
}
function functionThatUsesVariable (){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(globalVariables().sheetName);
}
Global variables certainly do exist in GAS, but you must understand the client/server relationship of the environment in order to use them correctly - please see this question:
Global variables in Google Script (spreadsheet)
However this is not the problem with your code; the documentation indicates that the function to be executed by the menu must be supplied to the method as a string, right now you are supplying the output of the function:
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#addMenu%28String,Object%29
function MainMenu_Init() {
Logger.log('init');
};
function onOpen() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var menus = [{
name: "Init",
functionName: "MainMenu_Init"
}];
spreadsheet.addMenu("Test", menus);
};
I needed something similar like the question, you can store and fetch from the cache https://developers.google.com/apps-script/reference/cache/cache
Example:
// call cache service
var cache = CacheService.getScriptCache();
// get an item from the cache
var cached = cache.get("somekey");
// if exists in the cache use it
if (cached != null) {
// use it whatever you like.
}else{
// calculate/assign your data to cache
cache.put("somekey","somevalueorobject");
// you can even put cache data on TTL (time to live) in seconds.
cache.put("somekey","somevalueorobject",60);
I use this: if you declare var x = 0; before the functions declarations, the variable works for all the code files, but the variable will be declare every time that you edit a cell in the spreadsheet
For constants I am using function arrow expressions.
The footprint is similar to a variable declaration. Just add the () => when declaring, and () when calling the (function) variable.
var currentSheet = () => SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var maxAttempts = () => 10;
function myFunction(){
var sheetName = currentSheet().getName();
for (var i=0; i< maxAttempts(); i++){
trySomething(i);
}
}
var userProperties = PropertiesService.getUserProperties();
function globalSetting(){
//creating an array
userProperties.setProperty('gemployeeName',"Rajendra Barge");
userProperties.setProperty('gemployeeMobile',"9822082320");
userProperties.setProperty('gemployeeEmail'," rajbarge#hotmail.com");
userProperties.setProperty('gemployeeLastlogin',"03/10/2020");
}
var userProperties = PropertiesService.getUserProperties();
function showUserForm(){
var templete = HtmlService.createTemplateFromFile("userForm");
var html = templete.evaluate();
html.setTitle("Customer Data");
SpreadsheetApp.getUi().showSidebar(html);
}
function appendData(data){
globalSetting();
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data");
ws.appendRow([data.date,
data.name,
data.Kindlyattention,
data.senderName,
data.customereMail,
userProperties.getProperty('gemployeeName'),
,
,
data.paymentTerms,
,
userProperties.getProperty('gemployeeMobile'),
userProperties.getProperty('gemployeeEmail'),
Utilities.formatDate(new Date(), "GMT+05:30", "dd-MM-yyyy HH:mm:ss")
]);
}
function errorMessage(){
Browser.msgBox("! All fields are mandetory");
}

addTimer not working with global app in GAS

I read about the (undocumented) addTmer and would like to use it for updating the UI.
The example at Auto refresh Google Apps Scripts webapp UI? does work well
// This code works
function doGet(e)
{
var app = UiApp.createApplication().setTitle('Test addTimer - createApplication in doget');
var appLocal = app;
var handler = appLocal.createServerHandler("update");
appLocal.addTimer(handler , 4000);
var label = appLocal.createLabel(new Date()).setId("label");
appLocal.add(label);
return appLocal;
}
function update(e)
{
var appLocal = UiApp.getActiveApplication();
// var appLocal = app;
appLocal.getElementById("label").setText(new Date());
var handler = appLocal.createServerHandler("update");
appLocal.addTimer(handler , 1000);
return appLocal;
}
but if I move createApplication out of doGet() into a global variable, an 'unexpected error' will occur at runtime.
Without addTimer it IS possible to move createApplication out of doget() into global scope.
// This code will crash at runtime
var app = UiApp.createApplication().setTitle('Test addTimer , createApplication outside doGet');
function doGet(e)
{
var appLocal = app;
var handler = appLocal.createServerHandler("update");
appLocal.addTimer(handler , 4000);
var label = appLocal.createLabel(new Date()).setId("label");
appLocal.add(label);
return appLocal;
}
function update(e)
{
var appLocal = UiApp.getActiveApplication();
// var appLocal = app;
appLocal.getElementById("label").setText(new Date());
var handler = appLocal.createServerHandler("update");
appLocal.addTimer(handler , 1000);
return appLocal;
}
As I've seen global use of
var app = createApplication();
and i suppose more people would like to use addTimer as well, I post my experience here.
Maybe someone can explain WHY this is a problem.
when you call a service outside of any function, this call is executed each time any function is called, that's to say when doGet is called but also when the handler function is called and also when the timer triggers a script execution.
It means that you are calling UiApp.createApplication() 2 (and more) times... this is not a good idea and generates an 'unexpected error'.
by the way, what would be the advantage of that approach ?
Uiapp is designed to work with a main service call and handlers, each handler getting the active instance using getActiveApplication() why are you trying to use it differently?

Automatically close a Google Apps Script UiApp after five seconds

I want to automatically close this UiApp after a certain number of seconds:
function showConfirmationDialogue() {
var app = UiApp.createApplication().setHeight('80').setWidth('400');
app.setTitle('test');
var panel = app.createVerticalPanel();
app.add(panel);
var doc = SpreadsheetApp.getActive();
doc.show(app);
// this part doesn't seem to work
Utilities.sleep(5000);
app.close();
return app;
}
Thanks!
The Ui you create is shown when you call doc.show(app) and the only way you can update it or close it is to use a handler function that ends with a return app.
So it is not possible to do what you want from the same function that creates the UI since it is "returned" only one time.
I know only one trick that can achieve what you want that is using a handler trigger source that will call a closing handler function automatically using a "special" property of the checkBox widget. Here is the code, it uses a checkBox that you can of course make invisible in your final code.
function showConfirmationDialogue() {
var app = UiApp.createApplication().setHeight('80').setWidth('400');
app.setTitle('test');
var panel = app.createVerticalPanel();
app.add(panel);
var handler = app.createServerHandler('closeWindow');
var chk = app.createCheckBox('checkBox to set invisible in real function').setValue(false,true).addValueChangeHandler(handler);
app.add(chk);
chk.setValue(true,true)//.setVisible(false);
var doc = SpreadsheetApp.getActive();
doc.show(app);
}
function closeWindow(){
Utilities.sleep(5000);
var app = UiApp.getActiveApplication().close();
return app;
}
You can use the same procedure to modify the UiApp instance in any way, change a Label text, add a widget... anything you want.

how to get value of Hidden object

I need global variable in google script, to hold page ID, like a string. Here, they suggested to use object Hidden for this purpose. I can create this object and set its value.
Code to achieve the same :
function doGet(e) {
var app = UiApp.createApplication();
//Get current indentificator
var mid = 'page-id';
app.add( app.createHidden('mid').setValue(mid).setId('mid'));
return app;
}
But how can I get this value from another function?
For example :
function maketbl(){
var app = UiApp.getActiveApplication();
app.(?!)
}
Thanks!
I see that your requirement is to have functionality similar to that of global variables. I suggest that you use script properties, user properties or the cache service to accomplish this feat. An example is below
ScriptProperties.setProperty('special', 'sauce'); // Use this to set the property
var specialValue = ScriptProperties.getProperty('special'); // use this to access the property
Try the bellow code:
function doGet(e) {
var app = UiApp.createApplication();
var mid = 'page-id';
var hidden = app.createHidden('mid').setValue(mid).setId('mid');
app.add(hidden);
//Create your handler
var handler = app.createServerHandler('maketbl');
handler.addCallbackElement(hidden)
//Create a button to trigger your function
app.add(app.createButton().setText("go forest").addClickHandler(handler));
return app;
}
function maketbl(e){
var app = UiApp.getActiveApplication();
//Retrieve the hidden field
var hidden = app.getElementById("mid");
//Show the value stored at e.parameter.mid where mid is the name of the field
var dialogBox = app.createDialogBox().setText(e.parameter.mid);
dialogBox.setPopupPosition(100, 100);
dialogBox.show();
return app;
}
Live version here.

why does the first occurrence of a handler call return an undefined value as parameter

I have been playing with this small test code that - I admit - isn't very useful but I noticed that the value returned in the callBackElement of the first handler is undefined when this handler is called for the first time.
I couldn't figure out why... so I added a condition that solves the problem but I still would like to understand why this is working like that...
The script comes from a idea shown in this post earlier today, I commented the line that causes the error in the script below (it's a bit long, sorry about that) and runs as a sort of timer/counter to illustrate the ability to fire a handler programmatically with checkBoxes.
If someone can explain why this condition is necessary ?
var nn=0;
//
function doGet() {
var app = UiApp.createApplication().setHeight('120').setWidth('200').setTitle('Timer/counter test');
var Panel = app.createVerticalPanel()
var label = app.createLabel('Initial display')
.setId('statusLabel')
app.add(label);
var counter = app.createTextBox().setName('counter').setId('counter').setValue('0')
var handler1 = app.createServerHandler('loadData1').addCallbackElement(Panel);
var handler2 = app.createServerHandler('loadData2').addCallbackElement(Panel);
var chk1 = app.createCheckBox('test1').addValueChangeHandler(handler1).setVisible(true).setValue(true,true).setId('chk1');
var chk2 = app.createCheckBox('test2').addValueChangeHandler(handler2).setVisible(true).setValue(false,false).setId('chk2');
app.add(Panel.add(chk1).add(chk2).add(counter));
SpreadsheetApp.getActive().show(app)
}
function loadData1(e) {
var app = UiApp.getActiveApplication();
var xx = e.parameter.counter
//*******************************************************
if(xx){nn = Number(xx)}; // here is the question
// nn = Number(xx); // if I use this line the first occurence = undefined
nn++
var cnt = app.getElementById('counter').setValue(nn)
Utilities.sleep(500);
var chk1 = app.getElementById('chk1').setValue(false,false)
var chk2 = app.getElementById('chk2').setValue(true,true)
var label = app.getElementById('statusLabel');
label.setText("Handler 1 :-(");
return app;
}
function loadData2(e) {
var app = UiApp.getActiveApplication();
var xx = Number(e.parameter.counter)
xx++
var cnt = app.getElementById('counter').setValue(xx)
Utilities.sleep(500);
var chk1 = app.getElementById('chk1').setValue(true,true)
var chk2 = app.getElementById('chk2').setValue(false,false)
var label = app.getElementById('statusLabel');
label.setText("Handler 2 ;-)");
return app;
}
The app looks like this:
and is testable here
EDIT : working solution is to fire the handler after adding the widgets to the panel (see Phil's answer)
like this :
var chk1 = app.createCheckBox('test1').addValueChangeHandler(handler1).setVisible(true).setId('chk1');
var chk2 = app.createCheckBox('test2').addValueChangeHandler(handler2).setVisible(true).setId('chk2');
app.add(Panel.add(chk1).add(chk2).add(counter));
chk1.setValue(true,true);
chk2.setValue(false,false);
return app
The callback element which you specified for that handler (Panel) has no elements at the time that it is invoked. So you are essentially passing along a empty panel to that handler. So since chk1 hasn't been added to the panel yet, its value isn't added as a parameter to the handler.
Put chk1.setValue(true,true) after the call to Panel.add(chk1).
As seen in this example, the handler is queued when setValue(true,true) is called. This means that all the parameters that will be passed to the handler are gathered. It looks at the callback elements, reads their values, and then continues executing the doGet. After doGet finishes, the handler is executed.