Display variable as html in Google App Script? - google-apps-script

Let's say we have a variable. This variable was created in google app script. On that app script project, you have two files. First, the .gs file, where the variable came from. Next, you have the html file. How do you transfer the variable to html?
GAS:
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function items() {
var exmp = 45;
document.getElementById("test").innerHTML = "You have " + exmp + " items";
HTML:
<script>
google.script.run.items();
</script>
<div id="test"></div>
However, this doesn't work. How can I make this work?

If you read over the Private Functions section of the HTML service documentation, you'll find an example that does almost exactly what you're trying. The code below adapts that example to yours.
You need to keep the GAS server stuff separate from the HTML client stuff. For example, document.getElementById("test").innerHTML = ... means nothing in the context of the server / GAS code. Instead, the modification of the document will be done by Javascript on the client side - in this case, by a success handler.
A success handler is a client-side Javascript callback function that will receive the asynchronous response from your server function items().
Client-side calls to server-side functions are asynchronous: after the
browser requests that the server run the function doSomething(), the
browser continues immediately to the next line of code without waiting
for a response.
This means that there is no waiting for the return code from the call to your server function... the browser just keeps going. You'll see this in this example, as the "More loading..." text gets displayed after the google.script.run call, but before the response is received.
What if items() needs to do something more advanced... like read info from a spreadsheet? Go ahead and change it... just make sure that you return the text you want displayed, and that what you're returning is going to be valid HTML (so the innerHTML operation is OK).
Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function items() {
Utilities.sleep(5000); // Added to allow time to see the div content change
var exmp = 45;
return( "You have " + exmp + " items" );
}
index.html
<div id="test">Loading...</div>
<script type="text/javascript">
function onSuccess(items) {
document.getElementById('test').innerHTML = items;
}
google.script.run.withSuccessHandler(onSuccess).items();
document.getElementById('test').innerHTML = "More loading...";
</script>

You need first to create the HTML using the createHTMLOutput function
In order for you to append strings youo have to use the var.append method
function items(){
var email = Session.getActiveUser().getEmail();
var output = HtmlService.createHtmlOutput(email);
var string1 = HtmlService.createHtmlOutput('<p>You have </p>')
string1.append(output);
string2= HtmlService.createHtmlOutput('<p> items</p>')
string1.append(string2);
Logger.log(string1.getContent());
}
Reference

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

How can I use a Library, which displays drafts and sends email into a Container-Bound Project?

I have 2 Google Apps Script Projects.
SheetsLib - This is a Library which I created, it contains my go-to functions.
TestFile - This is a container-bound script, which utilizes the SheetsLib.
SheetsLib contains the following functions which are relevant for this question:
displayDraftsSelector - Displays the draftSelector Html Page in the Sidebar.
draftSelector - Html file, which contains a js script as well that calls back-end function to populate a <select>
getDraftsArr - Server function which returns all gmail drafts from the user.
The SheetsLib function(s) do work, i.e. I have test functions to confirm that. My goal is to enhance this library, so that I can use it in multiple projects with the functionality to allow a user to choose an existing Gmail Draft and send it to selected users (in the active Spreadsheet).
PROBLEM
In my new container-bound script, which has access to the Library, I can only show the sidebar but not call a back-end function (which resides in the Library) when I press a button in sidebar:
I load the view successfully using displayDraftsSelector() which shows the view draftSelector. This is all functionality from the Library.
Then, the view calls the getDraftsArr() and this is what gets the error. But that function does exist in the Library (and it does work as intended).
The following is the error I see in the console when the sidebar loads:
Uncaught TypeError: google.script.run.withSuccessHandler(...).withFailureHandler(...).getDraftsArr is not a function
What should happen ideally is that, the back-end function getDraftsArr() is called and its result populates the select item. Then the user can select one draft in the sidebar. When the user confirms using a button, the active rows are the recipients. Overall, this all works when I copy-> paste, but what cannot figure out is how to keep the functionality in a library.
The following is the function located in the Library which I am trying to call.
// back-end in Library Project
function getDraftsArr(){
let output = [];
const messages = GmailApp.getDraftMessages();
messages.forEach( message => {
output.push({
id: message.getId(),
subject: message.getSubject()
});
});
return JSON.stringify(output)
}
The following is in the back-end of the library
// front-end in Library Project
<select id="draftsSelect"></select>
<script>
function getDrafts(){
const draftsSelect = document.getElementById("draftsSelect");
google.script.run
.withSuccessHandler( updateDrafts )
.getDraftsArr();
function updateDrafts( drafts ){
var options = "";
var draftsParsed = JSON.parse(drafts);
draftsParsed.forEach( draft => {
options += "<option value='" + draft.id + "'>" + draft.subject + "</option>";
});
draftsSelect.innerHTML = options; }
}
</script>
Thanks to #Rubén for the link to this: stackoverflow.com/q/59271952/1595451
Basically the solution was to create a function in my container-script with the same name as the back-end function which are in the library, which then call the library.
So in my Library I had getDraftsArr(){}before, and now I added the following to my container-bound project:
function getDraftsArr(){
return SheetsLib.getDraftsArr()
}
That did the trick.
If you can create a dummy function. Then the dummy function to create is this one:
function executeLibraryFunctionsByName(funcname) {
libraryname[funcname]();
}
This function will allow you to call all of your library functions by name. I'm not sure that this will work if your libary name has a space in it. In that case change it's name. Note: don't put quotations around the libraryname. Just write the libaryname with no quotes and I would avoid spaces and it becomes the 'this' for your function.
As an example I have a library xyzSUS1 and I can use a button like this:
<input type="button" value="test" onClick="google.script.run.callLibraryFunctionsByName('selectColumnsSkipHeader');" />
in my sidebar to run the library function xyzSUS1.selectColumnsSkipHeader() .
the command in function declaration in my gs code is this:
function callLibraryFunctionsByName(funcname) {
xyzSUS1[funcname]();//the this for funcname because xyzSUS1
}

How to use parameters that go to a script in html content?

I am new to using Google script, but not completely new to programming.
I have looked at some examples and they generate HTML output like this:
function doGet(e) {
return HtmlService.createHtmloutputFromFile('form.html')
}
I would like to pass a parameter to my script to use that in the output.
I have gotten as far as that I can use this in the function:
var room= e.parameter.room
So when I execute my script with url?room=test
I do get the value for that parameter.
But how can I use that variable room that I create in the HTML output and other code?
I have been looking at createTemplateFromFile but I am not getting anywhere.
Hope someone can point me in the right direction of what constructs and command to look at.
You can directly get query parameters client side1:
form.html
<script>
google.script.url.getLocation(function(location) {
alert(location.parameter.room); //alerts "test" on loading "url?room=test"
});
</script>
Alternatively, You can use scriplets to load html2:
code.gs
function doGet(e) {
var temp = HtmlService.createTemplateFromFile('form');
temp.room = e.parameter.room;
return temp.evaluate();
}
form.html:
<script>
alert('<?=room?>'); //Printing scriplets
</script>

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:

How to correctly construct state tokens for callback urls in Managed Libraries?

I'm having an issue with Google Apps Script state tokens when called from a managed library. This means a The state token is invalid or has expired. Please try again. error is always received is the state token is created from a sub function.
Here's some example code that would be in the library (you can add with project key MP9K5nBAvEJwbLYG58qx_coq9hSqx7jwh)
var SCRIPT_ID = "1eC5VsM2vkJXa9slM40MTKTlfARGAGyK1myMCU3AB_-Ox_jGxQaoPM8P2";
// get a callback url to render in popup
function getAuthURL() {
var authorizeURL = getCallbackURL('testCallback');
return authorizeURL;
}
// generate a user callback url
function getCallbackURL(callback) {
var state = ScriptApp.newStateToken().withTimeout(3600).withMethod(callback).createToken();
return 'https://script.google.com/macros/d/'+SCRIPT_ID+'/usercallback?state='+state;
}
// generate login popup
function showLogin(doctype){
doctype.getUi().showDialog(
HtmlService
.createTemplate("<div><p><a href='<?=getAuthURL()?>' id='start-auth'><?=getAuthURL()?></a></p>" +
"<p><a href='<?=getAuthURLStored()?>' id='start-auth'><?=getAuthURLStored()?></a></p></div>")
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.NATIVE)
);
}
// dummy callback function
function testCallback(e){
return HtmlService.createHtmlOutput('<b>Success. You can close this window. !</b>')
}
/*
Rather than using dynamic state url storing the callback url and getting from property
(you could set a script trigger to refresh this every 24 hours)
*/
function getAuthURLStored() {
var authorizeURL = getSetCallbackURL();
return authorizeURL;
}
function setCallbackURL(){
PropertiesService.getScriptProperties().setProperty('callbackURL', getCallbackURL('testCallback'))
}
function getSetCallbackURL(){
return PropertiesService.getScriptProperties().getProperty('callbackURL')
}
which could be called in a Google Document as (assuming managed library identifier is statetest.
function testFunction() {
statetest.showLogin(DocumentApp);
}
When testFunction is run the dialog in the Document presents two urls, the first with a dynamic state url is invalid the second with a stored state token works.
Is this a bug or expected behaviour?
What you are trying to do currently isn't supported. Specifically creating a state token in a library running in an outer script, but having the callback go straight to the library. As of today the callback must always be directed at the outer script, which can then delegate back to the library as needed. You can open a feature request on the issue tracker to support your use case and we'll consider it further.
An example to use a library to handle an authentication flow is to publish a web app from the library which the user is directed to to being the authentication process.
var SCRIPT_ID = "1eC5VsM2vkJXa9slM40MTKTlfARGAGyK1myMCU3AB_-Ox_jGxQaoPM8P2";
// get a callback url to render in popup
function getAuthURL() {
var authorizeURL = getCallbackURL('testCallback');
return authorizeURL;
}
// generate a user callback url
function getCallbackURL(callback) {
var state = ScriptApp.newStateToken().withTimeout(3600).withMethod(callback).createToken();
return 'https://script.google.com/macros/d/'+SCRIPT_ID+'/usercallback?state='+state;
}
// generate login
function doGet(e){
return HtmlService.createTemplate("<div><p><a href='<?=getAuthURL()?>' id='start-auth'><?=getAuthURL()?></a></p></div>")
.evaluate());
}
enter code here
// dummy callback function
function testCallback(e){
return HtmlService.createHtmlOutput('<b>Success. You can close this window. !</b>')
}