Keep images when adding choices to Google Forms with Apps script - google-apps-script

I'm using google scripts to add new choices to a checkbox item in a form every time the form gets submitted. Some of the existing checkbox-items have images attached that users see when filling in the form.
My problem is that every time my script gets run, it removes the images from the checkbox-items. Is there a way to keep the images attached to the form, while adding new choices to it?
(part of) my code:
// retrieve form-checkbox object
var item = form.getItems(FormApp.ItemType.CHECKBOX)
.filter(function(item){
return item.getTitle() === 'Top 5 films';
})[0].asCheckboxItem();
//make 2 lists, 'choices' with all choices in the checkbox obj
// 'existingChoicesTitles' with all the titles of the choices.
var choices = item.getChoices();
var existingChoicesTitles = choices.map(function(value){
return value.getValue();
})
//check if obj in list 'values' already exists in array 'choices, if not add to
//'choices'
for (i = 0; i < values.length; i++) {
if (existingChoicesTitles.includes(values[i]) == false){
choices.push(item.createChoice(values[i]));
}
}
//set 'choices' list as new list of choices
item.setChoices(choices);

Unfortunately there is currently no way to do that.
There is an active Feature Request for the ability to add these kind of images with the Apps Script FormApp.
Add Images to Form Items
I would suggest that you go and mark the ☆ on the issue to let Google know that you would like this functionality, and also to subscribe to updates. You might also want to add in your experience and use case in a comment.
In your case it seems that to add an item to the checkbox list, Apps Script regenerates all the checkbox items, and since FormApp doesn't support these types of images, it doesn't include them when they are regenerated.
For your use case there doesn't seem to be a practical workaround apart from simply not using images in this way if they are to be modified with Apps Script.
If you are willing to put in some extra work, you might want to implement the form as a simple web app. Then you would have almost infinite flexibility and far more functionality.
Web App example
This is a very simple example of a Web App that shows an input box and a button to send the info to the 'back-end', which in this example is Code.gs
Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile("index")
}
function receiveInfo(formResponse) {
Logger.log(formResponse)
}
index.html
<!DOCTYPE html>
<html>
<body>
<input type='text' id='input'></input>
<button id=main>Send Info</button>
<script>
function main(){
const input = document.getElementById('input')
const info = input.value
google.script.run.receiveInfo(info)
}
const mainButton = document.getElementById("main")
mainButton.addEventListener('click', () => main())
</script>
</body>
</html>
References
Feature Request
Web apps
Test a web app deployment
Client-to-server communication

Related

How to have One URL which sends to one-of-three Google Forms URLs?

I have a need to get equal populations in each of three surveys. The three surveys are identical except for one change - it contains different pictures.
I would like to distribute a single URL to my survey respondents.
I would like to count the number of previous responses I have, and add one.
I would like to redirect the session to one of three (Google Forms) URLs based upon the calculation
(Responses.Count + 1) MOD 3.
I think I need a Google Apps script to do this?
Here is some pseudocode:
var form0 = FormApp.openByUrl(
'htttps://docs.google.com/forms/d/e/2342f23f1mg/viewform'
);
var form1 = FormApp.openByUrl(
'htttps://docs.google.com/forms/d/e/23422333g/viewform'
);
var form2 = FormApp.openByUrl(
'htttps://docs.google.com/forms/d/e/2342wfeijqeovig/viewform'
);
var form0Responses = form0.getResponses();
var form1Responses = form1.getResponses();
var form2Responses = form2.getResponses();
var whichURL = (
form0Responses.length +
form1Responses.length +
form2Responses.length + 1
) % 3; // modulo three
// var goToForm = switch ( whichURL ) blah blah;
// redirect to goToForm;
// How do I redirect now?
Thanks!
Maybe there's a simpler solution possible but I don't think I know of it :)
The common link that you give out could be a link to a "proxy" page that doesn't contain anything but just redirects users to the correct page. Or it could be a link to the actual page with a necessary form embedded. Let's look at the options.
0) Publish your code as web app
In either case you'll need to have your code published as a web app. In GAS it's way simpler than it sounds, you'll find all the info here: https://developers.google.com/apps-script/guides/web
Make sure you set that Anyone, even anonymous can access the app and it always runs as you (if you choose User accessing the app, they'll have to go through authentication process which is not what you need here).
1) Redirect via GAS web app.
Your code in this case would look like so:
// I changed your function a bit so that it could be easier to work with
// in case the number of your forms changes later on
function getForm() {
var forms = [
{
// I'm using openById instead of openByUrl 'cause I've run into issues with
// the latter
form: FormApp.openById('1')
},
{
form: FormApp.openById('2')
},
{
form: FormApp.openById('3')
}
];
var whichURL = 0;
for (var i in forms) {
forms[i].responses = forms[i].form.getResponses().length;
whichURL += forms[i].responses;
}
whichURL++;
// we're returning the actual URL to which we should redirect the visitors
return forms[whichURL % forms.length].form.getPublishedUrl();
}
// doGet is Google's reserved name for functions that
// take care of http get requests to your web app
function doGet() {
// we're creating an html template from which getForm function is called
// as the template is evaluated, the returned result
// of the function is inserted into it
// window.open function is a client-side function
// that will open the URL passed to it as attribute
return HtmlService.createTemplate('<script>window.open("<?= getForm() ?>", "_top");</script>').evaluate();
}
So, after you've published your app, you'll get the link opening which the doGet function will run — and you're going to be redirected to your form.
The thing here is that the URL that you're getting this way is not rather beautiful, sth like https://script.google.com/macros/s/1234567890abcdefghijklmnopqrstuvwxyz/exec and it will also show a message at the top of the page "The app wasn't developed by Google" during those 1-2 seconds before redirect happens.
2) Embed your form into another webpage
The idea here is different: instead of giving your users a "proxy" link, you'll provide them with a page that'll ask a Google script for a correct form link and will display that form in the page in an iframe.
So, there are a couple of steps:
2.1) Change your doGet function (getForm will stay the same):
function doGet() {
return ContentService.createTextOutput(getForm());
}
In this case doGet will not return an html to render by browser but just a link to your form.
Sidenote: after changing the code you'll need to publish a new version of your code for the changes to take effect.
2.2) Create a Google site at sites.google.com
2.3) Insert an "Embed" block into your page, with the following code:
<script>
function reqListener(response) {
// change height and width as needed
document.body.insertAdjacentHTML('beforeend', '<iframe src="' + response.target.response + '" width="400" height="400"></iframe>');
}
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "INSERT-YOUR-SCRIPT-URL");
oReq.send();
</script>
What it does: javascript code sends an http request to your script, gets the form URL and passes it on into the callback function, reqListener which in turn inserts it into document body within an iframe element.
The good thing is that your URL which will be much more user-friendly (you could use this approach on your own site, too).
As a result, you'll have sth like this:

Run server-side code with google chart select

I have a google visualization table that I'm publishing in a web app.
Background:
I run a script that lists all the documents in a google folder in a spreadsheet. I then push that list into the google table I have published in the web app.
The need:
I want to manage those that same list of documents directly from the web app. I want to be able to to move the document from one folder to another when I select the applicable row on the table.
Two things I have accomplished:
I have a script that will move the document to a specific folder on
the google drive using it's doc id.
I have an event listener on the table so that when you click a row and
then click the delete icon, you get a prompt that asks, "are sure
you want to archive [enter document name]?" When I click ok, I get my
test prompt that says "document archived". When I click no, I get my
test prompt that says "request cancelled". So from this, I know I
have the appropriate code (at least that's how it seems).
What I'm struggling with:
I can't seem to get the codes above to work together. The event listener is providing me the url of the document which I have parsed to give me only the id. This is what I was hoping to use to get the rest of the code to run, but I think because I'm trying to interact with the server-side from the client-side, it's not working. Can anyone help me figure it out? I know that I need to use google.script.run.withSuccessHandler when running a server side script from the client side, but I don't know how it applies to this case the docid I need is being collected on table select. Any help is appreciated and I hope the above makes sense!
// Draw Dashboard
h2dashboard.bind([h2stringFilter, h2typeFilter], [h2chart]);
h2dashboard.draw(h2dataView);
google.visualization.events.addOneTimeListener(h2chart, 'ready', function() {
google.visualization.events.addListener(h2chart.getChart(), 'select', function() {
var selection = h2chart.getChart().getSelection();
var dt = h2chart.getDataTable();
// Get Value of clicked row
if (selection.length) {
var item = selection[0];
var docurl = dt.getValue(item.row, 1);
var docname = dt.getValue(item.row, 0);
var source = dt.getValue(item.row, 3);
// When button is clicked, show confirm box with value
$(document).ready(function() {
$("#hu2archive").on("click", function() {
var answer = confirm("Are you sure you want to archive " + docname + "?");
if (answer === true) {
var archive = DriveApp.getFolderById("FOLDER ID");
var docid = docurl.match(/[-\w]{25,}/); // This is where I'm grabbing the value from the row.
var doc = DriveApp.getFileById(docid);
doc.makeCopy(archive).setName(doc.getName());
source.removeFile(doc);
alert(docname + " has been archived!");
} else {
alert("Request cancelled");
}
});
});
}
});
});
I just got it! What I was having a hard time understanding was how to pass a variable from the client side to code.gs. I have only run a script in code.gs from the client side on button submit but never passed anything back.
So I ended up changing my code to the below which passes the variable I need into a successhandler where archiveDoc is the function in my code.gs and docurl is the name of the variable I need to pass from the eventlistener.
if (answer === true) { google.script.run.withSuccessHandler(onSuccess).withFailureHandler(err).archiveDoc(docurl);
I'm still new to coding so I just learned something new! So thanks Spencer Easton. I did in fact answer my own question.

Dynamically fill form fields based on other responses before submit

I have a form I want to dynamically change, I have read through the documents but I cannot seem to find any definitive answer. Can I make my form remove choices from dropdown lists because they used radio button #2 for the 3rd question? Can I format text from question 1 and use it to pre-fill question 6 with the same answer (by default, needs to be changeable)?
Basically I need to use code to determine if the address was spelled with shortforms (st, rd, cres, ct) and lengthen and capitolize them (Street, Road). I don't even know if this is possible. If it is can anyone provide sample code or point me to the right help docs, it would be appreciated. If not is this doable on a webserver if some of my multiple choice options need to be read from a google spreadsheet? Could i do it through google Sites?
Have you looked at the Form Class of Google Apps Services?
Class Forms
It states:
Forms can be accessed or created from FormApp.
For example, you can use:
addTextItem()
OR:
createChoice(value)
Google Documentation
// Open a form by ID and add a new multiple choice item.
var form = FormApp.openById('1234567890abcdefghijklmnopqrstuvwxyz');
var item = form.addMultipleChoiceItem();
item.setTitle('Do you prefer cats or dogs?')
.setChoices([
item.createChoice('Cats'),
item.createChoice('Dogs')
])
.showOtherOption(true);
You don't want to open a new form, but use the currently open one.
/**
* Adds a custom menu to the active form, containing a single menu item for
* invoking checkResponses() specified below.
*/
function onOpen() {
FormApp.getUi()
.createMenu('My Menu')
.addItem('Check responses', 'checkResponses')
.addToUi();
}
Check current responses?
/**
* Gets the list of responses and checks the average rating from the form
* created in createForm() above.
*/
function checkResponses() {
var form = FormApp.getActiveForm();
var responses = form.getResponses();
var score = 0;
for (var i = 0; i < responses.length; i++) {
var itemResponses = responses[i].getItemResponses();
for (var j = 0; j < itemResponses.length; j++) {
var itemResponse = itemResponses[j];
if (itemResponse.getItem().getType() == FormApp.ItemType.SCALE) {
score += itemResponse.getResponse();
}
}
var average = score / responses.length;
FormApp.getUi().alert('The score is ' + average);
}
}
You could use Apps Script HTML Service; write HTML to create a custom form; write the JavaScript code to do what you want; then add the Apps Script to the Google Site. Or just run the Apps Script HTML Service as a website on it's own. But this option requires you to be able to write HTML and JavaScript. What you want to do is possible.
As far as creating the custom form, checking the user input and changing it, that can be done in Apps Script HTML Service. Then you need to save the data somewhere. Google Forms is made to be user friendly to people who don't have programming knowledge.
It seems like you are just looking for general information on what is possible and not possible with different products so that you can make a choice.
For reading about HTML Service, click the following link as a place to start:
Google Documentation HTML Service

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

Displaying a GoogleScript UiApp with the htmlService / html template

I have a UiApp that creates some form elements that write data to a specific spreadsheet. Now I want to load the UiApp widgets into an htmlService template, and I'm not sure how to go about this.
My doGet function loads the base template, and the doGetApp builds the UiApp:
function doGet() {
return HtmlService.createTemplateFromFile('base_template').evaluate();
}
function doGetApp() {
// creates input forms for a spreadsheet (the following is just an example
var app = UiApp.createApplication();
var tabPanel = app.createDecoratedTabPanel();
var flowPanel1 = app.createFlowPanel();
var flowPanel2 = app.createFlowPanel();
tabPanel.add(flowPanel1, "Create New Projects");
tabPanel.add(flowPanel2, "Edit Projects");
app.add(tabPanel);
// return app; // <<<< original doGet function returned the app
// testing different ways of returning the UiApp
return HtmlService.createHtmlOutput(app); // this displays the text "HtmlOutput"
}
My base_html template is very simple right now:
<html >
<?!= getCSS("css_js"); ?>
<body>
<h1>Template Test</h1>
<?!= doGetApp(); ?>
</body>
</html>
I've tried a few variations on this, but the results are generally the same: Text output that appears to describe the object, as opposed to displaying the UiApp. (the getCSS function works fine)
The goal is to be able to easily load some of my own css / js as well as the UiApp in order to easily style the forms / resulting content. I'm pretty new to this, so it is entirely possible that I'm approaching this the wrong way. Appreciate any pointers. Thanks!
-greg
For now, you either use html service or uiapp - you cannot combine the two