handler design for dynamically created listboxes - google-apps-script

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.

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

How to set the go to sections on a Google Forms question using app script

I have a Google form where some of the fields are "static" content, and some are populated by app script from a Google sheet which will be run on a daily basis. I nearly have it how I want it, but not quite.
I have a couple of dropdown lists that need to contain choices that navigate to different sections of the form, e.g:
Phill / Beacon Hill - Go to section called "Beacon Hill
Jane / Harbord - Go to section called "Harbord"
Fred / Beacon Hill - Go to section called "Beacon Hill"
etc...
What's happening is that instead of appending choices to the list, I'm overwriting which means I only end up with ONE choice in my dropdown, which is the last one added, i.e. Fred in the example above.
I've looked at lots of examples on the web but can't get this to work. I feel I'm very close but not quite there yet. Here's my code with a couple of lines that I believe are the problem. Can someone please tell me where I'm going wrong.
function populatePlayersClubsListV2() {
// ------------------------------------------------------------------------------------------------
// This gets each male player and the junior club they've been assigned to from the
// Junior_Clubs_Training_Sessions s/sheet (which is this s/sheet). It creates a list that the player
// chooses their name from and sets up a branch to the appropriate Club training sessions section
// in the form depending on which club they're assigned to. .
// ------------------------------------------------------------------------------------------------
// Open the "Find Junior Club Training Sessions" form
var form = FormApp.openById("1fo33ncgJY.................iRnMsERRTen8WjTH_xrx0");
// Identify the sheet in this spreadsheet holding the data needed to populate the drop-down. Here
// it's the "PlayersClubs" tab which says which club each player is assigned to.
var ss = SpreadsheetApp.getActive();
var playersClubsSheet = ss.getSheetByName("PlayersClubs");
// Grab the values from the rows & columns of the sheet - use 2 to skip header row. Use getMaxRows
// and getMaxColumns to avoid hard-coding the number of rows and columns.
var playersClubsSsValues = playersClubsSheet.getRange(2, 1, playersClubsSheet.getMaxRows() - 1, playersClubsSheet.getMaxColumns() - 1).getValues();
// We need to loop thro twice - once to populate the male players, and again for the female players.
// Males/females populate different fields and we hold the data-item-IDs of those fields in an array.
var formFieldsArray = [
["Male", 1397567108],
["Female", 1441402031]
];
for(var h = 0; h < formFieldsArray.length; h++) {
// Open the form field you want to populate - it must be a dropdown or multiple choice.
// Right-click field, inspect and look for data-item-ID followed by a number.
var playersClubsFormList = form.getItemById(formFieldsArray[h][1]).asListItem();
// Define array to hold values coming from the s/sheet and used to populate form fields.
var playersClubsArray = [];
var sectionMalePlayers = form.getItemById(309334479).asPageBreakItem();
var sectionFemalePlayers = form.getItemById(856495273).asPageBreakItem();
// Create the array of players and their clubs ignoring empty cells. Check if the s/sheet row
// matches male/female against formFieldsArray[h][0].
for(var i = 0, j = 0; i < playersClubsSsValues.length; i++) {
if(playersClubsSsValues[i][0] != "" && playersClubsSsValues[i][1] == formFieldsArray[h][0]) {
playersClubsArray[j] = playersClubsSsValues[i][0] + " - " + playersClubsSsValues[i][2];
if (formFieldsArray[h][0] = "Male") {
// ** THIS IS THE LINE THAT OVERWRITES BUT I NEED IT TO APPEND *** //
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
}
else {
// ** THIS IS THE LINE THAT OVERWRITES BUT I NEED IT TO APPEND *** //
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionFemalePlayers)]);
}
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
j = j + 1;
} // end if
} // end for loop
} // end for loop for Males/Females
}
Issue:
When setChoices is used, all choices that were previously stored in the item get removed. Only the ones that are specified when using setChoices get added to the item.
Right now, you are only specifying one choice:
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
Solution:
You have to specify all choices you want the item to have. So in this case you would have to do the following:
Retrieve all choices that were previously stored via getChoices. This will retrieve an array with all current choices in the item.
Use Array.prototype.push() to add the choice you want to add to the list of choices.
When using setChoices, the array retrieved in step 1 and modified in step 2 should be provided as the argument.
Code sample:
Change this:
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
For this:
var choices = playersClubsFromList.getChoices();
choices.push(playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers));
playersClubsFormList.setChoices(choices);
Note:
The same change would have to be made for all the lines where this problem is happening, like this one:
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionFemalePlayers)]);
Reference:
getChoices()
setChoices(choices)

What would be the behaviour of GmailApp.createLabel when called multiple times with same label name?

I tried the following code.
function addLabel() {
console.log(GmailApp.createLabel('FOO'));
console.log(GmailApp.createLabel('FOO'));
}
After running this function, I see that there is only one label FOO and the threads which are assigned earlier to FOO are retained. And no exception thrown at runtime for 'duplicate label name'.
Is this a valid behaviour? Can it be relied upon? The Official documentation doesn't mention anything like this.
Calling GmailApp.createLabel() with a label name that already exists will return the already existing label. It will not make any changes to your existing label.
According to the documentation, the only way to create or get a label is via the label's name. Importantly, the only identifying property listed in the GmailLabel class is the name. As such, my assumption would be that Apps Script is enforcing uniqueness of names and that it's preventing overwriting of existing labels.
We can try a simple test. If the overwrite protection does not exist, then creating a new label would likely remove the association between label & email. So let's see which emails appear under a certain label, create a new label with the same name, and see if the list of emails is the same.
function test() {
var label1 = GmailApp.getUserLabelByName("test_label"); // Get existing label
var threads = label1.getThreads();
var label1_messages = [];
for (var i in threads) {
var messages = threads[i].getMessages();
for (var j in messages) {
label1_messages.push(messages[j].getId()); // Store the message IDs in label1_messages
}
}
var label2 = GmailApp.createLabel("test_label"); // Create a new label with the same name
var threads = label2.getThreads();
var label2_messages = [];
for (var i in threads) {
var messages = threads[i].getMessages();
for (var j in messages) {
label2_messages.push(messages[j].getId()); // Store the message IDs in label2_messages
}
}
Logger.log(JSON.stringify(label1_messages) == JSON.stringify(label2_messages)); // Quick, non-robust check of the arrays results in TRUE
}
The result is that they are the same, so that confirms the assumption that createLabel() is smart enough to avoid overwriting existing labels.
We can go further, though. The Gmail API clearly indicates that labels have an ID. Here again, I don't see any requirement that label names be unique (although, we can assume it given that end users can only interact with label names–it would be terrible UX if multiple with the same name existed).
If you enable the Gmail API in Advanced Google services, we can test the API requirements. Try creating a new label with the same name of a label that we already know exists.
function createLabel() {
Gmail.Users.Labels.create({name: "test_label"}, "me");
}
That results in the below error, which then confirms that label names must be unique.
API call to gmail.users.labels.create failed with error: Label name
exists or conflicts
Let's go one step further. Initially, I assumed that Apps Script was protecting against overwriting existing labels. So let's check the ID of the existing label, then call GmailApp.createLabel() with the same label name, and see if a new label was created/the label ID changed.
function finalTest() {
var response = Gmail.Users.Labels.list("me"); // Get labels
for (var i in response.labels) {
var label = response.labels[i];
if (label.name == "test_label")
Logger.log(label.id); // ID: Label_48
}
var newLabel = GmailApp.createLabel("test_label"); // Create a new label with the same name
var response = Gmail.Users.Labels.list("me"); // Get labels again to see if any difference
for (var i in response.labels) {
var label = response.labels[i];
if (label.name == "test_label")
Logger.log(label.id); // ID: Label_48
}
}
As you can see, the label ID remains the same, meaning that GmailApp.createLabel() is indeed protecting against overwriting existing labels.

Identify a tab in a tabPanel after selection using GAS

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?

remove selected items from google form dropdown list

I have been trying to get this to work for a couple of days now and I give up.
I want to create a Google form with a drop down list populated form a spreadsheet. I don't what anyone to choose the same as any one else. (like in a potluck situation)
example:
I am giving away :
a comb
a brush
a bowl full of mush
I tell Thomas, Richard and Henry that they can each have one and send them a link to a Google form I created. Tom is quick and opens the form 1st. He enters his name and chooses a comb out of a three item drop down list. Dick opens the form link and in the same drop down question he chooses out of the two remaining items. He chooses the brush. Harry is a bit of a slow poke, so when he gets home he opens my link, but alas, he can only have a bowl full of mush.
How can I get this done?
Based on my research so far I will be needing to use the if function on the responses spread sheet to see if there has been a take for an item (see if the cell is vacant) and maybe VLOOKUP, but I can't get a clear picture of how to make it all work together.
Thank you,
Good night
EDIT:
Based on gssi's answer, I wanted to post the code and describe the way I did it.
function updateListChoices(item){
var inventory = (SpreadsheetApp.openById(theIdOfTheResponceSpreadsheet)
.getSheetByName("inventory")
.getDataRange()
.getValues());
var selected = (SpreadsheetApp.openById("0Al-3LXunCqgodHB5RGNpR0RyQ0pERmVnek1JeUJKS0E")
.getSheetByName("responses")
.getDataRange()
.getValues());
var choices = [];
var selectedReal = [];
for (var i = 0; i< selected.length; i+=1){
selectedReal.push(selected[i][2]) }
for (var i = 1; i< inventory.length; i+=1){
if(selectedReal.indexOf(inventory[i][0])=== -1){
choices.push(item.createChoice(inventory[i][0]));}
}
item.setChoices(choices);
}
var LIST_DATA = [{title:"the title of the question", sheet:"inventory"}]
function updateLists() {
var form = FormApp.getActiveForm();
var items = form.getItems();
for (var i = 0; i < items.length; i += 1){
for (var j = 0; j < LIST_DATA.length; j+=1) {
var item = items[i]
if (item.getIndex() === 1){
updateListChoices(item.asListItem(), "inventory");
break;
}
}
}
}
In the building of the form, click the tools menu, then click script editor. Copy the code from here (with changes to fit your needs) to the script editor and hit save. Click the Resources menu and hit the project triggers (the 1st option). Click Add trigger. Choose updateLists from form do this once with when sending and once when opening (you should end up with 2 lines.)
It isn't very elegant, but this is what I am capable of. Good Luck.
I tried to accomplish exactly the same (list with products to select from), but I couldn't make it work with your final code example. Here's mine, with detailed instructions. Just for anybody who is landing on this page and is looking for a working code.
(Menu names might differ from yours, because I'm using a non-English Google Forms, and I'm just guessing the translations here.)
1) Create a new form, and create a new radio button based question (multiple choice) (In this example, I use the question name: "Select a product"). Don't add any options to it. Save it.
2) Open the spreadsheet where the responses are going to be stored, and add a new sheet in it (name: "inventory")
3) Fix the first row (A) of the inventory sheet, and put in A1: "Select a product"
4) Put in column A all the products you want to appear in the form
5) Open the form editor again, and go to tools > script editor
6) Paste this code in the editor, put in your form and spreadsheet ID's (3x) and save it.
var LIST_DATA = [{title:"Select a product", sheet:"inventory"}];
function updateLists() {
//var form = FormApp.getActiveForm();
var form = FormApp.openById("paste_ID_of_your_FORM_here");
var items = form.getItems();
for (var i = 0; i < items.length; i += 1){
for (var j = 0; j < LIST_DATA.length; j+=1) {
var item = items[i];
if (item.getTitle() === LIST_DATA[0].title){
updateListChoices(item.asMultipleChoiceItem(), LIST_DATA[0].sheet);
break;
}
}
}
}
function updateListChoices(item, sheetName){
var inventory = (SpreadsheetApp.openById("paste_ID_of_your_RESPONSE_SHEET_here")
.getSheetByName("inventory")
.getDataRange()
.getValues());
var selected = (SpreadsheetApp.openById("paste_ID_of_your_RESPONSE_SHEET_here")
.getSheetByName("responses")
.getDataRange()
.getValues());
var choices = [];
var selectedReal = [];
for (var i = 0; i< selected.length; i+=1){
selectedReal.push(selected[i][1])
}
for (var i = 1; i< inventory.length; i+=1){
if(selectedReal.indexOf(inventory[i][0])=== -1){
choices.push(item.createChoice(inventory[i][0]));}
}
if (choices.length < 1) {
var form = FormApp.getActiveForm();
form.setAcceptingResponses(false);
} else {
item.setChoices(choices);
}
}
7) With the code editor open, go to Resources > Create triggers and create these two triggers. They need to appear in this order:
updateLists - from form - sending
updateLists - from form - opening
Now you're good to go. If you open the form editor, the products added in the inventory sheet will appear as options.
Every time a product is chosen, it will disappear from the form. To reset all the chosen products, go to the form editor, and choose Responses > Remove all responses. You might need to remove all responses from the responses sheet manually as well (Don't know why, but that happened to me). After that, you need to manually run the updateLists script in the code editor.
Here's a 'How-To' that describes how to construct the spreadsheet.
Column A : named 'Inventory', contains the names of the items initially available.
Column B : named 'Indices', in cell B1 contains the formula =if(isnumber(match(Inventory,Selected,0)),"",if(row(B1)=1,1,max(offset(Indices,0,0,row(B1)-1,1))+1)). Copy the formula in B1 into all cells below it in column B.
Column C : named 'Selected', contains the names of items currently selected starting in row 1 and continuing down contiguously.
Column D : named 'Available', cell D1 contains the formula =if(isnumber(match(row(D1),Indices,0)),index(Inventory,match(row(D1),Indices,0),1),"") which is then copied into all cells below it in column D.
The Available column will always contain a contiguous list of the 'as-yet-unselected' inventory items.