Multiple Page UI using UiService - google-apps-script

I would like to use Google Apps Script UiService to produce a multiple page user interface.
Here's what I've got so far:
function doGet(e)
{
var app=UiApp.createApplication();
var nameLabel=app.createLabel('Name:');
var button=app.createButton("next");//my button on clicking,trying to divert to other UI
var handler=app.createServerHandler("myclick");
button.addClickHandler(handler);
app.add(namelabel);
app.add(button);
return app;
}
function myClick(){
//on clicking the button it should call the other ui or other html page
is there any method for that.}
How can I do this?

You should look at How To Allow Users to Review Answers before Submiting Form?, which has an example that does this.
The idea is to create your UiApp with multiple Panels, then show or hide them in response to user actions, using setVisible(). (If you were using the HtmlService, you would enclose your "pages" in different <div>s, and change their display attributes. See toggle show/hide div with button?.)
The Best Practices also describes use of client-side handlers for responsiveness, so let's try that.
/**
* Very simple multiple page UiApp.
*
* This function defines two panels, which appear to the end user
* as separate web pages. Visibility of each panel is set to
* control what the user sees.
*/
function doGet() {
var app = UiApp.createApplication();
var page1 = app.createFlowPanel().setId('page1');
var page2 = app.createFlowPanel().setId('page2');
// Content for Page 1
page1.add(app.createLabel('Page 1'));
var page1Button = app.createButton('Next Page');
page1.add(page1Button);
// Create client handler to "change pages" in browser
var gotoPage2 = app.createClientHandler()
.forTargets(page1).setVisible(false)
.forTargets(page2).setVisible(true);
page1Button.addClickHandler(gotoPage2);
// Content for Page 2
page2.add(app.createLabel('Page 2'));
var page2Button = app.createButton('Previous Page');
page2.add(page2Button);
// Create client handler to "change pages" in browser
var gotoPage1 = app.createClientHandler()
.forTargets(page1).setVisible(true)
.forTargets(page2).setVisible(false);
page2Button.addClickHandler(gotoPage1);
app.add(page1);
app.add(page2);
// Set initial visibility
page1.setVisible(true);
page2.setVisible(false);
return app;
}
That works for changing the view of the UI. To extend this for general purposes, you would likely want to add server-side handlers to the same buttons to perform work, and update the contents of the panels as things progress.

Here is 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.

You could load different UI's on reading the parameters in your app.
The doGet(e) passes the parameters in the app's url. This way you could call your app with for example: ?myapp=1 (url parameter).
in your doGet you could read that parameter with: e.parameter.myapp
This way you could load different applications depending on the parameters that where passed.
You could just change your button with a link (to your own app, with different url parameters).
You could also do it with buttons and handlers but the above way has my preference.
If you want to use a button<>handler just change you main (first panel) and each time add a completely new panel to your app object. This way you would start from scratch (i.e. create a new application).

Related

Using server handlers with modal dialogs

I am displaying a User Interface over a sheet using showModalDialog passing in the app I just created. I also setup a button with a server handler. When server handler function is called I try to get the app again using "UiApp.getActiveApplication()" to hide some elements and show some different elements, however, the changes are not reflected. At the end of the method I tried to close the app, and show a new modal dialog, I tried to return the app, I tried to do nothing, and nothing seems to work.
I can't post my whole code since it is very long, so I made a very simple version that gets the point across. When I put some logging statements in testHandler() it proves that the code is running.
function test() {
var app = UiApp.createApplication().setHeight(700).setWidth(1500);
var label = app.createLabel("Hi").setId("label");
var label2 = app.createLabel("GoodBye").setId("label2").setVisible(false);
var button = app.createButton("Press Me").setId("button");
app.add(label);
app.add(label2);
app.add(button);
var testHandler = app.createServerHandler('testHandler');
testHandler.addCallbackElement(label);
testHandler.addCallbackElement(label2);
button.addClickHandler(testHandler);
SpreadsheetApp.getUi().showModalDialog(app, 'Test');
}
function testHandler() {
var app = UiApp.getActiveApplication();
app.getElementById('label').setVisible(false);
app.getElementById('label2').setVisible(true);
// Not sure what to do now
}
Thank you in advance for your help
return app; //where you are not sure what do do

GAS - define listBox() displayed value

I have a form built and displayed in UiApp which uses the listBox class. My listBoxes are created as in the example below:
var yearText = app.createListBox().setName("yearText")
.addItem("")
.addItem("Reception")
.addItem("Nursery");
When the form loads for the first time they default to display blank (the value at index 0). What I am trying to do is reload the form with stored data, populating the listBox with it's saved value whilst still offering the same options as above (blank, Reception, Nursery).
I have tried using the various setValue methods available to the listBox class but I am not making any progress (variations of setValue are working fine for textArea, checkBox and dateBox classes elsewhere in the form). Any help or guidance gratefully received!
The method for that is setItemSelected(index,Boolean), example below
function doGet(){
var app = UiApp.createApplication();
var list = app.createListBox().addItem('').addItem('v2').addItem('v3').addItem('v4').setItemSelected(2,true);
app.add(list);
return app;
}
Or also : setSelectedIndex(index) (same link for doc
example below too
function doGet(){
var app = UiApp.createApplication();
var list = app.createListBox().addItem('').addItem('v2').addItem('v3').addItem('v4').setSelectedIndex(2);
app.add(list);
return app;
}

create a new page in a form dynamically, based on data of the prev. page

I have a question regarding forms in google-apps-script. Lets say I have already created a form with a single page and a input box for text.
Is it possible to create the follow-up page dynamically, based on the data out of the textbox? Something like:
First Page: insert customer id -> continue -> Second Page: information about the customer.
I know that there are events like onLoad and onSubmit, but there is no onContinue event for example.
Is it possible to create something like that with google-apps-script? What would be the best way to archive such a behavior?
B.R.
Here is some working code
that demonstrates a multiple page form.
The code uses a single "hidden" state in a TextBox and multiple SubmitButtons 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 SubmitButtons are "rewired" using ClientHandlers that simply modify the hidden state prior to form submission.
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.

Google app script - callback confusion

I'm confused about the behavior of the following code sample.
Why can't I access statusLabelU in the callback via the app object ?
It is available in the argument
BTW, what is the type of the argument variable e in the callback ?
function doGet() {
var app = UiApp.createApplication();
var button = app.createButton('Enter Symbol');
app.add(button);
var symbolText = app.createTextBox().setName('symbolText').setId('symbolText');
app.add(symbolText);
var labelU = app.createLabel('Unknown symbol U')
.setId('statusLabelU');
var labelK = app.createLabel('Unknown symbol K')
.setId('statusLabelK');
app.add(labelU);
app.add(labelK);
var handler = app.createServerHandler('myClickHandler');
handler.addCallbackElement(symbolText);
button.addClickHandler(handler);
return app;
}
function myClickHandler(e) {
var app = UiApp.getActiveApplication();
var symU = app.getElementById('symbolText');
var symK = e.parameter.symbolText;
var financeU = FinanceApp.getStockInfo(symU);
var financeK = FinanceApp.getStockInfo(symK);
var label = app.getElementById('statusLabelU');
label.setText(financeU.name);
var label = app.getElementById('statusLabelK');
label.setText(financeK.name);
app.close();
return app;
}
If you run
labelU.setName('labelU');
handler.addCallbackElement(labelU);
you will be be able to access the value of the label in the callback like so:
var value = e.parameter.labelU;
The argument 'e' (or 'eventInfo') contains information about how the callback was triggered. There is some general information about user ID, x/y position of cursor, and also the source element that triggered the callback. Apart from that, values from widgets that are explicitly added to the handler will be accessible as parameters. You can always check out the content by doing a
Logger.log(e);
and check out the log from the coding environment (cmd/ctrl + return).
Actually you can access statusLabelU in the callback via the app object. What you cannot do (at least I dont know any way) to access the contents of a TextBox except than passing it as a parameter to your event-handler via addCallbackElement (you can also pass a container to addCallbackElement, then all elements in this container are passed to your event-handler). So what happens in your example:
var symU = app.getElementById('symbolText');
returns a kind of Proxy of your TextBox, which returns, when converted to a string 'Generic'.
FinanceApp.getStockInfo('Generic');
then in turn returns undefined, which is then set as Text of your label statusLabelU.
Yeah it took me a while to understand what was going on. The way I finally understood it is this:
The server processes stuff, then serves up UI to the client. Every time the client does something, like click a button, he submits this stuff to the server, but the server has no recollection of what it did before, so all those variables you made prior to serving the UI to the client, it no longer knows.
Thus if you want the server to remember those values it created from before serving the client, then you need to embed them along with the UI sent to the client so that when he does something, the data gets sent back to the server.
That embedded crap is considered a hidden callback element, something the user doesn't interact with, and is solely there to pass it back to the server during the next processing action. The 'normal' callback elements are data the server doesn't know yet, such as form elements (names, addresses, etc). It will need to know this information once the user hits the submit button to process it, so that's why it's called callback info.

Replicate tab Panel in GUI Builder google apps

I think I've seen this answer, but I can't remember where for certain.
I'm trying to to create a tabbed panel interface using the GUI Builder, but don't see that option. The part I seem to recall is someone having an approach to replicate that in the GUI Builder. I just can't seem to find that information in my brain, the old google groups or here.
Can someone jog my memory?
Thank you...
Maybe the post you were referring to was this one ? Anyway, no matter how much panels you have, yo could design them in the GUI, one on top of the other or (more simply) one under each other in a 'parent' vertical panel and play with client handlers to show/hide the one you need.
I have an example here with 2 panels in an UI, the UI is designed with script but that is not important, look at the client handlers to see how it works.
If I have some free time tonight I'll make a demo script for 4 panels in GUI ;-).
EDIT : here is a test example (standalone) or embedded in a Google site + link to the script (make a copy to edit)
Note that in the GUI builder you'l have to 'play' with visibility of each panel to work on it, I used a main panel large enough to hold 2 panels together so you can have a better vision of "harmony" between panels (which is not the case in my test;-))
and the code (very simple basic example 4 panels with each of them a textBox & a Label, just to test the handlers on the buttons):
function doGet() {
var app = UiApp.createApplication();
var UI=app.loadComponent('multiUi')
var panel1 = app.getElementById('panel1')
var panel2 = app.getElementById('panel2')
var panel3 = app.getElementById('panel3')
var panel4 = app.getElementById('panel4')
var Button1 = app.getElementById('Button1')
var Button2 = app.getElementById('Button2')
var Button3 = app.getElementById('Button3')
var Button4 = app.getElementById('Button4')
var pHandler1 = app.createClientHandler()
.forTargets(panel1).setVisible(true).forTargets(panel2,panel3,panel4).setVisible(false)
Button1.addClickHandler(pHandler1)
var pHandler2 = app.createClientHandler()
.forTargets(panel2).setVisible(true).forTargets(panel1,panel3,panel4).setVisible(false)
Button2.addClickHandler(pHandler2)
var pHandler3 = app.createClientHandler()
.forTargets(panel3).setVisible(true).forTargets(panel2,panel1,panel4).setVisible(false)
Button3.addClickHandler(pHandler3)
var pHandler4 = app.createClientHandler()
.forTargets(panel4).setVisible(true).forTargets(panel2,panel3,panel1).setVisible(false)
Button4.addClickHandler(pHandler4)
app.add(UI)
return app;
}
The following code makes the tabs, based on the array you put in, dynamically:
function doGet() {
// create application
var app = UiApp.createApplication();
// set array
var aTabs = ['donald','katrijn','dagobert'];
// create tab panel
var pTab = app.createTabPanel();
// add tabs to panel
for(var k=0; k<pTabs.length; k++) {
pTab.add(app.createLabel("This tab is reserved for " + aTabs[k]), aTabs[k]);
}
// add panel to application
app.add(pTab);
// set focus to first tab
pTab.selectTab(0);
// return to application
return app;
}
See link for tabPanel reference.
Publishing your script as a web-app, allows you to insert the script on a google sites.