Identify a tab in a tabPanel after selection using GAS - google-apps-script

I want to add tabs to a tabPanel dynamically depending on certain criteria.
Sometimes a user will have 3 tabs, another time he (or another user) might have up to 10 tabs. Also the order of tabs added might be different each time.
So I would like to be able to identify the tabs by id and not by index.
After adding several tabs to a tabPanel you can select a tab by
tabPanel.selectTab(2); // Will select third tab of the tabPanel
Using a selectionHandler I can identify the tabPanel and the index of the tab selected
var onSelectHandler = app.createServerHandler('onSelectHandler');
tabPanel.addSelectionHandler(onSelectHandler)
...
function onSelectHandler(e)
{
var source = e.parameter.source; // Id of the tabPanel
var indexTab = e.parameter[source]; // Index of the tab selected
}
But I can't findout how I can get the id of the tab selected nor select a tab by id.
Is there a way to identify a tab by Id apart from hidden fields and other tricks?

Individual tabs do not have Ids, only TabPanels have Ids. A TabPanel has multiple Tabs.
However, you can pass arbitrary information about the TabPanel around via setTag() and JSON.
Something like this for instance:
function doGet() {
var app = UiApp.createApplication();
var clickHandler = app.createServerHandler('myClickHandler');
var tab = app.createTabPanel().setId('asdf');
var fooLabel = app.createLabel('I am foo.').setId('fooId');
var barLabel = app.createLabel('I am bar.').setId('barId');
var tag = [];
tab.add(fooLabel, 'Foo');
tag.push(fooLabel.getId());
tab.add(barLabel, 'Bar');
tag.push(barLabel.getId());
tab.addSelectionHandler(clickHandler);
tab.setTag(JSON.stringify(tag));
app.add(tab);
return app;
}
function myClickHandler(e) {
var app = UiApp.getActiveApplication();
var tag = JSON.parse(e.parameter[e.parameter.source + '_tag']);
var id = tag[e.parameter[e.parameter.source]];
Logger.log(id);
return app;
}
Click on the resulting tabs named Foo and Bar and then check your log.
Note, however, that this happens every time you click on the header, even if it's already the one selected. Use with caution.
I am curious though: What do you plan on doing with this information in the callback?
Are you anticipating updating the content of the panel on the fly, with current data as the tab is exposed? Or populating the panel with widgets at that time?

Related

How can I add choices in a Google Form with Google Apps Script

I'm trying to master creating Google Forms programmatically, but can't assign choices to a multiple-choice item. I can create the item (testQuestion) and give it a title, but my createChoice() statements don't add choices.
Here's my code, based on https://developers.google.com/apps-script/reference/forms/page-navigation-type
function testCreateChoices() {
/* modification of
testPageNavigation()
to see how to import choices from a range on sheet
*/
// Create a form
var form = FormApp.create('createChoice Test');
// add a multiple-choice item
var testQuestion = form.addMultipleChoiceItem();
testQuestion.setTitle('Anything here?');
var whatsis = testQuestion.createChoice('bozzle');
var firstChoice = testQuestion.createChoice('this');
testQuestion.createChoice("that");
testQuestion.createChoice("the other");
testQuestion.createChoice("Ouch!");
//add a new multiple-choice item and a pagebreak item
var item = form.addMultipleChoiceItem();
var pageBreak = form.addPageBreakItem();
// Set some choices with go-to-page logic.
var rightChoice = item.createChoice('Vanilla', FormApp.PageNavigationType.SUBMIT);
var wrongChoice = item.createChoice('Chocolate', FormApp.PageNavigationType.RESTART);
// For GO_TO_PAGE, just pass in the page break item. For CONTINUE (normally the default), pass in
// CONTINUE explicitly because page navigation cannot be mixed with non-navigation choices.
var iffyChoice = item.createChoice('Peanut', pageBreak);
var otherChoice = item.createChoice('Strawberry', FormApp.PageNavigatio[enter image description here][1]nType.CONTINUE);
item.setChoices([rightChoice, wrongChoice, iffyChoice, otherChoice]);
}
Here's what I get, with the choices "bozzle" and so on not displayed, and an image of what I want but can't create.
Many thanks for any help!
Here's a screenshot, with no labels/choices under "Anything here?"
And a mockup with "bozzle", "this" and so on as choices
The reason you are not seeing the options for the testQuestion is because you didn't set the choices for the question.
Therefore, I suggest you update the bit where you create your first question to this:
var testQuestion = form.addMultipleChoiceItem();
testQuestion.setChoices([testQuestion.createChoice('bozzle'), testQuestion.createChoice('this'), testQuestion.createChoice("that"), testQuestion.createChoice("the other"), testQuestion.createChoice("Ouch!")]);
The createChoice method is used to create the choice items only and in order to actually add them to the question, you have to use the setChoices method as well.
Reference
Apps Script Class MultipleChoiceItem - createChoice(value);
Apps Script Class MultipleChoiceItem - setChoices(Choice).

Use Over-grid Button to reference button's anchor cell in script

I'm looking to use a button's anchor cell in the function that that button calls. My buttons are inserted on different rows. Clicking on one should have it interact only with the data on the row of its anchor cell.
This is for use in a Sheets/Apps Script that I use to search our G Suite domain's Google Groups for external mailing addresses. It is easy to do a memberOf() check for internal users, but G Suite has no way of searching for external users' group memberships.
The script creates a "Remove" button next to each result. Clicking the remove button should remove the user from the group listed
The removal script is working as long as the correct cell is provided. I just can't see how to make sure the button's anchor cell becomes the selected/active cell when the user interacts with it.
Here's the script for adding the remove buttons, to give you an idea of how they're inserted:
function createRemoveButtons(numberOfResults) {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var searchResultStartingColumn = 3;
var searchResultStartingRow = 4;
for (let i = 0; i < numberOfResults.length; i++) {
const resultRow = numberOfResults[i];
sheet.getRange(searchResultStartingRow + i, searchResultStartingColumn)
.activate();
// Get Remove button image.
var ImageBlob = DriveApp.getFileById("1wxSNDJhRz5ehxNZBFCoshMW8j2nFTfGx")
.getBlob().getAs('image/png');
var image = sheet.insertImage(ImageBlob, searchResultStartingColumn, searchResultStartingRow, 1, 1);
image.setHeight(29).setWidth(73).assignScript("removeUserFromGroup");
}
}
You have to create a function for each button and use that function to set the row to act on when that button is clicked.
function buttonRow10(){
var row = 10;
// Do the things to do on row 10
}
The above because the function called by clicking on a button isn't aware of which button was clicked. In other words, contrary as occurs on simple/installable triggers that have an event object as argument, the functions assigned to a button hasn't it.

Multiple Page UI using UiService

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).

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.

handler design for dynamically created listboxes

I'm trying to make a UI that allow users to select an action for each agenda item in a spreadsheet. After a user select an action, I would like to update the spreadsheet with the selection. Since the data in the spreadsheet are not static, the UI was written dynamically.
This is how I created the listboxes :
// add labels and a drop down box of actions for each agenda item mark for today
for (i = 0; i < labels.length; i++) {
// labels is an array of objects
var topic = labels[i]['topic'];
// add label to grid
myGrid.setWidget(i, 0, myApp.createLabel(topic));
// the id of each listbox is the content of its corresponding label
var id = ObjApp.camelString(topic)
var lboxActions = myApp.createListBox().setId(id);
//add items to listbox
lboxActions.addItem('Select');
lboxActions.addItem('Add to agenda');
lboxActions.addItem('Move to another meeting');
lboxActions.addItem('Move to a special meetin');
lboxActions.addItem('Move to email');
//add drop down list to grid
myGrid.setWidget(i, 1, lboxActions);
}
I have 3 questions:
1) Which is a better design?
a) Design 1: a save button next to each listbox.
b) Design 2: one submit button at the bottom to save every entry
2) How would I collect information on what the user select? How would I write such handlers? I added the following code for each design but I don't think I'm doing it right.
a) Design 1: the following lines of code were added to the for loop described above
var buttonSave = myApp.createButton('Save');
myGrid.setWidget(i, 2, buttonSave);
var handlerSelection = myApp.createServerHandler('selectAction');
handlerSelection.addCallbackElement(mainPanel);
buttonSave.addClickHandler(handlerSelection);
b) Design 2: the following lines of code were added outside the for loop
//update spreadsheet when user click "submit"
var handlerUpdate = myApp.createServerHandler('responseToSubmit');
handlerUpdate.addCallbackElement(mainPanel);
buttonSubmit.addClickHandler(handlerUpdate);
mainPanel.add(myGrid);
mainPanel.add(buttonSubmit);
myApp.add(mainPanel);
3) How do I write functions for the handlers? These are not correct because I wasn't able to extract the information from the list boxes.
a) Design 1
function responseToSave(e) {
var name = e.parameter.source;
var selection = e.parameter.name;
var selectionObj = new Object();
selectionObj['status'] = selection;
selectionObj['name'] = name;
choicesMade.push(selectionObj)
Logger.log(choicesMade);
return choicesMade;
}
b) Design 2
function responseToSubmit(e) {
var myApp = UiApp.getActiveApplication();
for (i=0; i < labels.length; i++) {
var lboxId = ObjApp.camelString(labels[i]['topic']);
//[EDIT] e.parameter.lboxId would not work because lboxId is a string
var selection = e.parameter[lboxId];
choicesMade[labels[i]] = selection;
}
Logger.log(choicesMade);
return choicesMade;
}
Thanks
Q1:
The design purely depends on what your application is intended to do and how your users use it. There is no 'better' design - each of them has its own pros and cons and the choice would be based on how your app is used.
However, do also consider Design 3 which is saving the changes when the dropdown box is changed. It will be one click less for the user
Q2 and Q3:
You should use the setName on the listBox
var lboxActions = myApp.createListBox().setId(id).setName(id);
I generally use the same string for the id and the name to avoid confusion, but you should use setName
After that you can access the item selected in the handler function as
e.parameter.id
Finally, on buttonSave, you should use the addClickHandler instead of addChangeHandler.