Displaying a GoogleScript UiApp with the htmlService / html template - google-apps-script

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

Related

Apps Script - Use a Code.gs variable in HTML script

Here, the task is to use a variable from Code.gs to be used in the HTML side.
The best idea I've had is using google.script.run to get access to Code.gs where I have stored a variable that I wish to use in the HTML script. Eg: Suppose there is a variable in the Code.gs side that turned out to be 1+1. Then I would very much have liked the following to work:
Code.gs
function getMyGSValue() {
return 1+1
}
HTML
<script>
google.script.run.withSuccessHandler(myGsValue => {
myScriptVar = myGsValue
}).getMyGsValue()
// Here use MyScriptVar endlessly.
</script>
Which unfortunately fails to work. If it's of any help, I'm more interested in using string variables from the Code.gs side as these will be more likely the link to the images I want to display if particular conditions are met.
A related question follows:
Passing variable from Code.gs to html in Google App Script
But to be honest, It seemed to have its focus elsewhere.
Doing it with google.script.run. I just used window.onload event to get the data from the server
html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<input type="text" id="txt1" />
<script>
window.onload = () =>{
//console.log("window.onload")
google.script.run.withSuccessHandler((v) => {
document.getElementById("txt1").value = v;
}).getMyGsValue()
//console.log("Code");
}
</script>
</body>
</html>
gs:
function getMyGsValue() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0");
return sh.getRange("A1").getValue();
}
function launchmydialog() {
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutputFromFile("ah2"),"Title");
}
If you need that the "variable" from the server side be ready when the web browser start parsing the web application, instead of using client-side code to retrieve the "variable", generate the client-side code with the "variable" that you need by generating first a HtmlTemplate, then assigns the variable to a HtmlTemplate property and finally evaluates it.
Below is an over simplistic example:
function doGet(e){
const myServerVariable = 1 + 1;
const template = `<body><script>const myClientVariable = <?!= tmpProp?> </script></body>`;
return (
HtmlService.createTemplate(template)
.tmpProp = myServerVariable
).evaluate();
}
The above example is so simple that you could do the same by using regular JavaScript string manipulation. Anyway, <?!= tmpProp?> is a force-printing scriptlet, also there are standard scriptlets and printing scriptlets that might be more frequently used on .html files in Google Apps Script.
Be careful when using scriptlets to built the client-side code to not make them to take too much time to generate the client-side code as this will impact how fast the web app responds to the initial request.
By the other hand, if you want to keep using google.script.run, just declare a global variable, and update it in the withSuccessHandler callback
<script>
var MyScriptVar;
google.script.run.withSuccessHandler(myGsValue => {
MyScriptVar = myGsValue
}).getMyGsValue()
// Here use MyScriptVar endlessly.
</script>
Just consider the case that MyScriptVar will be undefined while the google.script.run finish it's execution.
Related
How to pass a parameter to html?
Passing variable from google script to html dialog
References
https://developers.google.com/apps-script/guides/html/templates

Keep images when adding choices to Google Forms with 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

Auto submit google form after a time limit

I want to use app script in my Google form to automatically submit the form in 20 minutes if the user doesn't click on submit within 20 minutes. Anyway to implement this????
No, you cannot control the client-side of Google Forms, even if you add an Apps Script to it, because Apps Script runs on the server.
One possible solution is to serve your form as a Google Apps Script web app. At that point you can write client-side JavaScript and use window.setTimeout to submit the form after 20 minutes.
Here are some example files, Code.gs and quiz.html, that can provide a basic skeleton to start the web app. A blank project will have Code.gs as the default file, then you have to add File > New > HTML file to start the other file.
You can enter the id of any spreadsheet you own in the commented out lines in Code.gs to append the response into that spreadsheet. (You can also automate that process by creating a new spreadsheet as needed. Example of creating spreadsheet to hold data for Apps Script example can be found here.
// file Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile("quiz");
}
function doPost(request) {
if (request.answer) {
console.log(request.answer); // View > Execution transcript to verify this
//var ss = SpreadsheetApp.openById(id).getSheetByName("Quiz Responses");
//ss.appendRow([request.answer /* additional values comma separated here */ ]);
}
}
<!DOCTYPE html>
<!-- file quiz.html -->
<html>
<head>
<base target="_top">
</head>
<body>
<h1>Quiz</h1>
<form>
What is Lorem Ipsum?
<input name="loremipsum" type="text"/>
<button>Submit</button>
</form>
<script>
const button = document.querySelector("button");
const timeLimitMinutes = 1; // low number for demo; change to 20 for application
const timeLimitMilliseconds = timeLimitMinutes * 60 * 1000;
// For this demo we are not going to serve a response page, so don't try to.
button.addEventListener("submit", submitEvent => submitEvent.preventDefault());
// attach our custom submit to both the button and to the timeout
button.addEventListener("click", submitForm)
window.setTimeout(submitForm, timeLimitMilliseconds)
function submitForm() {
button.setAttribute("disabled", true);
document.querySelector("h1").textContent = "Quiz submitted";
// for demo: submitting just a single answer.
// research Apps Script documentation for rules on submitting forms, certain values not allowed
// consider a helper function `makeForm()` that returns a safe object to submit.
const answer = document.querySelector("input").value;
google.script.run.doPost({ answer });
}
</script>
</body>
</html>
Test with Publish > Deploy as web app...

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:

google apps script - web app doesn't seem to do anything . .

I'm a Java developer but I did a small site for a non-profit group using Google Sites. I have a form I'd like to be somewhat dynamic and Google Apps Script seemed to be a viable option. As frequently happens when one is learning a new technology, I copied and pasted the code below from a tutorial/documentation. I then published it, and inserted the script widget into a page on the site and saved the page. When I reload the page, the "place holder" for the widget is there, but nothing happens - no buttons, no panel, nothing. Same results when I run it from the script editor. I'm sure I'm missing something obvious, but I haven't been able to get the UI to render at all. A little direction would be greatly appreciated.
thanks in advance!
function doGet(e) {
Logger.log("Executing the doGet() method . . .");
var app = UiApp.createApplication();
var aPanelRoot = app.createVerticalPanel();
var button = app.createButton('Click Me');
aPanelRoot.add(button);
var label = app.createLabel('The button was clicked.');
label.setId('statusLabel');
aPanelRoot.add(label);
var handler = app.createServerHandler('myClickHandler');
handler.addCallbackElement(label);
button.addClickHandler(handler);
aPanelRoot.setVisible(true);
return app;
}
function myClickHandler(e) {
var app = UiApp.getActiveApplication();
var label = app.getElementById('statusLabel');
label.setVisible(true);
//app.close();
return app;
}
It seems that you simply forgot to add aPanelRoot to the app in the doGet() function
app.add(aPanelRoot)
also : by default all widgets are visibles so you can remove all the setVisible(true) statements as they are only necessary if you set them to false somewhere else...
And if I may add a last comment, it's generally a good idea to choose the parent widget as callbackElement so you don't risk to forget to add elements when you begin to have lots of them (all children are automatically included in the parent) .