What is the best way to manage two UI's? - google-apps-script

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;
};

Related

How can I change focus between panels in a StackPanel using GAS

I have a stackpanel containing 3 panels.
After the user clicks a button on page-1, I want page-2 to become visible.
How can I achieve this?
Edit-1
As I thought the question I asked really is a general one, I did not provide code.
But Serge insas and Zig Mandell asked for code, so here it is.
function doGet()
{
var app = UiApp.createApplication();
var stackPanel = app.createStackPanel().setSize('100%', '100%'); //Create stack panel
var onClick = app.createServerHandler('onClick');
var button = app.createButton('Button on second panel...').setId('btnPageTwo').addClickHandler(onClick);
//add widgets to each stack panel, and name the stacked panels
stackPanel.add(app.createLabel('Text on first panel...'), 'One');
stackPanel.add(button, 'Two');
stackPanel.add(app.createLabel('Text on third Panel...'), 'Three');
app.add(stackPanel); //Add the panel to the application
return app;
}
function onClick(e)
{
Logger.log('In onClick --> show stackPanel "Three" now');
}
In this example I would like to show panel Two at startup and after clicking the button I would like to show panel 3.
I tried using focusPanels, but that didn't help
function doGet()
{
var app = UiApp.createApplication();
var stackPanel = app.createStackPanel().setSize('100%', '100%'); //Create stack panel
// FocusPanels are limited to contain ONE widget
var focusOne = app.createFocusPanel().setId('focusOne');
var focusTwo = app.createFocusPanel().setId('focusTwo');
var focusThree = app.createFocusPanel().setId('focusThree');
// Create panels to overcome the one-widget-limitation of focuspanels
var ver = app.createVerticalPanel().setId('ver');
var hor = app.createHorizontalPanel().setId('hor');
var tab = app.createTabPanel().setId('tab');
var tabOne = app.createVerticalPanel().setId('tabOne');
var tabTwo = app.createHorizontalPanel().setId('tabTwo');
tab.add(tabOne, 'One').add(tabTwo, 'Two');
focusOne.add(ver);
focusTwo.add(hor);
focusThree.add(tab);
var labOne = app.createLabel('Text on first panel...');
var labThree = app.createLabel('Text on second tab of third panel...');
var onClick = app.createServerHandler('onClick');
var button = app.createButton('Button on second panel...').setId('btnPageTwo').addClickHandler(onClick);
ver.add(labOne);
hor.add(button);
tabTwo.add(labThree);
tab.selectTab(1); // Select second tab
//add widgets to each stack panel, and name the stack panel
stackPanel.add(focusOne, 'stackOne').add(focusTwo, 'stackTwo').add(focusThree, 'stackThree');
app.add(stackPanel); //Add the panel to the application
return app;
}
function onClick(e)
{
Logger.log('In onClick --> show focusPanel "Three" now');
var app = UiApp.getActiveApplication();
var focusThree = app.getElementById('focusThree');
focusThree.setFocus(true);
return app;
}
Unfortunately this has been the subject of an enhancement request for quite a while (dec 2012) but I'm afraid Google won't do anything about it since they stopped UiApp development (they recommend switching to HTMLService).
You could eventually use tabPanel instead, this one has all the necessary features.
Test here
code below :
function doGet() {
var app = UiApp.createApplication();
var tabPanel = app.createTabPanel().setSize('100%', '100%').setId('tabP'); //Create tab panel
var onClick = app.createServerHandler('onClick');
var button = app.createButton('Button on second panel...').setId('btnPageTwo').addClickHandler(onClick);
//add widgets to each tab panel, and name the tabed panels
tabPanel.add(app.createLabel('Text on first panel...'), 'One');
tabPanel.add(button, 'Two');
tabPanel.add(app.createLabel('Text on third Panel...'), 'Three');
app.add(tabPanel); //Add the panel to the application
tabPanel.selectTab(1);
return app;
}
function onClick(e){
var app = UiApp.getActiveApplication();
var tabP = app.getElementById('tabP').selectTab(2);
return app;
}

refreshing a button's text is not working with GAS

I have the following code which creates a simple app to allow the user to enter two values and click a button:
function start() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var app = UiApp.createApplication();
app.setTitle("Appraisals Analysis");
app.setHeight(100);
app.setWidth(500);
var grid = app.createGrid(3, 2);
grid.setId("grid");
grid.setCellSpacing(2);
grid.setCellPadding(2);
var uafLabel = app.createLabel("Unprocessed apparaisals folder name: ");
uafLabel.setStyleAttributes({"font-weight": "bold"});
var uafTextBox = app.createTextBox();
uafTextBox.setName('uafTextBox').setId('uafTextBox');
uafTextBox.setText('Unprocessed Appraisals');
grid.setWidget(0, 0, uafLabel);
grid.setWidget(0, 1, uafTextBox);
var pafLabel = app.createLabel("Processed apparaisals folder name: ");
pafLabel.setStyleAttributes({"font-weight": "bold"});
var pafTextBox = app.createTextBox();
pafTextBox.setName('pafTextBox').setId('pafTextBox');
pafTextBox.setText('Processed Appraisals');
grid.setWidget(1, 0, pafLabel);
grid.setWidget(1, 1, pafTextBox);
var button = app.createButton('Submit').setId("submitButton");
grid.setWidget(2, 0, button);
var mypanel = app.createVerticalPanel();
mypanel.add(grid);
app.add(mypanel);
var clickHandler = app.createServerClickHandler("parseFiles");
button.addClickHandler(clickHandler);
clickHandler.addCallbackElement(grid);
ss.show(app);
}
I then have the parseFiles function which can take up to 2 minutes to do its job as follow
function parseFiles(e) {
var app = UiApp.getActiveApplication();
var processedFolder = DocsList.getFolder(e.parameter.pafTextBox);
var workingFolder = DocsList.getFolder(e.parameter.uafTextBox);
var appraisals = workingFolder.find('Performance Appraisal');
app.getElementById("submitButton").setText("Parsing Files...");
for (var i in appraisals) {
var doc = DocumentApp.openById(appraisals[i].getId());
parseDocument(doc, getEmpName(doc.getName()));
}
return app;
}
My problem is that when I click the button, the work gets done, but the button text stays as "Submit" instead of changing to "Parsing Files...". Once the work is over, then the button changes.
Any idea what I may be doing wrong?
Regards
Crouz
You got nothing wrong, just the concept. Think like this: all Apps Script code you write runs on a google server (server-side), but the interface is (obviously) shown on your computer (client-side). The Apps Script "environment" has a client-side script (that we do not have access or control of) that receives the information on how to build the interface you defined in your code (server-side).
So, everything you do in your code gets updated at once, in a bundle, after your function finishes. And that's why we need to return app, so that our UiApp definition gets sent/returned to the client-side that have triggered the script.
For very simple situations, like disabling or setting the text on a button or label, there's a clientHandler that can perform basic operations on directly the client-side without requiring a network trip to the server-side to run your custom code. Since these operations are done on the client-side they're done "instantly". Note that this is not for generic code, but only predefined operations. clientHandlers are really meant just for simple stuff. It's difficult (if not impossible) to do complex operations.
Here's my suggestion using a clientHandler:
function start() {
//your current code...
clickHandler.addCallbackElement(grid);
var clientHandler = app.createClientHandler().forEventSource().setText('Parsing Files...').setEnabled(false);
button.addClickHandler(clientHandler);
ss.show(app);
}
function parseFiles(e) {
//...
app.getElementById("submitButton").setText("Submit again").setEnabled(true);
//...
return app;
}
Note that you can add multiple handlers, client or server, to a button (or any other widget that accept handlers) and all of them will run concurrently.
Also, it's very important to notice that we're talking about UiApp here, when using HtmlService the approach is significantly different.

Embedding a button in a grid causes the grid to behave as the button?

Using Google Apps Script to build a web app, when I put a button widget in a grid widget, it seems the entire grid turns into the "button".
i.e. if I put:
var myGrid = app.createGrid(4,4);
var addButton = myGrid.setWidget(3,3,app.createButton("Add"));
var handler = app.createServerClickHandler('add');
addButton.addClickHandler(handler);
app.add(myGrid);
In the resulting web app if I click anywhere on the grid the clickhandler for the button fires. Even worse, if I embed multiple buttons in the grid, clicking anywhere on the grid fires all button clickhandlers.
Are buttons in grids not supported? Or am I doing something wrong?
Thanks.
Edit: If you want to see the behaviour for yourself, I've replicated the issue by modifying one of Google's examples. The second example for "Validators" here: https://developers.google.com/apps-script/uiapp#Validators I modified and put the textboxes and the button in a Grid Widget. After entering numbers in the text boxes, every time you click anywhere on the grid the "add" clickhandler will fire and add the numbers again:
function doGet() {
var app = UiApp.createApplication();
var rgx = "^\\$?[0-9]+$";
// Create input boxes and button.
//var textBoxA = app.createTextBox().setId('textBoxA').setName('textBoxA');
//var textBoxB = app.createTextBox().setId('textBoxB').setName('textBoxB');
var myGrid = app.createGrid(4,4);
myGrid.setWidget(0,0,app.createTextBox().setId('textBoxA').setName('textBoxA'));
myGrid.setWidget(0,1,app.createTextBox().setId('textBoxB').setName('textBoxB'));
var textBoxA = app.getElementById('textBoxA');
var textBoxB = app.getElementById('textBoxB');
var addButton = myGrid.setWidget(3,3,app.createButton("Add").setEnabled(false));
var label = app.createLabel("Please input two numbers");
// Create a handler to call the adding function.
// Two validations are added to this handler so that it will
// only invoke 'add' if both textBoxA and textBoxB contain
// numbers.
var handler = app.createServerClickHandler('add').validateMatches(textBoxA,rgx).validateMatches(textBoxBrgx).addCallbackElement(textBoxA).addCallbackElement(textBoxB);
// Create a handler to enable the button if all input is legal
var onValidInput = app.createClientHandler().validateMatches(textBoxA,rgx).validateMatches(textBoxB,rgx).forTargets(addButton).setEnabled(true).forTargets(label).setVisible(false);
// Create a handler to mark invalid input in textBoxA and disable the button
var onInvalidInput1 = app.createClientHandler().validateNotMatches(textBoxA,rgx).forTargets(addButton).setEnabled (false).forTargets(textBoxA).setStyleAttribute("color", "red").forTargets(label).setVisible(true);
// Create a handler to mark the input in textBoxA as valid
var onValidInput1 = app.createClientHandler().validateMatches(textBoxA,rgx).forTargets(textBoxA).setStyleAttribute("color", "black");
// Create a handler to mark invalid input in textBoxB and disable the button
var onInvalidInput2 = app.createClientHandler().validateNotMatches(textBoxB,rgx).forTargets(addButton).setEnabled(false).forTargets(textBoxB).setStyleAttribute("color", "red").forTargets(label).setVisible(true);
// Create a handler to mark the input in textBoxB as valid
var onValidInput2 = app.createClientHandler().validateMatches(textBoxB,rgx).forTargets(textBoxB).setStyleAttribute("color","black");
// Add all the handlers to be called when the user types in the text boxes
textBoxA.addKeyUpHandler(onInvalidInput1);
textBoxB.addKeyUpHandler(onInvalidInput2);
textBoxA.addKeyUpHandler(onValidInput1);
textBoxB.addKeyUpHandler(onValidInput2);
textBoxA.addKeyUpHandler(onValidInput);
textBoxB.addKeyUpHandler(onValidInput);
addButton.addClickHandler(handler);
app.add(myGrid);
//app.add(textBoxB);
//app.add(addButton);
app.add(label);
return app;
}
function add(e) {
var app = UiApp.getActiveApplication();
var result = parseFloat(e.parameter.textBoxA) + parseFloat(e.parameter.textBoxB);
var newResultLabel = app.createLabel("Result is: " + result);
app.add(newResultLabel);
return app;
}
When you write
var addButton = myGrid.setWidget(3,3,app.createButton("Add"));
and then you add a handler to the variable addButton you are in fact adding the handler to the grid, not to the button.
I would suggest to rewrite it like this (I commented the code) and it will work normally
var myGrid = app.createGrid(4,4);
var addButton = app.createButton("Add");
myGrid.setWidget(3,3,addButton);// here you add the button to the grid
var handler = app.createServerClickHandler('add');
addButton.addClickHandler(handler);
app.add(myGrid);// only the grid must be added, the button is already in it
or, if you want to make it more compact :
var handler = app.createServerClickHandler('add');
var myGrid = app.createGrid(4,4).setWidget(3,3,createButton("Add",handler));// here you add the button to the grid
app.add(myGrid);// only the grid must be added, the button is already in it

google script unable to update web page

Below is snippet where I create a web page / app and show a list. Then on click of item in list I want to replace the list with another list. I try it by using vertical panel as root container and clearing and adding list to it. The web page though keeps showing old list even after handler for new list executes fine.
//user_list function updates vertical panel but the web page still shows old rep_list
function doGet(e) {
if(e == null || e.parameter.dvid == null) {
return rep_list();
}
}
function user_list(path) {
var app = UiApp.getActiveApplication().setTitle("List Folders");
var content = app.getElementById('root');
content.clear();
var list = app.createListBox();
//populate list
content.add(list);
return app;
}
function rep_list () {
var app = UiApp.createApplication().setTitle("List Repositories");
var content = app.createVerticalPanel().setId('root');
var list = app.createListBox(true).setId('repoList').setName('repo');
var handler = app.createServerHandler("listFolders");
list.addClickHandler(handler)
//populate list
content.add(list);
app.add(content);
return app;
}
function listFolders(e){
var repo = e.parameter.repo;
user_list(repo);
}
Regards,
Miten.
Your listFolders() function isn't returning a new UI instance. Try:
function listFolders(e){
var repo = e.parameter.repo;
var app = user_list(repo);
return app;
}

Safe getElementById or try to determine if ID exists in GUI

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;
}