Safe getElementById or try to determine if ID exists in GUI - google-apps-script

Method UiInstance.getElementById(ID) always returns GenericWidget object, even if ID does not exist.
Is there some way how to find out that returned object does not exist in my app, or check whether UI contains object with given ID?
Solution for UI created with GUI builder:
function getSafeElement(app, txtID) {
var elem = app.getElementById(txtID);
var bExists = elem != null && Object.keys(elem).length < 100;
return bExists ? elem : null;
}
It returns null if ID does not exist. I didn't test all widgets for keys length boundary, so be careful and test it with your GUI.
EDIT: This solution works only within doGet() function. It does not work in server handlers, so in this case use it in combination with #corey-g answer.

This will only work in the same execution that you created the widget in, and not in a subsequent event handler where you retrieve the widget, because in that case everything is a GenericWidget whether or not it exists.
You can see for yourself that the solution fails:
function doGet() {
var app = UiApp.createApplication();
app.add(app.createButton().setId("control").addClickHandler(
app.createServerHandler("clicked")));
app.add(app.createLabel(exists(app)));
return app;
}
function clicked() {
var app = UiApp.getActiveApplication();
app.add(app.createLabel(exists(app)));
return app;
}
function exists(app) {
var control = app.getElementById("control");
return control != null && Object.keys(control).length < 100;
}
The app will first print 'true', but on the click handler it will print 'false' for the same widget.
This is by design; a GenericWidget is a "pointer" of sorts to a widget in the browser. We don't keep track of what widgets you have created, to reduce data transfer and latency between the browser and your script (otherwise we'd have to send up a long list of what widgets exist on every event handler). You are supposed to keep track of what you've created and only "ask" for widgets that you already know exist (and that you already know the "real" type of).
If you really want to keep track of what widgets exist, you have two main options. The first is to log entries into ScriptDb as you create widgets, and then look them up afterwards. Something like this:
function doGet() {
var app = UiApp.createApplication();
var db = ScriptDb.getMyDb();
// You'd need to clear out old entries here... ignoring that for now
app.add(app.createButton().setId('foo')
.addClickHandler(app.createServerHandler("clicked")));
db.save({id: 'foo', type: 'button'});
app.add(app.createButton().setId('bar'));
db.save({id: 'bar', type: 'button'});
return app
}
Then in a handler you can look up what's there:
function clicked() {
var db = ScriptDb.getMyDb();
var widgets = db.query({}); // all widgets
var button = db.query({type: 'button'}); // all buttons
var foo = db.query({id: 'foo'}); // widget with id foo
}
Alternatively, you can do this purely in UiApp by making use of tags
function doGet() {
var app = UiApp.createApplication();
var root = app.createFlowPanel(); // need a root panel
// tag just needs to exist; value is irrelevant.
var button1 = app.createButton().setId('button1').setTag("");
var button2 = app.createButton().setId('button2').setTag("");
// Add root as a callback element to any server handler
// that needs to know if widgets exist
button1.addClickHandler(app.createServerHandler("clicked")
.addCallbackElement(root));
root.add(button1).add(button2);
app.add(root);
return app;
}
function clicked(e) {
throw "\n" +
"button1 " + (e.parameter["button1_tag"] === "") + "\n" +
"button2 " + (e.parameter["button2_tag"] === "") + "\n" +
"button3 " + (e.parameter["button3_tag"] === "");
}
This will throw:
button1 true
button2 true
button3 false
because buttons 1 and 2 exist but 3 doesn't. You can get fancier by storing the type in the tag, but this suffices to check for widget existence. It works because all children of the root get added as callback elements, and the tags for all callback elements are sent up with the handler. Note that this is as expensive as it sounds and for an app with a huge amount of widgets could potentially impact performance, although it's probably ok in many cases especially if you only add the root as a callback element to handlers that actually need to verify the existence of arbitrary widgets.

My initial solution is wrong, because it returns false exist controls.
A solution, based on Corey's answer, is to add the setTag("") method and here is ready to use code. It is suitable for event handlers only, because uses tags.
function doGet() {
var app = UiApp.createApplication();
var btn01 = app.createButton("control01").setId("control01").setTag("");
var btn02 = app.createButton("control02").setId("control02").setTag("");
var handler = app.createServerHandler("clicked");
handler.addCallbackElement(btn01);
handler.addCallbackElement(btn02);
btn01.addClickHandler(handler);
btn02.addClickHandler(handler);
app.add(btn01);
app.add(btn02);
return app;
}
function clicked(e) {
var app = UiApp.getActiveApplication();
app.add(app.createLabel("control01 - " + controlExists(e, "control01")));
app.add(app.createLabel("control02 - " + controlExists(e, "control02")));
app.add(app.createLabel("fake - " + controlExists(e, "fake")));
return app;
}
function controlExists(e, controlName) {
return e.parameter[controlName + "_tag"] != null;
}

Related

Google Sheets App Script Mysterious Error

I'm teaching a class and for my class I keep all of my student's marks on a google spreadsheet. On my website I would like to present information to students on an individual basis. I've created an app where it presents them with a password text box. They type in their password and then it retrieves information from the spreadsheet that is unique to them and presents it to them in a label. I've been trying to hack this all together, but it's just not working properly and I'm getting an error that I cannot diagnose. If I print out the information using Browser.msgBox() it outputs the info, but otherwise it generates an error. Why is this happening and what is the fix? Here's the code:
var pointsSheet = SpreadsheetApp.openById('1o8_f063j1jYZjFEnI_P7uAztpnEAvQ6mc3Z1_Owa69Y');
//creates and shows an app with a label and password text box
function doGet() {
var app = UiApp.createApplication().setTitle('Incomplete Challenges');
var mygrid = app.createGrid(1, 2);
mygrid.setWidget(0, 0, app.createLabel('Password:'));
mygrid.setWidget(0, 1, app.createPasswordTextBox().setName("text"));
var mybutton = app.createButton('Submit');
var submitHandler = app.createServerClickHandler('getResults');
submitHandler.addCallbackElement(mygrid);
mybutton.addClickHandler(submitHandler);
var mypanel = app.createVerticalPanel();
mypanel.add(mygrid);
mypanel.add(mybutton);
app.add(mypanel);
SpreadsheetApp.getActiveSpreadsheet().show(app);
//return app; //UNCOMMENT WHEN DEPLOYING APP
}
//obtains data based on password entered by user and outputs their info
function getResults(eventInfo) {
var app = UiApp.createApplication().setTitle('Incomplete Challenges');
var password = eventInfo.parameter.text;
var passwordCheckRange = pointsSheet.getRange("B34:C34").getValues();
if (passwordCheckRange == null) {
Browser.msgBox("Error: Range is null");
return app;
}
var name;
for(var i = 0; i < passwordCheckRange.length; i++) {
if(passwordCheckRange[i][1] == password) {
name = passwordCheckRange[i][0];
break;
}
}
var studentRecordRange = pointsSheet.getRange("B3:AY29").getValues();
var headingRange = pointsSheet.getRange("B1:AY2").getValues();
if (studentRecordRange == null) {
Browser.msgBox("Error: Range is null");
return app;
}
var requestedRecord;
for(var i = 0; i < studentRecordRange.length; i++) {
if(studentRecordRange[i][0] == name)
requestedRecord = studentRecordRange[i];
}
var stringRecord = "";
for(var i = headingRange[1].length-1; i >= 7; i--) {
if (requestedRecord[i] == "")
stringRecord += headingRange[1][i] + ": " + headingRange[0][i] + "XP" + "\\n";
}
var mygrid = app.createGrid(2, 1);
mygrid.setWidget(0, 0, app.createLabel('INCOMPLETE CHALLENGES'));
mygrid.setWidget(1, 0, app.createLabel(stringRecord));
var mypanel = app.createVerticalPanel();
mypanel.add(mygrid);
app.add(mypanel);
//Browser.msgBox(stringRecord);
return app;
}
The error that I experience is: Error encountered: An unexpected error occurred.
As you can see it's very helpful.
Line 28 it should be getActiveApplication() and not createApplication().
You cant create an application on another application. :)
Also I think line 63 it should be "<br>"; instead "\n"; along with line 68 it should be createHTML instead of createLabel
I also think that you have apply few styling css so that your app looks good. check on .setStyleAttributes in UiApp.
There are a few errors in this code, the first one -that generates the error you get - is (as mentioned in the other answer) the UiApp.createApplication() in the handler function.
You can't create an UiApp instance in a handler function, you should instead get the active instance and eventually add elements to it (using UiApp.getActiveApplication()).
You can't neither change the title of this instance. Btw, it doesn't make sense since this title will not appear as a "title" when you will be deploying this app as a webapp. It will simply show up at the top of your browser window (as a page title) as your app will occupy the whole screen and not a modal popup anymore. So if you want a title to appear in your Ui, simply add it as an HTML widget where you can choose the font size and weight (and any other CSS styles).
The other error is in the password check, you are using Browser.msgBox("Error: Range is null"); but Browser class won't work in UiApp. You should only use UiApp elements, not spreadSheetApp elements.
And, as a more general comment, I suggest you test your app directly using the .dev url (last saved version) of the app (after saving a beta version and having deployed it) so that you are in the "real" use condition and have a pertinent pov on the result.

Can I develop an wizard application (sequentially submit forms)?

In my understanding now, only one doGet() can trigger unique doPost() in a Google Apps Script application.
I would like to perform a Software Publisher System that user upload the file or fill up revision information in forms and push submit to the next step. The final page will show the input information, send email to guys and complete all operation.
But how do I enter next form after the submit button pushed?
I have tried a method that creating the 2nd step and 3rd step forms in the doPost(), and using try...catch to difference which step form triggered the current step, like the following code.
(Because any steps can't get the callback item throw by non-previous step, then it arises an exception)
It works very well but I think it doesn't make sens and very silly. Have any better solutions? Thanks, please.
//---------------------------------------------------------------------------
function doGet(e)
{
var app = UiApp.createApplication().setTitle("AP Publisher");
createFileUploadForm(app);
return app;
}
//---------------------------------------------------------------------------
function doPost(e)
{
var app = UiApp.getActiveApplication();
try {
// 2nd step form
var fileBlob = e.parameter.thefile;
createRevisionForm();
}
catch(error) {
try {
// 3rd step form
createConfirmForm(e);
}
catch(error2) {
//Complete
sendMail(e);
modifySitePageContent(e);
saveHistoryFile(e);
showConfirmedInfo(e);
}
}
return app;
}
This answer is copied entirely from create a new page in a form dynamically, based on data of the prev. page.
Using the UiApp service, you have one doGet() and one doPost() function... but here's a way to extend them to support a dynamic multi-part form. (The example code is borrowed from this answer.)
Your doGet() simply builds part1 of your form. In the form, however, you need to identify your form by name, like this:
var form = app.createFormPanel().setId("emailCopyForm");
You doPost() then, will pass off handling of the post operation to different functions, depending on which form has been submitted. See below. (Also included: reportFormParameters (), a default handler that will display all data collected by a form part.)
/**
* doPost function with multi-form handling. Individual form handlers must
* return UiApp instances.
*/
function doPost(eventInfo) {
var app;
Logger.log("Form ID = %s", eventInfo.parameter.formId);
// Call appropriate handler for the posted form
switch (eventInfo.parameter.formId) {
case 'emailCopyForm':
app = postEmailCopyForm(eventInfo);
break;
default:
app = reportFormParameters (eventInfo);
break;
}
return app;
}
/**
* Debug function - returns a UiInstance containing all parameters from the
* provided form Event.
*
* Example of use:
* <pre>
* function doPost(eventInfo) {
* return reportFormParameters(eventInfo);
* }
* </pre>
*
* #param {Event} eventInfo Event from UiApp Form submission
*
* #return {UiInstance}
*/
function reportFormParameters (eventInfo) {
var app = UiApp.getActiveApplication();
var panel = app.createVerticalPanel();
panel.add(app.createLabel("Form submitted"));
for (var param in eventInfo.parameter) {
switch (param) {
// Skip the noise; these keys are used internally by UiApp
case 'lib':
case 'appId':
case 'formId':
case 'token':
case 'csid':
case 'mid':
break;
// Report parameters named in form
default:
panel.add(app.createLabel(" - " + param + " = " + eventInfo.parameter[param]));
break;
}
}
app.add(panel);
return app;
}
To generate each form part, subsequent form handlers can use the data retrieved in previous parts to dynamically add new Form objects to the ui.
I think it would be simpler to use 3 (or more) different panels in your doGet function with all the items you need and to play with their visibility.
At first only the 1rst panel would be visible and, depending on user input (using client Handlers to handle that) show the next ones (and eventually hide the first one).
In the end the submit button will call the doPost and get all data from the doGet.
First a tip of my hat to Mogsdad. His post(s) were guiding lights in the darkly documented path that led me here. Here is some working code
that demonstrates a multiple page form, i.e. it does the initial doGet() and then lets you advance back and forth doing multiple doPost()'s. All this is done in a single getForm() function called by both the standard doGet() and the doPost() functions.
// Muliple page form using Google Apps Script
function doGet(eventInfo) {return GUI(eventInfo)};
function doPost(eventInfo) {return GUI(eventInfo)};
function GUI (eventInfo) {
var n = (eventInfo.parameter.state == void(0) ? 0 : parseInt(eventInfo.parameter.state));
var ui = ((n == 0)? UiApp.createApplication() : UiApp.getActiveApplication());
var Form;
switch(n){
case 0: {
Form = getForm(eventInfo,n); // Use identical forms for demo purpose only
} break;
case 1: {
Form = getForm(eventInfo,n); // In reality, each form would differ but...
} break;
default: {
Form = getForm(eventInfo,n) // each form must abide by (implement) the hidden state variable
} break;
}
return ui.add(Form);
};
function getForm(eventInfo,n) {
var ui = UiApp.getActiveApplication();
// Increment the ID stored in a hidden text-box
var state = ui.createTextBox().setId('state').setName('state').setValue(1+n).setVisible(true).setEnabled(false);
var H1 = ui.createHTML("<H1>Form "+n+"</H1>");
var H2 = ui.createHTML(
"<h2>"+(eventInfo.parameter.formId==void(0)?"":"Created by submission of form "+eventInfo.parameter.formId)+"</h2>");
// Add three submit buttons to go forward, backward and to validate the form
var Next = ui.createSubmitButton("Next").setEnabled(true).setVisible(true);
var Back = ui.createSubmitButton("Back").setEnabled(n>1).setVisible(true);
var Validate = ui.createSubmitButton("Validate").setEnabled(n>0).setVisible(true);
var Buttons = ui.createHorizontalPanel().add(Back).add(Validate).add(Next);
var Body = ui.createVerticalPanel().add(H1).add(H2).add(state).add(Buttons).add(getParameters(eventInfo));
var Form = ui.createFormPanel().setId((n>0?'doPost[':'doGet[')+n+']').add(Body);
// Add client handlers using setText() to adjust state prior to form submission
// NB: Use of the .setValue(val) and .setValue(val,bool) methods give runtime errors!
var onClickValidateHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)));
var onClickBackHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)-1));
Validate.addClickHandler(onClickValidateHandler);
Back.addClickHandler(onClickBackHandler);
// Add a client handler executed prior to form submission
var onFormSubmit = ui.createClientHandler()
.forTargets(state).setEnabled(true) // Enable so value gets included in post parameters
.forTargets(Body).setStyleAttribute("backgroundColor","#EEE");
Form.addSubmitHandler(onFormSubmit);
return Form;
}
function getParameters(eventInfo) {
var ui = UiApp.getActiveApplication();
var panel = ui.createVerticalPanel().add(ui.createLabel("Parameters: "));
for( p in eventInfo.parameter)
panel.add(ui.createLabel(" - " + p + " = " + eventInfo.parameter[p]));
return panel;
}
The code uses a single "hidden" state (here visualized in a TextBox) and multiple SubmitButton's to allow the user to advance forward and backward through the form sequence, as well as to validate the contents of the form. The two extra SubmitButton's are "rewired" using ClientHandler's that simply modify the hidden state prior to form submission.
Notes
Note the use of the .setText(value) method in the client handler's. Using the Chrome browser I get weird runtime errors if I switch to either of the TextBox's .setValue(value) or .setValue(value, fireEvents) methods.
I tried (unsuccessfully) to implement this logic using a Script Property instead of the hidden TextBox. Instead of client handlers, this requires using server handlers. The behavior is erratic, suggesting to me that the asynchronous server-side events are occurring after the form submission event.

setName vs setId in Google App Script

In 'google script', on trigger of an event (button click), I am trying to change the name as well as Id of a textbox. Following is simplified code:
function addRow(e){
var app = UiApp.getActiveApplication();
app.getElementById('tbox')
.setId('txt1')
.setName('txt1');
}
Now, setId is working but setName is throwing me an error. Is there something that I am obviously missing?
From a quick test I did it seems that, although it does not throw an error if used alone, it is not possible to change an element's id, as shown in this little test:
function doGet() {
var app = UiApp.createApplication().setTitle('Button Test');
return app.add(app.createTextBox().setId('tbox').setName('tbox').setText('tbox')
.addKeyPressHandler(app.createServerHandler('inspectBox'))).add(
app.createButton('Change').addClickHandler(app.createServerHandler('btnClick')));
}
function inspectBox(e) {
var p = e.parameter;
var app = UiApp.getActiveApplication();
return app.add(app.createLabel('id: '+p.source)).add(
app.createLabel('name: '+(p.tbox ? 'tbox' : p.txt1 ? 'txt1' : 'not found')));
}
function btnClick() {
var app = UiApp.getActiveApplication();
app.getElementById('tbox').setId('txt1');
//app.getElementById('tbox').setName('txt1');
return app;
}
Later, if you comment the setId line and uncomment the setName one, then type a char in the textbox, you'll see that the name changes. And the setName call works fine if you continue working on the element using, for example, setText. Which is not true for setId, that fails if you try anything on the element.
Anyway, as commented by others, it's indeed odd that you have such a requirement. I guess you should find another way (possibly a better one) to do what you want (which you have not described).

Google Apps Script, HTML addClickHandler ServerHandler does NOT work

Can anyone confirm that HTML widgets accept ClickHandlers on the Server side ? I can't get my below code to work.
I create a serverHandler (and for good measure I have even added a useless callback element). Subsequently, I add it to a HTML.addClickHander (for good measure I have even added it to .addMouseUpHandler as well). The function is NOT executed.
var mouseclick = app.createServerHandler("handleTrainingClick_").addCallbackElement(lstFilter);
var params = [ "fromOrg", "trainingTitle", "dueDate", "medical", "status" ];
var resultSet = blSelectActiveTrainings_();
while (resultSet.hasNext()) {
var training = resultSet.next();
var html = TRAINING_ROW;
for (var pI in params) {
html = html.replace("$"+params[pI], training[params[pI]]);
}
pnlList.add(app.createHTML(html).setId(training.id).addClickHandler(mouseclick).addMouseUpHandler(mouseclick)
.addMouseMoveHandler(mousemove).addMouseOutHandler(mouseout).addMouseOverHandler(mouseover));
}
function handleTrainingClick_(e) {
Logger.log(e.source);
var app = UiApp.getActiveApplication();
return app;
}
HTML widgets server side handlers work just fine. It was an incorrect reference in my code. Thanks all.

What is the best way to manage two UI's?

I have created two user interfaces. How can I close the first one and activate the next? Is it possible to have two UI under Google apps script?
I have try something like:
var app = UiApp.getActiveApplication();
app.add(app.loadComponent("APPGui"));
var panel1 = app.getElementById("LoginPanel1");
panel1.setVisible(false);
return app;
The easiest way is probably to design both panels in the same GUI builder, one over each other in 2 separate panels, the 'login panel' being above the other it will mask the other one when active. As you set it 'invisible', you'll see the one underneath.
Depending on your use case the login panel might hide all or only a part of your main panel.
The GUI builder has all the necessary tools to decide which is in front or backwards.
Here's and example of three dialogs shown one after the other, maintaining state/data between them via the CacheService object.
(You could use UserProperties, ScriptProperties or even a Hidden Field as an alternative, each has their own scope though...)
Hopefully this makes sense without explaining what each dialog in the UI Builder contains.
function showDialog1(){
var app = UiApp.createApplication();
app.add( app.loadComponent("Dialog1") );
SpreadsheetApp.getActiveSpreadsheet().show(app);
}
function onDialog1OKButton(e){
CacheService.getPrivateCache().put("n1", e.parameter.n1);
var app = UiApp.getActiveApplication();
var d2 = app.loadComponent("Dialog2");
app.add(d2);
SpreadsheetApp.getActiveSpreadsheet().show(app);
}
function onDialog2OKButton(e){
var c = CacheService.getPrivateCache();
c.put("n2", e.parameter.n2);
var app = UiApp.getActiveApplication();
app.add(app.loadComponent("DialogResult"));
var n1 = c.get("n1");
var n2 = c.get("n2");
var l = app.getElementById("Label2");
l.setText( "" + n1 + " + " + n2 + " = " + (parseInt(n1) + parseInt(n2)) );
SpreadsheetApp.getActiveSpreadsheet().show(app);
}
I prefer to build multiple GUI. With this code you can jump between them.
function doGet() {
var app = UiApp.createApplication();
var base0 =app.createAbsolutePanel().setId('GUI_base0').setHeight('630px').setWidth('1125px');
app.createAbsolutePanel().setId('GUI_base1'); // create all abs_panells but not use
// you need to create all abspanels if you want to jump between them
app.createAbsolutePanel().setId('GUI_base2'); // create here all the absolute panels (1 for every GUI)
// app.createAbsolutePanel() ... GUI3, GUI4 ...
var component0 = app.loadComponent("GUI_password"); // load first GUI (his name is "password"
/// this is an example of code for the 1st GUI ////////////////////
/// I can check if the user can see the second GUI
var label_ID = app.getElementById('LB_ID');
var user = Session.getActiveUser().getEmail();
if ( user == 'XXX#yyyy.com' ) {
label_ID.setText(user).setTag(user); // only show if ....
}
////////////////////////////////////////////////////////////////////
base0.add(component0); // GUI_password over absolute panel
app.add(base0);
// handler Button1 // we can show a button only if the password is correct or is a valid user or ...
app.getElementById('BT_jump').addClickHandler(app.createServerHandler('NOW_gui1'));
return app;
};
function NOW_gui1(e) {
var app = UiApp.getActiveApplication();
var base0 = app.getElementById("GUI_base0").setVisible(false); // hide 1st abs_panel created with code
var base2 = app.getElementById("GUI_base2").setVisible(false); // hide 3rd abs_panel created with code
/// hide all others abs_panel
var base1 = app.createAbsolutePanel().setId('GUI_base1').setHeight('630px').setWidth('1125px'); // maybe get by ID ??, but this work
var component1 = app.loadComponent("GUI_1"); // load the second GUI
base1.add(component1); // load GUI_1 over 2n absolute panel
app.add(base1);
// HERE THE CODE OF THE GUI_1
// handler Button2
app.getElementById('BT_jump_1_to_2').addClickHandler(app.createServerHandler('NOW_gui2'));
return app;
};