Summary:
Google apps script runs in a stateless
environment. Anything stored in global object is not maintained across sessions. If you add something to the global object during a session, it is not available in the next session run. Use Immediately Invoked Functions or Call functions in global scope to fill the global scope(this
object) before any function is actually called by the user interface.
Explanation:
The workaround mentioned by mr…@bbtv.com in comment#17 of this issue and by Tanaike in this answer both utilises filling up the global scope using Closures/Immediately Invoked Functions(IIFE).
To understand this, You need to understand when Script function names are read and loaded. The following steps happen in order:
-
Menu Click(Button click seems to skip step 1 and 2 and therefore doesn’t seem to be interceptable)
-
All Script in script editor is executed and list of functions names in global
this
is created(In this step, no function is run/called, but all of the script is fully executed). This is equivalent to loading a webpage with your script:<script>...code.gs...</script>
-
Check if the currently invoked button’s/menu’s function name is present in global
this
, -
If present, execute the function(i.e.,calling the function, that the linked button/menu’s function name refers to). This is like adding
myFunction()
at the end of the already loaded script. If not found, throw Error:Script function not found
-
Script ends. This is like closing the loaded webpage. All “state” is lost. No global scope or
this
is stored permanently.
When dynamically adding menu items using this[function-name]
, it is important to realize when you’re adding the function. If you’re adding it during onOpen
, then this
has those functions in global scope during onOpen
execution, but it immediately lost after onOpen
script execution is complete.
function onOpen(){
this['a'] = () => 'a';
SpreadsheetApp.getUi()
.createMenu("Test")
.addItem("Call function a","a")
.addToUi()
}
This will successfully add a
function to Ui
menu, but notice that a
is only added to global this
scope during onOpen
execution. This this
is then lost after that execution is complete and a new this
(global scope) is created, next time any function is called(Step 1 to 5 repeats). So, when a menu click occurs, step 2 creates a new this
and looks for a function named a
in all the script, but wouldn’t find any, because this newly created this
doesn’t have a
(because onOpen
is declared, but not executed and therefore a
is not added to this
this time).
Solution:
During or before Step2, You would need to add the function to the global this
:
function onOpen(){
SpreadsheetApp.getUi()
.createMenu("Test")
.addItem("Call function a","a")
.addToUi()
}
(function IIFE(){
this['a'] = () => 'a';
})();
The IIFE function above intercepts Step 2 “every time”, any function is called. So, the a
is always present in this
at or after Step 3. In Tanaike’s solution, this is done by installFunctions()
in a global scope. That function is executed every time any function is called. The same is true in case of createMenuFunctions(this);
in comment#17.
Documentation excerpts:
From add-on documentation link,
Warning: When your onOpen(e) function runs, the entire script is loaded and any global statements are executed. These statements execute under the same authorization mode as onOpen(e) and fail if the mode prohibits them. This prevents onOpen(e) from running. If your published add-on fails to add its menu items, look in the browser’s JavaScript console to see if an error was thrown, then examine your script to see whether the onOpen(e) function or global variables call services that aren’t allowed in AuthMode.NONE.
Sample script:
/**Runs every time any script function is called*/
(function IIFE(scope) {
'use strict';
scope['options'] = ['a', 'b', 'c']; //pollute current scope
options.forEach(
option =>
(scope[option] = () =>
SpreadsheetApp.getUi().alert(`You clicked option ${option}`))
);
})(this);//pass global scope
function onOpen() {
const testMenu = SpreadsheetApp.getUi().createMenu('Test');
options.forEach(option =>
testMenu.addItem('Call function ' + option, option)
);
testMenu.addToUi();
}
References:
-
IIFE
-
Related answer
-
Stateless vs Stateful