All,
I am trying to implement a StackPanel containing multiple sub-panels to collect user data. Each subpanel (I have named them SwapRecordPanels) represents a line in a Google spreadsheet containing the data for two people who want to swap which days they will work. The SwapRecordPanels contain a delete button. On clicking the delete button, I would like the SwapRecordPanel to remove itself from the StackPanel.
However, I have found that when I attempt to remove an element from a StackPanel, the change does not register in the GUI. As an attempt at the proof of the concept that I could delete elements in a StackPanel, I added an extra button titled "Click Me". As you can see in the code below, its handler attempts to remove an element from the StackPanel "LeftPanel." However, when I click the button, the "LeftPanel" remains unchanged. I'm wondering if there's some sort of refresh/repaint method to be called to update the GUI. If not, could anyone offer any advice? Any help would be greatly appreciated.
function myViewer() {
var myapp = doGet();
SpreadsheetApp.getActive().show(myapp)
}
function doGet() {
var myapp = UiApp.createApplication();
var LeftPanel = myapp.createStackPanel().setId("LeftPanel");
var LeftScrollPanel = myapp.createScrollPanel(LeftPanel).setPixelSize(410,200)
myapp.add(LeftScrollPanel)
var SwapPane1 = SwapRecordPanel(LeftPanel);
var SwapPane2 = SwapRecordPanel(LeftPanel);
LeftPanel.add(SwapPane1,"Test 1 Header");
LeftPanel.add(SwapPane2, "Test 2 Header");
var myButton = myapp.createButton("Click Me").addClickHandler(myapp.createServerHandler("ButtonClicker"));
myapp.add(myButton);
return myapp;
}
function ButtonClicker()
{
Logger.log("I was clicked")
var myapp = UiApp.getActiveApplication();
var LeftPanel = myapp.getElementById("LeftPanel");
LeftPanel.remove(myapp.getElementById("panel1"));
}
You have to "return" the changes to the app from the handler function for the UI to update.
Simply add return myapp; at the end of the ButtonClicker() function.
Related
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.
I want script when button is pressed change its color immediately but actually it's change it's color after script of copyProject is completed, copyProject function takes around 40 second
I have thought to split handler function to make it change color on press and before running copyProject Function, any ideas ?
function copyProject(e)
{
var app=UiApp.getActiveApplication();
var button=app.getElementById("button");
process(app,button);
var sourceProjectID=e.parameter.id;
copProject(sourceProjectID);
return app.close();
}
function process(app,button)
{
button.setStyleAttribute("color", "red").setText("Processing");
return app;
}
Thanks
Here is an example of a Client Server and a serverHandler on a button that simulates the behavior you want : the button changes color when you click it ... after the long serverHandler function it shows a message.
function doGet(){
var app = UiApp.createApplication();
var handler = app.createServerHandler('test');
var cHandler = app.createClientHandler();
var btn = app.createButton('click this button').addClickHandler(handler).addClickHandler(cHandler);
cHandler.forEventSource().setStyleAttributes({'color':'red'})
app.add(btn);
return app
}
function test(e){
var app = UiApp.getActiveApplication();
app.add(app.createLabel('operation completed'));
Utilities.sleep(4000);
return app;
}
Live test here
Processing happens on the server, thus it will only redraw the app after the process is complete and your UI will not be responsive.
The proper way to work with the UI and make it responsive is to use client handler which enable you update your UI on the fly. Also look into validators, which are useful to validate inputs before you process them without going to the server.
https://developers.google.com/apps-script/uiapp#ClientHandlers
https://developers.google.com/apps-script/uiapp#Validators
Let me know if I can help you further
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
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.
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;
};