GAS Client Handler Validation on List Boxes - google-apps-script

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

Related

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.

How To Allow Users to Review Answers before Submiting Form?

I've built a form with UiApp to collect information from the user. It's rather complex with multiple panels and file uploads, so I would like to give the user the opportunity review their inputs before submitting. I was hoping to display their inputs on one final review panel that would then allow them to decide to edit the info and move back to a earlier panel to edit.
Following is the test script. The farthest I've gotten is getting it to return 'textBox' and not the value of the textBox. Is it possible to get the values while staying in the doGet portion of my script, or must I move to doPost to access the values?
What would be the work around you would suggest?
Thanks for any and all help!
function doGet(e){
var app = UiApp.createApplication();
var appPanel = app.createVerticalPanel();
var form = app.createFormPanel();
var panel1 = app.createHorizontalPanel();
var emailLabel = app.createLabel('Your Email');
var email = app.createTextBox().setName('email').setId('email');
app.add(form);
var button1 = app.createButton('Go to Review');
panel1.add(emailLabel);
panel1.add(email);
panel1.add(button1);
appPanel.add(panel1);
form.add(appPanel);
var panel2 = app.createHorizontalPanel().setVisible(false);
var reviewLabel = app.createLabel('Your Email:');
var reviewEmail = app.createLabel(email);
panel2.add(reviewLabel);
panel2.add(reviewEmail);
appPanel.add(panel2);
//
var reviewPageTwo = app.createClientHandler()
.forTargets(panel1).setVisible(false)
.forTargets(panel2).setVisible(true);
button1.addClickHandler(reviewPageTwo);
return app;
}
UPDATE 8.24.12
I'm including the resulting script. It includes the review function, the button to lead the user back to edit, and the submitButton to post it. (You will need to replace the spreadsheet ID for the post to work.)
Thank for the help all!
Martin
function doGet(e){
var app = UiApp.createApplication();
var appPanel = app.createVerticalPanel();
var form = app.createFormPanel();
var panel1 = app.createHorizontalPanel().setId('panel1');
var emailLabel = app.createLabel('Your Email');
var email = app.createTextBox().setName('email').setId('email');
var syncChangeHandler = app.createServerHandler('syncText').addCallbackElement(form);
app.add(form);
var button1 = app.createButton('Go to Review');
panel1.add(emailLabel);
panel1.add(email);
panel1.add(button1);
appPanel.add(panel1);
form.add(appPanel);
var panel2 = app.createHorizontalPanel().setId('panel2').setVisible(false);
var reviewGrid = app.createGrid(3,3).setId('reviewGrid');
var reviewEmail = app.createLabel().setId('reviewEmail');
var reviewLabel = app.createLabel('Your Email:');
var submitButton = app.createSubmitButton('Submit');
var button2 = app.createButton('Edit Response');
panel2.add(reviewLabel);
panel2.add(reviewEmail);
panel2.add(button2);
panel2.add(submitButton);
appPanel.add(panel2);
//
var editResponse = app.createClientHandler()
.forTargets(panel1).setVisible(true)
.forTargets(panel2).setVisible(false);
button1.addClickHandler(syncChangeHandler);
button2.addClickHandler(editResponse);
return app;
}
function syncChangeHandler(e){
var app = UiApp.getActiveApplication();
app.getElementById('reviewEmail').setText(e.parameter.email);
app.getElementById('panel1').setVisible(false);
app.getElementById('panel2').setVisible(true);
return app;
}
function doPost(e){
var ss = SpreadsheetApp.openById('*your spreadsheet id here*').getSheets()[0];
var range = ss.getRange(ss.getLastRow()+1, 1, 1,2);
var values = [[new Date(),e.parameter.email]];
range.setValues(values);
var app = UiApp.getActiveApplication();
var label = app.createLabel('Thank You!');
app.add(label);
return app;
}
You have your entire function inside doGet(). The doGet() function is executed when your UI is first loaded.
So,
var email = app.createTextBox().setName('email').setId('email');
actually resolves to a text box. When you do
var reviewEmail = app.createLabel(email);
you are trying to pass a text box as an argument to createLabel, which is not allowed. Therefore this won't work. You must handle the changes to the text box in a handler.
function doGet(){
var syncChangeHandler = app.createServerHandler('syncText').addCallbackElement(form);
var email = app.createTextBox().setName('email').setId('email');
...
var reviewEmail = app.createLabel().setId('reviewEmail');
...
}
function syncText(e){
var app = UiApp.getActiveAplication();
app.getElementById('reviewEmail').setText(e.parameter.email);
return app;
}
What Srik said is true (of course ;-)), you can't indeed assign a label this type of value... What I would do (since you work in a doGet/doPost structure) is to create a second button just aside of the submit button that triggers a handler to a 'review' function that populates all the corresponding textBoxes , listBoxes or whatever you have with the values coming from your main form (a sort of copy of it) in the review panel that you already have. To achieve this you will need to add the form as a callBackElement to the handler (which was not necessary with the doPost scheme).
Another option could be to add this handler to all the widgets separately with 'key up' triggers or 'Value change triggers' so that the review panel is always up to date in real time, in this case the 'review before submit panel' could be visible at any time without further action from the user other than make it eventually visible (although it could also be always visible). In this option the handler function would be more like a 'synchroniser'. I'm afraid you'll have some difficulties with file upload though (since this can only work in a doGet/doPost structure).

How do events work in Google Script in regards to getting data from elements

Question 1) When a button is clicked is it possible to use something like this (see code below)?
function Submit(e) {
var app = UiApp.getActiveApplication();
var checked = app.getElementById("checkbox").getValue();
}
Question 2) When a label is clicked is it possible to use something like this (see code below)?
function LabelClick(e) {
var LabelText = e.parameter.getText();
}
Sorry, this probably stupid, but I can't see to find any decent examples of this and can't seem to work this out from Google's documentation and I'm just getting used to google script too. If you have the answer I would really appreciate it.
you are not very far... but not close enough to get it working...
Ui element's value is sent to the handler function in a so called callbackelement that is added to the handler. This callbackelement may be a button, a label or, more easily, the parent widget that contains all the other widgets. These "elements" are in the "e" of the handler function and are identified by their names.
In the other direction, ie if you need to modify an Ui element from another function then you can get this element by its ID (getElementbyId()) and assign it a value just the same way as you'd do it in the UI definition function.
I copy/paste a sample code from another post to illustrate what I said, you can see the e.parameter.chkmode that holds the value of the checkBox and I'll add a Label to show the reverse process (the text is changed when the button is clicked).
Hoping I was clear enough,
var sh = SpreadsheetApp.getActiveSheet();
var ss = SpreadsheetApp.getActiveSpreadsheet();
//
function move() {
var app = UiApp.createApplication().setTitle("move test")
.setHeight(100).setWidth(400).setStyleAttribute("background-color","beige");
var panel = app.createVerticalPanel();
var next = app.createButton('next').setWidth('180');
var chkmode = app.createCheckBox("moving mode (checked = up/dwn, unchecked=L/R)").setValue(false).setName('chkmode');
var label = app.createLabel("test Label with text that will be modified on click").setId('label');
panel.add(next).add(chkmode).add(label);
var handler = app.createServerHandler('click').addCallbackElement(panel);
next.addClickHandler(handler);
app.add(panel);
ss.show(app);
}
//
function click(e) {
var app = UiApp.getActiveApplication();
var activeline = sh.getActiveRange().getRow();// get the row number of the selected cell/range
var activecol = sh.getActiveRange().getColumn();// get the row number of the selected cell/range
var label = app.getElementById('label');
label.setText('You have clicked the button');
var chkmode=e.parameter.chkmode;
if(chkmode=="true"){
activeline++
}else{
activecol++}
var sel=sh.getRange(activeline,activecol);
sh.setActiveSelection(sel);// make the next row active
return app;
}
For a UI and events tutorial, I recommend:
https://developers.google.com/apps-script/uiapp

How to know which Radio Button is selected?

I have 3 Radio buttons in my Ui in the same Radio group. They are,
var rbutton1 = app.createRadioButton('dist','5 miles');
var rbutton2 = app.createRadioButton('dist','10 miles');
var rbutton3 = app.createRadioButton('dist','25 miles');
In the event handler function, the variable, e.parameter.dist gives true or false just based on whether rbutton3 (the last radio button) is checked or not. Is there any way to determine what radio button is selected exactly?
The only way the make radio buttons group work like this (as intended by design) is by using them in a FormPanel and looking the name (in your case "dist") on a doPost from a submit action of the form.
There's some workarounds though, using the new client handlers that make it radio buttons usage on any panel roughly the same as on the from. Please take a look at this issue on the tracker. You may want to star this issue as well, to keep track of updates and kind of vote for it.
I use:
eventData.parameter.source
and pick up the change using addClickHandler.
You need to store this somewhere
Are these buttons suppose to be in an exclusive OR mode. If so, they need to have the same name. Look at Serge's answer for a detailed explanation and example code.
in the meantime I came up with a workaround to set the radiobutton as well, in this example I use a listBox but any other data could be used.
Here is the complete code : (to test in a spreadsheet container)
function radiotest() {
var app = UiApp.createApplication();
var panel = app.createVerticalPanel();
var radioValue = app.createTextBox().setId('radioValue');
radioValue.setId("radioValue").setName("radioValue");
var listhandler = app.createServerHandler('listhandler').addCallbackElement(panel);
var list = app.createListBox().addChangeHandler(listhandler).setName('list');
for(var i = 1; i < 10; i++){
var name = 'choice '+i;
list.addItem('Activate '+name,name)
var handler = app.createClientHandler().forTargets(radioValue).setText(name);
panel.add(app.createRadioButton('radioButtonGroup',name).addValueChangeHandler(handler).setId(name));
}
panel.add(radioValue);
var getit=app.createButton("Valide").setId("val");
panel.add(getit).add(list)
var handler = app.createServerHandler("valide")
handler.addCallbackElement(panel)
getit.addClickHandler(handler);
app.add(panel);
SpreadsheetApp.getActiveSpreadsheet().show(app);// show app
}
//
function valide(e){ ;// This function is called when key "validate" is pressed
var sh = SpreadsheetApp.getActiveSheet();
var RadioButton = e.parameter.radioValue;
sh.getRange('A1').setValue(RadioButton);
var app = UiApp.getActiveApplication();
return app;
}​
function listhandler(e){ ;// This function is called when listBox is changed
var sh = SpreadsheetApp.getActiveSheet();
var app = UiApp.getActiveApplication();
var listvalue = e.parameter.list
var radioValue = app.getElementById('radioValue').setValue(listvalue)
sh.getRange('A2').setValue(listvalue);
var radiobutton = app.getElementById(listvalue)
radiobutton.setValue(true)
return app;
}​
the selected radioButton values comes in the textBox value and the listBox allows to select which radioButton is activated... it shows up like this
There is also another approach, as stated by eddyparkinson that is to use the e.parameter.source but this works only if the handler is assigned directly to the radioButton and not using a 'submit' button. In many case it can be used and makes the code a(little) bit lighter.
Here is a test of this code
function radiotest2() {
var app = UiApp.createApplication();
var panel = app.createVerticalPanel();
var listhandler = app.createServerHandler('listhandler2').addCallbackElement(panel);
var list = app.createListBox().addChangeHandler(listhandler).setName('list');
var handler = app.createServerHandler("valide2")
handler.addCallbackElement(panel)
for(var i = 1; i < 10; i++){
var name = 'choice '+i;
list.addItem('Activate '+name,name)
panel.add(app.createRadioButton('radioButtonGroup',name).setId(name).addClickHandler(handler));
}
panel.add(list)
app.add(panel);
SpreadsheetApp.getActiveSpreadsheet().show(app);// show app
}
function valide2(e){ ;// This function is called when a radioButton is selected
var sh = SpreadsheetApp.getActiveSheet();
var source = e.parameter.source;
var radioValue = '';
if(source.match('choice')=='choice'){radioValue=source}
sh.getRange('A1').setValue(radioValue);
var app = UiApp.getActiveApplication();
return app;
}​
function listhandler2(e){ ;// This function is called when listBox is changed
var sh = SpreadsheetApp.getActiveSheet();
var app = UiApp.getActiveApplication();
var listvalue = e.parameter.list
sh.getRange('A2').setValue(listvalue);
var radiobutton = app.getElementById(listvalue)
radiobutton.setValue(true)
return app;
}​

Unable to use list box with server handler to change text of label

If the title is confusing, hopefully this makes it more clear what I'm trying to do:
function doGet(e) {
var app = UiApp.createApplication();
var list = app.createListBox().setName('list');
list.addItem("this");
list.addItem("that");
list.addItem("they");
var handler = app.createServerHandler('foo').addCallbackElement(list);
list.addChangeHandler(handler);
var label = app.createLabel("test").setId('label');
var panel = app.createVerticalPanel();
panel.add(list);
panel.add(label);
app.add(panel);
return app;
}​
function foo(e) {
var app = UiApp.getActiveApplication();
var value = e.parameter.list;
var label = app.getElementById('label');
label.setText(value);
}
It doesn't call any errors. If I intentionally put an error in foo, I get an error message, so I'm assuming the handler is getting called and just isn't doing anything. This works in a spreadsheet if I just have it bring up a Browser.msgBox(value), so I know that much works.
I'm trying to use this in a program that will automatically update all the listboxes on the page based on what is selected in the first listbox. I've been able to change things like visibility using server handlers and app.getElementById, but only with radio buttons, not a list box. I'm clearly doing something wrong here, but it's not obvious what that is.
To have your changes to the UiApp updated you have to return the app on your handler, e.g.
//...
label.setText(value);
return app;
}