In Slate foundry, I would modify parameter of a Dropdown programmatically.
Change the display value or disable value. But it doesn't work.
My code
const select ={{w_selectEndMonthYear}}
select.selectedValue = "202201"
select.selectedDisplayValue = "January 2022"
select.disabled = true
console.log({{w_selectEndMonthYear}})
return {{w_selectEndMonthYear.selectedDisplayValue}}
in my console, I have
{selectedDisplayValue: 'January 2022', selectedValue: '202201', disabled: 'true'}
It seems good, but neither the return nor the display of the widget changed
Thanks for your help
You're on the right track - to template the selected value of a dropdown list programmatically you need to change to the </> tab of the dropdown widget configuration and use Handlebars to provide a valid default selection for both the the selectedValue and selectedDisplayValue parameter.
Note that you cannot set widget properties from inside a Slate Function. You need to return the value (or a json object with multiple values to reference) and then use a Handlebar statement in the widget configuration to template the appropriate values into the widget configuration.
You can read a bit about this in the context of resetting widget selection state to default values in this section of the documentation.
You can also search your Foundry instance for some helpful Slate tutorials - find the Foundry Training and Resources project and navigate to the Reference Examples/App Building In Slate/1. Tutorials folder to find a series of interactive Slate apps demonstrating the many patterns for using Handlebars, Functions, and other Slate components.
The easiest way i via Functions, also to deliver the the content of dropdown.
Just a hint, to sort the content you need 'transformColumnSchemaToRowSchema' and 'transformRowSchemaToColumnSchema'
Here is a Slate JavaScript snippet for enabling/disabling a button:
Similar you could do this for drop-downs i think.
// Collect Inputs for Validation
// -----------------------------
var inputs = {
UserID: _.trim(_.toUpper({{w_pD_AdmUserAdd_UserID.text}})),
Foundry: _.trim({{w_pD_AdmUserAdd_Foundry.placeholder}}),
NameFirst: _.trim(_.toUpper({{w_pD_AdmUserAdd_NameFirst.text}})),
NameLast: _.trim(_.toUpper({{w_pD_AdmUserAdd_NameLast.text}})),
Email: _.trim(_.toUpper({{w_pD_AdmUserAdd_Email.text}})),
Team: {{w_pD_AdmUserAdd_Team.selectedValue}},
Company: {{w_pD_AdmUserAdd_Company.selectedValue}}
}
// Initialize Variables
// --------------------
var disable = false;
var messages = [];
// Implement Form Validation Checks
// --------------------------------
// Check if all the required fields have a value
if (!(inputs.UserID &&
inputs.Foundry &&
inputs.NameFirst &&
inputs.NameLast &&
inputs.Email &&
inputs.Team &&
inputs.Company
)){
disable = true;
messages.push("Please complete all required fields.");
}
var email_regex = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (inputs.Email && !email_regex.test(inputs.Email)){
messages.push('Please enter a valid email for "Email"')
disable = true;
}
var data_users = {{s_buyers.data}}
var index_email = data_users.str_email.indexOf(inputs.Email)
if (inputs.Email && index_email !== -1){
disable = true;
messages.push('This Email already exist')
}
var index_userid = data_users.str_buyer_id.indexOf(inputs.UserID)
if (inputs.UserID && index_userid !== -1){
disable = true;
messages.push('This User ID already exist')
}
return {
inputs,
disable,
messages
}
Related
Starting a new question to extend Dynamically Add Widget on user click in Gmail Add-on using CardService Appscript (GAS)
I was able to get this to work on a fairly complex UI card I am designing.
There are two things that I can't seem to control:
The selector after OnChangeAction reverts to the default item, so the user looses the context of their selection. Is there a way to persist the selection when rebuilding the card?
There is no way to setup that the value of the default selected item to force an OnChangeAction event to push its value to the UI on the cards initial load.
Anyone able to use this method to accomplish the above?
I came up with a hack for this that seems to work.
I added a default item to the top of the item list with a label of item0
.addItem("Click here to select an existing item to edit","item0", true)
I also created a cache to cache this label. CacheService.getUserCache().put('itemSelected','item0')
When the item is selected from the dropdown I update the Cache with that items value, e.g. item(n).
I then set up two Widgets within a single function itemManagerCard(e, item) that takes the item I want to change as a parameter.
WidgetOne is the base state of the card where the parameters are undefined
if(item === undefined)
WidgetTwo responds when onModeChange(e) returns the target card with the item parameters I want to change
return itemManagerCard(e, item);
if(item != undefined)
I can then populate WidgetTwo with whatever UI data I want to collect through the function's parameters. Such as:
var editItemNumText = 'You are currently editing ${itemNum}'
function itemManagerCard(e, item) {
var selectItemBodyWidget = CardService.newSelectionInput()
.setType(CardService.SelectionInputType.DROPDOWN)
.setTitle('Which item do you want to edit?')
.setFieldName('editItem');
.setOnChangeAction(CardService.newAction().setFunctionName('onModeChange'))
.addItem("Click here to select an existing Item to edit","item0", true)
.addItem("Item 1","item1", false);
//
if (item != undefined)
let itemNum = CacheService.getUserCache().get('itemNum');
var itemText = `You are currently editing ${itemNum}`;
var itemWidget = CardService.newTextParagraph()
.setText(itemText);
}
function onModeChange(e) {
let itemNum = (e.formInput.editItem);
CacheService.getUserCache().put('itemNum', itemNum);
var item = itemArr[itemNum]
return itemManagerCard(e, item);
}
Major drawback is needing to build and maintain the two separate virtually identical Widgets, which can get messy the more complex it gets.
I have a very basic Google Workspace Add-on that uses the CalendarApp class to toggle the visabilty of a calendar’s events when a button is pressed, using the setSelected() method
The visabilty toggling works, but the change in only reflected in the UI when the page is refreshed. Toggling the checkbox manually in the UI reflects the change immediately without needing to refresh the page.
Is there a method to replicate this immediate update behaviour via my Workspace Add-On?
A mwe is below.
function onDefaultHomePageOpen() {
// create button
var action = CardService.newAction().setFunctionName('toggleCalVis')
var button = CardService.newTextButton()
.setText("TOGGLE CAL VIS")
.setOnClickAction(action)
.setTextButtonStyle(CardService.TextButtonStyle.FILLED)
var buttonSet = CardService.newButtonSet().addButton(button)
// create CardSection
var section = CardService.newCardSection()
.addWidget(buttonSet)
// create card
var card = CardService.newCardBuilder().addSection(section)
// call CardBuilder.call() and return card
return card.build()
}
function toggleCalVis() {
// fetch calendar with UI name "foo"
var calendarName = "foo"
var calendarsByName = CalendarApp.getCalendarsByName(calendarName)
var namedCalendar = calendarsByName[0]
// Toggle calendar visabilty in the UI
if (namedCalendar.isSelected()) {
namedCalendar.setSelected(false)
}
else {
namedCalendar.setSelected(true)
}
}
In short: Create a chrome extension
(2021-sep-2)Reason: The setSelected() method changes ONLY the data on server. To apply the effect of it, you need to refresh the page. But Google Workspace Extension "for security reason" does not allow GAS to do that. However in an Chrome Extension you can unselect the checkbox of visibility by plain JS. (the class name of the left list is encoded but stable for me.) I have some code for Chrome Extension to select the nodes although I didn't worked it out(see last part).
(2021-jul-25)Worse case: Default calendars won't be selected by getAllCalendars(). I just tried the same thing as you mentioned, and the outcome is worse. I wanted to hide all calendars, and I am still pretty sure the code is correct, since I can see the calendar names in the console.
const allCals = CalendarApp.getAllCalendars()
allCals.forEach(cal => {console.log(`unselected ${cal.setSelected(false).getName()}`)})
Yet, the principle calendar, reminder calendar, and task calendar are not in the console.
And google apps script dev should ask themselves: WHY DO PEOPLE USE Calendar.setSelected()? We don't want to hide the calendar on the next run.
In the official document, none of these two behaviour is mentioned.
TL;DR part (My reason for not using GAS)
GAS(google-apps-script) has less functionality. For what I see, google is trying to build their own eco-system, but everything achievable in GAS is also available via javascript. I can even use typescript and do whatever I want by creating an extension.
GAS is NOT easy to learn. The learning was also painful, I spent 4 hours to build the first sample card, and I can interact correctly with the opened event after 9 hours. The documentation is far from finished.
GAS is poorly supported. The native web-based code editor (https://script.google.com/) is not build for coding real apps, it loses the version control freedom in new interface. And does not support cross-file search. Instead of import, codes run from top to bottom in the list, which you need to find that by yourself. (pass along no extension, no prettier, I can tolerate these)
In comparison with other online JS code editors, like codepen / code sandbox / etcetera it does so less function. Moreover, VSCode also has a online version now(github codespaces).
I hope my 13 hours in GAS are not totally wasted. As least whoever read this can just avoid suffering the same painful test.
Here's the code(typescript) for disable all the checks in Chrome.
TRACKER_CAL_ID_ENCODED is the calendar ID of which I don't want to uncheck. Since it is not the major part of this question, it is not very carefully commented.
(line update: 2022-jan-31) Aware that the mutationsList.length >= 3 is not accurate, I cannot see how mutationsList.length works.
Extension:
getSelectCalendarNode()
.then(unSelectCalendars)
function getSelectCalendarNode() {
return new Promise((resolve) => {
document.onreadystatechange = function () {
if (document.readyState == "complete") {
const leftSidebarNode = document.querySelector(
"div.QQYuzf[jsname=QA0Szd]"
)!;
new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.target) {
let _selectCalendarNode = document.querySelector("#dws12b.R16x0");
// customized calendars will start loading on 3th+ step, hence 3, but when will they stop loading? I didn't work this out
if (mutationsList.length >= 3) {
// The current best workaround I saw is setTimeout after loading event... There's no event of loading complete.
setTimeout(() => {
observer.disconnect();
resolve(_selectCalendarNode);
}, 1000);
}
}
}
}).observe(leftSidebarNode, { childList: true, subtree: true });
}
};
});
}
function unSelectCalendars(selectCalendarNode: unknown) {
const selcar = selectCalendarNode as HTMLDivElement;
const calwrappers = selcar.firstChild!.childNodes; // .XXcuqd
for (const calrow of calwrappers) {
const calLabel = calrow.firstChild!.firstChild as HTMLLabelElement;
const calSelectWrap = calLabel.firstChild!;
const calSelcted =
(calSelectWrap.firstChild!.firstChild! as HTMLDivElement).getAttribute(
"aria-checked"
) == "true"
? true
: false;
// const calNameSpan = calSelectWrap.nextSibling!
// .firstChild! as HTMLSpanElement;
// const calName = calNameSpan.innerText;
const encodedCalID = calLabel.getAttribute("data-id")!; // const decodedCalID = atob(encodedCalID);
if ((encodedCalID === TRACKER_CAL_ID_ENCODED) !== calSelcted) {
//XOR
calLabel.click();
}
}
console.log(selectCalendarNode);
return;
}
There is no way to make a webpage refresh with Google Apps Script
Possible workarounds:
From the sidebar, provide users a link that redirects them to the Calendar UI webpage (thus a new, refreshed version of it will be opened)
Install a Goole Chrome extension that refreshes the tab in specified intervals
I would like to add a server script that replaces special characters when a textbox is filled. The info for the "onValueEdit" routine states:
This script will run on the client whenever the value of this widget is edited by the user. The widget can be referenced using parameter widget and the new value of the widget is stored in newValue. Unlike onValueChange(), this runs only when a user changes the value of the widget; it won't run in response to bindings or when the value is set programmatically.
Therefore I have built the following server script that should take the text from the textbox, overwrite the special characters and replace the text in the textbox. But when I add the script to "onValueEdit" event, Google App Maker returns "function is undefined".
function cleanup(input, output) {
if (input !== null) {
output = input.trim();
output = output.replace('ß','ss');
output = output.replace('ä','ae');
output = output.replace('ö','oe');
output = output.replace('ü','ue');
return output;
}
}
In case you want to make this changes only on client side the ritgh way to do it will be adding this code to the onValueEdit event handler:
// onValueEdit input's event handler
if (newValue !== null) {
output = newValue.trim();
output = output.replace('ß','ss');
...
widget.value = output;
}
If you need to securely enforce this override prior to persisting to database, then you need to go with Model Events:
// onBeforeCreate and onBeforeSave events
if (record.FieldToChange !== null) {
record.FieldToChange = record.FieldToChange.trim();
record.FieldToChange = record.FieldToChange.replace('ß','ss');
...
}
With this approach you don't need any client code since all changes made on server should automatically sync back to client.
Below is old; look at the updated text at the bottom.
So my friends and I use google docs to communicate while in school, and we setup the challenge to create a working and "efficient" chat bar to communicate with better results. I've been around JavaScript for quite some time, but have never fooled around with Google Apps Script before. We are using the document app for our chats; the code I came up with is as below, but I have a few problems with it:
Errors when a user closes it, then goes to Chat -> Open Chat in the toolbar to re-open, saying, "Error encountered: An unexpected error occurred"; does not specify a line or reason
Needs a hidden element somewhere in the document which can allow users to see what others have typed, but that they can't edit without using the chat box (would add event listener to update chat box when text is ammended)
//Main function, ran when the document first opens.
function onOpen() {
var app = UiApp.createApplication(); //Create a Ui App to use for the chat bar
if(getCurrentUser()=="dev1"||getCurrentUser()=="dev2"){ //user-Id's hidden for privacy
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addItem('Elements', 'displayElements') //Hidden as it is not important for regular use
.addItem('MyID', 'showUser')
.addToUi();
}else{
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addToUi();
}
}
//Creates and returns the chats GUI
function createChatBox(){
var app = UiApp.getActiveApplication()
app.setTitle("Chat Bar (not yet working)");
var vPanel = app.createVerticalPanel().setId('chatPanel').setWidth('100%');
var textArea = app.createTextArea().setId('chatBox').setName('chatBox').setReadOnly(true).setText('').setSize('250px', '450px'); //Read only so they can not edit the text, even if it won't affect overall chat
var textBox = app.createTextBox().setId('messageBox').setName('messageBox').setText('Words');
var chatHandler = app.createServerHandler("sayChat").addCallbackElement(textArea).addCallbackElement(textBox);
var chatButton = app.createButton().setId("sayButton").setText("Say!").addMouseUpHandler(chatHandler);
vPanel.add(textArea);
vPanel.add(textBox);
vPanel.add(chatButton);
app.add(vPanel);
DocumentApp.getUi().showSidebar(app);
return app;
}
//The event handler for when the "Say!" (post) button is pressed. Is probably where the conflict stems from.
function sayChat(eventInfo){
var app = UiApp.getActiveApplication();
var parameter = eventInfo.parameter;
app.getElementById("chatBox").setText(parameter.chatBox+"["+getCurrentUser()+"]: "+parameter.messageBox);
app.getElementById("messageBox").setText("");
return app;
}
//A debug function and a function to tell you the unique part of your email (useless, really)
function showUser(){
DocumentApp.getUi().alert("Your userId is: "+getCurrentUser());
}
//Returns the unique part of a person's email; if their email is "magicuser#gmail.com", it returns "magicuser"
function getCurrentUser(){
var email = Session.getActiveUser().getEmail();
return email.substring(0,email.indexOf("#"));
}
//The Auto-color and displayElements methods are hidden as they contain other user-info. They both work as intended and are not part of the issue.
I do not need someone to rewrite the code (although that'd be greatly appreciated!), but instead point out what I'm doing wrong or suggest something to change/add.
Last, before you suggest it, the google docs chat does not work with our computers. It is not the fault of the document, but probably a compatability error with our browser. It is because of this issue that we are going through this fun yet hasty process of making our own chat method.
Update
I decided to give up on my version of the chat using pure Google Apps Script and help improve my friends version using both G-A-S and HTML. I added image thumbnail/linking support with command /img or /image, along with improved time and counter, and some other behind the scenes updates. Here is a quick screenshot of it:
Magnificent chat programmed from scratch, and no buggy update methods, just a casual refresh database to check for messages and set HTML text-area text. No more buggy getText methods. For each new message in the database, whether targeted toward the user or toward everyone in the chat, we load all the database messages up to a limit (50 messages at a time), then display them. The use of HTML in the messages is key to its appearence and features, such as images.
function getChat() {
var chat = "";
var time = getTime();
var username = getCurrentUsername();
var db = ScriptDb.getMyDb();
var query = db.query({time : db.greaterThan(getJoinTime())}).sortBy('time', db.DESCENDING).limit(50);
var flag = query.getSize() % 2 != 0;
while(query.hasNext()) {
var record = query.next();
if(record.showTo == "all" || record.showTo == getCurrentUsername()) {
var text = record.text;
for(var i = 0; i < text.split(" ").length; i++) {
var substr = text.split(" ")[i];
if(substr.indexOf("http://") == 0 || substr.indexOf("https://") == 0) {
text = text.replace(substr, "<a href='" + substr + "'>" + substr + "</a>");
}
}
var message = "<pre style='display:inline;'><span class='" + (flag? "even" : "odd") + "'><b>[" + record.realTime + "]</b>" + text;
message += "</span></pre>";
chat += message;
flag = !flag;
}
}
//DocumentApp.getUi().alert(getTime() - time);
return chat;
}
I am going to re-do his getChat() method to only check for new messages, and not load every message at each refresh.
First thing to to to get rid of your error message is to create the UiApp in the createChat function instead of onOpen.
I also used a client handler to clear the textBox because it's just more efficient. Here is the modified code :
code removed see updates below
As for your second request I'm not sure I understand exactly what you want to do... could you explain more precisely the behavior you expect ? (this is more a comment than an answer but I used the "answer field" to be more readable)
EDIT : I played a little with this code and came to something that -almost- works... it still needs to be improved but it's worth showing how it works.
I used scriptProperties to store the common part of the conversation, I think that's a good approach but the issue it to know when to update its content. Here is the code I have so far, I keep being open to any suggestion/improvement of course.
code removed, new version below
EDIT 2 : here is a version with an auto update that works quite good, the script updates the chat area automatically for a certain time... if no activity then it stops and wait for a user action. please test (using 2 accounts) and let us know what you think.
note I used a checkBox to handler the autoUpdate, I keep it visible for test purpose but of course it could be hidden in a final version.
EDIT 3 : added a message to warn the user when he's been put offline + changed textBox to colored textArea to allow for longer messages + condition to clear the messageBox so that the warning message doesn't go in the conversation. (set the time out to a very short value for test purpose, change the counter value to restore to your needs)
function onOpen() {
if(getCurrentUser()=="dev1"||getCurrentUser()=="dev2"){ //user-Id's hidden for privacy
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addItem('Elements', 'displayElements') //Hidden as it is not important for regular use
.addItem('MyID', 'showUser')
.addToUi();
}else{
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addToUi();
}
}
function createChatBox(){
ScriptProperties.setProperty('chatContent','');
var app = UiApp.createApplication().setWidth(252);
app.setTitle("Chat Bar");
var vPanel = app.createVerticalPanel().setId('chatPanel').setWidth('100%');
var chatHandler = app.createServerHandler("sayChat").addCallbackElement(vPanel);
var textArea = app.createTextArea().setId('chatBox').setName('chatBox').setReadOnly(true).setText('').setSize('250px', '450px');
var textBox = app.createTextArea().setId('messageBox').setName('messageBox').setText('Start chat...').setPixelSize(250,100).setStyleAttributes({'padding':'5px','background':'#ffffcc'}).addKeyPressHandler(chatHandler);
var clearTextBoxClientHandler = app.createClientHandler().forTargets(textBox).setText('');
textBox.addClickHandler(clearTextBoxClientHandler);
var chatButton = app.createButton().setId("sayButton").setText("Say!").addMouseUpHandler(chatHandler);
var chkHandler = app.createServerHandler('autoUpdate').addCallbackElement(vPanel);
var chk = app.createCheckBox().setId('chk').addValueChangeHandler(chkHandler);
vPanel.add(textArea);
vPanel.add(textBox);
vPanel.add(chatButton);
vPanel.add(chk);
app.add(vPanel);
DocumentApp.getUi().showSidebar(app);
return app;
}
function sayChat(e){
var app = UiApp.getActiveApplication();
var user = '['+getCurrentUser()+'] : ';
if(e.parameter.messageBox=="You have been put offline because you didn't type anything for more than 5 minutes..., please click here to refresh the conversation"){
app.getElementById('messageBox').setText('');// clear messageBox
ScriptProperties.setProperty('chatTimer',0);// reset counter
return app;
}
if(e.parameter.source=='messageBox'&&e.parameter.keyCode!=13){return app};
var content = ScriptProperties.getProperty('chatContent');
ScriptProperties.setProperty('chatContent',content+"\n"+user+e.parameter.messageBox)
app.getElementById("chatBox").setText(content+"\n"+user+e.parameter.messageBox+'\n');
app.getElementById('messageBox').setText('');
app.getElementById('chk').setValue(true,true);
ScriptProperties.setProperty('chatTimer',0);
return app;
}
function autoUpdate(){
var app = UiApp.getActiveApplication();
var content = ScriptProperties.getProperty('chatContent');
var counter = Number(ScriptProperties.getProperty('chatTimer'));
++counter;
if(counter>20){
app.getElementById('chk').setValue(false);
app.getElementById('messageBox').setText("You have been put offline because you didn't type anything for more than 5 minutes..., please click here to refresh the conversation");
return app;
}
ScriptProperties.setProperty('chatTimer',counter);
var content = ScriptProperties.getProperty('chatContent');
app.getElementById("chatBox").setText(content+'*'); // the * is there only for test purpose
app.getElementById('chk').setValue(false);
Utilities.sleep(750);
app.getElementById('chk').setValue(true,true).setText('timer = '+counter);
return app;
}
function showUser(){
DocumentApp.getUi().alert("Your userId is: "+getCurrentUser());
}
function getCurrentUser(){
var email = Session.getEffectiveUser().getEmail();
return email.substring(0,email.indexOf("#"));
}
I am building an UI form via code (not using the UI Builder) and I noticed that the SubmitButton class style is not consistent with the Button class look & feel.
Would you know any way to adjust the look & feel of either the Button class or the SubmitButton class to make them similar.
I noticed that the Button has a call setStylePrimaryName, setStyleName etc... but the documentation is vague - says: "This is useful for debugging"!!!
Any suggestion?
See below screenshoot, first button is of class Button, second button is SubmitButton. You can see they don't even align.
You can style (a button) the way you want with setStyleAttribute
var _btn= {
"background-color":"none",
"background":"none",
"width":"80px",
"height":"24px",
"border":"None",
"font-family":"hobo std",
"font-size":"0.9em",
"color":"3f3f3f",
"opacity":"1",
}
....
....
var closeb = app.createButton("Submit");
library.applyCSS(submit,_btn);
....
....
And in your library you have the function (credits to James Fereira)
function applyCSS(element, style){
for (var key in style){
element.setStyleAttribute(key, style[key]);
}
}
I resolved this cosmetic dilemma by using multiple submit buttons in the same form. I experimented with CSS sans success; the Submit & Reset buttons are two unique beasts in the world of button widgets.
Here is some working code
that demonstrates a multiple page form where each page uses three submitButton's to advance back and forth doing multiple doPost()'s.
// 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.