Can't retrieve ServerHandler by calling app.getElementById(id) function - google-apps-script

I am trying to build a FlexTable with dynamic number of rows. Each row contains a TextBox. There is a button to add new row to the table. And a Submit button that should send all TextBox widgets for processing.
I have initially declared a ServerHandler and added to it all fixed elements:
var handler = app.createServerHandler('handler12')
.addCallbackElement(fixedTextBox1)
.addCallbackElement(fixedTextBox2);
handler.setId('myHandlerId');
button.addClickHandler(handler);
Note that I have set an id to the handler. Now when I process the addition of new TextBox (row to the table) I have to call addCallbackElement(newlyCreatedField) to the handler. Otherwise I will not have it as param when the form is submitted.
I do this:
var handler = app.getElementById('myHandlerId');
handler.addCallbackElement(newlyCreatedField);
But I get the following error:
Error encountered: Cannot find function addCallbackElement in object Generic.
I logged the type of the handler var with Logger.log(handler.getType()) and it says Generic. I called app.getElementById(id) on non-existent id and it again returns an object of type Generic.
What am I doing wrong? Thank you for looking into this. Thank you for your time.

Is there a specific reason why you want to add each text box as a callback element. If not I suggest that you add the Flextable as the callback element and all the elements within the Flextable will be recognized in your handler functions.
var handler = app.createServerHandler('handler12')
.addCallbackElement(flexTable);

(Srik beat me to it...) You don't need to have every TextBox as a CallbackElement, if you have the container as a CallbackElement.
You will need to keep the names of the contained TextBox elements unique, and keep track of those names. To do that, you can use a Tag, and update it as you go.
function doGet() {
var app = UiApp.createApplication();
// FlexTable - use tag to count # rows.
var fTable = app.createFlexTable()
.setId('fTable')
.setTag('0');
app.add(fTable);
// Button to add new row
var addRow = app.createButton('Add Row');
app.add(addRow);
var addRowHandler = app.createServerHandler('addRowHandler');
addRowHandler.addCallbackElement(fTable);
addRow.addClickHandler(addRowHandler);
// Submit button
var submit = app.createButton('Submit');
app.add(submit);
var submitHandler = app.createServerHandler('submitHandler');
submitHandler.addCallbackElement(fTable);
submit.addClickHandler(submitHandler);
return app;
}
function addRowHandler(e) {
var app = UiApp.getActiveApplication();
var row = parseInt( e.parameter.fTable_tag ); // Get row counter
var tBox = 'textBox'+row;
var textBox = app.createTextBox()
.setId(tBox)
.setName(tBox);
// Place new text box in FlexTable
var fTable = app.getElementById('fTable');
fTable.setWidget(row, 0, textBox);
// Update row counter
fTable.setTag((++row).toString());
return app;
}
function submitHandler(e) {
var app = UiApp.getActiveApplication();
var rows = parseInt( e.parameter.fTable_tag ); // Get row counter
// Log contents of all textBoxes in FlexTable
for (var row=0; row<rows; row++) {
var tBox = 'textBox'+row;
Logger.log(tBox+'='+e.parameter[tBox]);
}
return app;
}
A more complete example of using dynamic form elements is available on Waqar's Apps Script Tutorial.

Related

Google Apps Script - Share widgets between functions

This is my first time working with Google apps scripts, and I'm a bit confused as to how to access widgets from multiple functions.
Basically, I'd like to have a button that updates a label widget. So the label has some default text, but then updates to show some other text after an 'Update' button is pressed.
From what I've read, the only things that can be passed into event handlers are objects with a setName method. A label widget doesn't have this, so what can I do to update the value of a widget in my doGet function from the other handler function?
Here is an idea of what I'd like to do (but can't get to work):
function doGet() {
var app = UiApp.createApplication();
// Create the label
var myLabel = app.createLabel('this is my label')
app.add(myLabel)
// Create the update button
var updateButton = app.createButton('Update Label');
app.add(updateButton)
// Assign the update button handler
var updateButtonHandler = app.createServerHandler('updateValues');
updateButton.addClickHandler(updateButtonHandler);
return app;
}
function updateValues() {
var app = UiApp.getActiveApplication();
// Update the label
app.myLabel.setLabel('This is my updated label')
return app;
}
I've been scouring the internet for hours trying to find a solution but can't seem to figure it out. Any suggestions?
What you mention about getting the value of a widget from an object name property is to GET the value of a widget, not to SET it. (in this case uppercase is not to "shout" but simply to get attention :-))
And the example of the Label is typically an example of a widget that you cannot read the value...
What you are looking for is a way to set widget value : you have to get the element by its ID : see example below in your updated code:
function doGet() {
var app = UiApp.createApplication();
// Create the label
var myLabel = app.createLabel('this is my label').setId('label');
app.add(myLabel)
// Create the update button
var updateButton = app.createButton('Update Label');
app.add(updateButton)
// Assign the update button handler
var updateButtonHandler = app.createServerHandler('updateValues');
updateButton.addClickHandler(updateButtonHandler);
return app;
}
function updateValues() {
var app = UiApp.getActiveApplication();
// Update the label
var label = app.getElementById('label').setText('This is my updated label');
return app;
}

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.

Can I change handler callbackfunction in another server handler

I would like to change the callback function within another server handler. I get a response "Cannot find function setCallbackFunction in object Generic." as a result of
app.getElementById('treeHandler').setCallbackFunction('noSelection');
while the handler is defined in the mainline as
var handler = app.createServerHandler('nameSelected').setId('treeHandler');
so it looks as though we can't get elements of type ServerHandler within a server handler.
Is this expected behaviour?
That is correct; you cannot get them back and do anything with them.
That is not completely correct, you can add an hidden element to application. Just adopt the axmaple below by using createHidden instead of createTextBox.
function doGet() {
var app = UiApp.createApplication();
var textBox1 = app.createTextBox().setName("textBox1");
var textBox2 = app.createTextBox().setId("textBox2");
app.add(textBox1);
app.add(textBox2);
var textBox3 = app.createTextBox().setName("textBox3");
var panel = app.createFlowPanel();
panel.add(textBox3);
app.add(panel);
var button = app.createButton("a button");
var handler = app.createServerHandler("handlerFunction");
handler.addCallbackElement(textBox1)
.addCallbackElement(textBox2)
.addCallbackElement(panel)
button.addClickHandler(handler);
app.add(button);
return app;
}
function handlerFunction(eventInfo) {
var parameter = eventInfo.parameter;
// There's a lot of information in 'parameter' about the event too, but we'll focus here
// only on the callback elements.
var textBox1 = parameter.textBox1; // the value of textBox1
var textBox2 = parameter.textBox2; // undefined! setId is not the same as setName
var textBox3 = parameter.textBox3; // works! the parent "panel" was a callback element
}
Source: https://developers.google.com/apps-script/class_serverhandler#addCallbackElement

How to make the last item added to a flextable return directly underneath the previous item?

Could someone please show me how to make the last item added to the flextable go directly underneath the existing item?
function doGet() {
var app = UiApp.createApplication();
var listBox = app.createListBox();
listBox.addItem("item 1").addItem("item 2").addItem("item 3").setName("myListBox");
var handler = app.createServerHandler("buttonHandler");
// pass the listbox into the handler function as a parameter
handler.addCallbackElement(listBox);
var table = app.createFlexTable().setId("myTable");
var button = app.createButton("+", handler);
app.add(listBox);
app.add(button);
app.add(table);
return app;
}
function buttonHandler(e) {
var app = UiApp.getActiveApplication();
app.getElementById("myTable").insertRow(0).insertCell( 0, 0).setText( 0, 0, e.parameter.myListBox);
return app;
}
The idea is to keep in memory the index of the row you write to. One can use many ways to achieve this but the most straightforward is probably to write this row index in the table or listBox tag, something like this (I didn't test the code but hope I made no error):
EDIT : the tag solution didn't seem to work, so I changed to a hidden widget solution.(tested)
function doGet() {
var app = UiApp.createApplication();
var listBox = app.createListBox();
listBox.addItem("item 1").addItem("item 2").addItem("item 3").setName("myListBox");
var hidden = app.createHidden().setName('hidden').setId('hidden')
var handler = app.createServerHandler("buttonHandler");
// pass the listbox into the handler function as a parameter and the hidden widget as well
handler.addCallbackElement(listBox).addCallbackElement(hidden);
var table = app.createFlexTable().setId("myTable");
var button = app.createButton("+", handler);
app.add(listBox).add(button).add(table).add(hidden);// add all widgets to the app
return app;
}
function buttonHandler(e) {
var app = UiApp.getActiveApplication();
var pos = e.parameter.hidden;// get the position (is a string)
if(pos==null){pos='0'};// initial condition, hidden widget is empty
pos=Number(pos);// convert to number
var table = app.getElementById("myTable")
table.insertRow(pos).insertCell( pos, 0).setText(pos, 0, e.parameter.myListBox);// add the new item at the right place
++pos ;// increment position
app.getElementById('hidden').setValue(pos);// save value
return app;// update app
}
The following piece of code is doing the same, but with the usage of a ScriptProperties method. Nothing fancy to it and works like a charm:
function doGet() {
var app = UiApp.createApplication();
// create listBox and add items
var listBox = app.createListBox().setName("myListBox")
.addItem("item 1")
.addItem("item 2")
.addItem("item 3");
// set position
ScriptProperties.setProperty('position', '0');
// create handle for button
var handler = app.createServerHandler("buttonHandler").addCallbackElement(listBox);
// create flexTable and button
var table = app.createFlexTable().setId("myTable");
var button = app.createButton("+", handler);
// add all to application
app.add(listBox)
.add(button)
.add(table);
// return to application
return app;
}
function buttonHandler(e) {
var app = UiApp.getActiveApplication();
// get position
var position = parseInt(ScriptProperties.getProperty('position'),10);
// get myTable and add
app.getElementById("myTable").insertRow(position).insertCell(position, 0).setText(position, 0, e.parameter.myListBox);
// increment position
var newPosition = position++;
// set position
ScriptProperties.setProperty('position', newPosition.toString());
// return to application
return app;
}
Please de-bug the code, so that the authorization process is initiated.
Reference: Script and User Properties

GAS Client Handler Validation on List Boxes

I am coding a simple UI web form in GAS. I have several text boxes and list boxes. I have added client handlers to validate the input on the text boxes on blur and they all work fine. However, when I try to use the client handler on a list box it doesn't work. For example, the first item in the list box is an empty string and I want to check that an item has been picked. Whatever validation option I use, it fires. I've tried validate length. I thought perhaps the script might be picking up the index value of the list so I tried option and range. Also, a no-go. I would guess the script is working with a null value which is short-circuiting any validation. Any tips? When the widget is a List Box this thing fires all the time regardless of the conditions.
var required_field = app.createClientHandler()
.validateNotLength(widget, min, max)
.forTargets(lblInfo)
.setStyleAttribute('color','red')
.setText('Please correct errors in red NOW.')
.forTargets(lbl)
.setStyleAttribute('color','red')
.forTargets(widget)
.setStyleAttribute('background', '#ffe7e7');
widget.addBlurHandler(required_field);
I was interested in this issue so I ran a couple of tests and ended up with a workaround that might be interesting. The code is a bit long but I didn't find any way to make it shorter and still clear ;-)
This version is working, there are a few comments to suggest how to make it 'transparent' and also how to demonstrate that the handlers validation doesn't work on listBoxes.
Tell me what you think ;-)
EDIT : I've put a version online for a quick test.
function showtest() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
var app = UiApp.createApplication().setTitle('ListBox Clienthandler test');
var panel = app.createVerticalPanel();
var lb = app.createListBox(false).setId('myId').setName('myLbName').setWidth(350);
var former = app.createTextBox().setName('former value').setId('former').setWidth(350);
var textbox = app.createTextBox().setName('text').setId('valtext').setWidth(350)//.setVisible(false);// set it visible to test how it works, invisible to use it 'transparently'
var label = app.createHTML("<BR><BR>Use this textBox as a button, click on it to update its value,<BR>the Client handler doesn't fire when selected color = white<BR>(if you set the upper textBox invisible then it works as if the<BR>listBox had the handler)<BR><BR>")
// add items to ListBox
lb.setVisibleItemCount(7);
lb.addItem('white');
lb.addItem('pink');
lb.addItem('orange');
lb.addItem('green');
lb.addItem('yellow');
lb.addItem('red');
lb.addItem('cyan');
//
panel.add(textbox);
panel.add(lb);
panel.add(label);
panel.add(former);
var handler = app.createServerClickHandler('click').addCallbackElement(panel);
lb.addClickHandler(handler);
app.add(panel);
doc.show(app);
}
//
function click(e) {
var app = UiApp.getActiveApplication();
var value = e.parameter.myLbName
if(value==''){value='white'}
var textboxvalue = e.parameter.text
if(textboxvalue==''){textboxvalue='none'}
var text=app.getElementById('valtext')
text.setText(value)
var lb=app.getElementById('myId')
var former=app.getElementById('former')
// var handlerval = app.createClientHandler().validateNotMatches(lb, 'white') // this line puts handler on listBox and doesn't work
var handlerval = app.createClientHandler().validateNotMatches(text, 'white') // this line puts handler on TextBox and works as expected
.forEventSource().setText("Click here for Former Value = "+textboxvalue+" / present value = "+value)
.forTargets(lb).setStyleAttribute("background", value)
former.addClickHandler(handlerval);
return app;
}
EDIT 2 :
here is another possibility to simulate (almost exactly) the function you need : (online version)
function showtest() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
var app = UiApp.createApplication().setTitle('ListBox Clienthandler test');
var panel = app.createVerticalPanel();
var lb = app.createListBox(false).setId('lb').setName('lb').setWidth(350);
var label = app.createHTML("<BR>You forgot to choose a value in the list").setVisible(false).setId('label');
lb.setVisibleItemCount(7);
lb.addItem('');
lb.addItem('pink');
lb.addItem('orange');
lb.addItem('green');
lb.addItem('yellow');
lb.addItem('red');
lb.addItem('cyan');
var othertextbox = app.createTextBox().setText('other question');
var grid = app.createGrid(4,1)
grid.setWidget(0, 0, lb)
grid.setWidget(1, 0, label)
grid.setWidget(3, 0, othertextbox)
panel.add(grid)
var handler = app.createServerBlurHandler('alert').addCallbackElement(panel);
lb.addBlurHandler(handler);
app.add(panel);
doc.show(app);
}
//
function alert(e) {
var app = UiApp.getActiveApplication();
var lb = app.getElementById('lb')
var label = app.getElementById('label')
var value = e.parameter.lb
if(value==''){
lb.setStyleAttribute("background", 'red');
label.setVisible(true)
}else{
lb.setStyleAttribute("background", 'white');
label.setVisible(false)
}
return app;
Client handlers dont work with ListBox. But don't forget that you always have an option with Server handlers which are applicable in most of the cases when you can 1-2 seconds lag in reaction