Unable to pass HTML data into Code.gs function - google-apps-script

When I click on the run button (See my HTML code), I am unable to paste the data into sheet.
When I run the makeRequest function from the editor, I get an error - TypeError: Cannot read property 'startDate' of undefined.
HTML
<div class="block form-group">
<label for="select">Date Range</label>
<select id="select">
<option selected id="default" value="default">None</option>
<option selected id="today" value="today">Today</option>
<option selected id="yesterday" value="yesterday">Yesterday</option>
</select>
<div class="form-group">
<button id="btn">Run it</button>
</div>
</div>
Javascript. HTML
<script>
document.getElementById('btn').addEventListener("click",retrieveFacebookData);
function retrieveFacebookData () {
var facebookAccountData = {};
//I will be adding more key value pairs in this object later
facebookAccountData.startDate = document.getElementById('select').value;
google.script.run.makeRequest(facebookAccountData);
}
</script>
Code.gs
function makeRequest(facebookAccountData) {
console.log(facebookAccountData.startDate);
const row = [facebookAccountData.startDate,newDate()];
SpreadsheetApp.getActiveSheet().appendRow(row);
}

Modification points:
From When I run the makeRequest function from the editor, I get an error - TypeError: Cannot read property 'startDate' of undefined., in this case, if you directly run makeRequest with the script editor, the argument of facebookAccountData is not declared. By this, such error occurs. In your script, it is required to run the Javascript at the sidebar, dialog and Web Apps.
About your HTML & Javasript side, I think that there is a modification point. In your Javascript, document.getElementById('btn').addEventListener("click",retrieveFacebookData); is used. But <button class="btn">Run it</button> has no ID.
When above points are reflected to your script, it becomes as follows.
Modified script 1:
When you want to directly run the function of makeRequest with the script editor, how about the following modification?
function makeRequest(facebookAccountData) {
facebookAccountData = {startDate: "sample"};
console.log(facebookAccountData.startDate);
const row = [facebookAccountData.startDate, "newDate()"];
SpreadsheetApp.getActiveSheet().appendRow(row);
}
Modified script 2:
When you want to run the function of makeRequest with HTML & Javascript side, how about the following modification?
Google Apps Script side:
function openDialog() {
const html = HtmlService.createHtmlOutputFromFile("index");
SpreadsheetApp.getUi().showModalDialog(html, "sample");
}
function makeRequest(facebookAccountData) {
console.log(facebookAccountData.startDate);
const row = [facebookAccountData.startDate, "newDate()"];
SpreadsheetApp.getActiveSheet().appendRow(row);
}
HTML & Javascript side: index.html
<div class="block form-group"> <label for="select">Date Range</label> <select id="select">
<option selected id="default" value="default">None</option>
<option selected id="today" value="today">Today</option>
<option selected id="yesterday" value="yesterday">Yesterday</option>
</select>
<div class="form-group"> <button id="btn">Run it</button> </div>
</div>
</div>
<script>
document.getElementById('btn').addEventListener("click",retrieveFacebookData);
function retrieveFacebookData () {
var facebookAccountData = {};
//I will be adding more key value pairs in this object later
facebookAccountData.startDate = document.getElementById('select').value;
google.script.run.makeRequest(facebookAccountData);
}
</script>
In this case, please run openDialog() with the script editor. By this, a dialog is opened and HTML can be seen. When you select it and click the button, makeRequest is run with facebookAccountData. By this, the value is appended to the Spreadsheet.
Reference:
Dialogs and Sidebars in Google Workspace Documents

Related

HTML (Metro 4) and Google Sheets, Loading Data Asynchronously

I have been trying since October to load an array of items into a select. I'm able to do it with synchronous coding in the template, but not with asynchronous coding (Explanation). I have watched videos, read stackoverflow question after question, read google documentation, metro documentation, and just can't figure it out. This is a google apps script project with a .gs file back end and an .html file that's supposed to be used to load a sidebar.
I have this HTML
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- Metro 4 -->
<link rel="stylesheet" href="https://cdn.metroui.org.ua/v4/css/metro-all.min.css">
</head>
<body>
<!-- Title -->
<h2 style="text-align: center;">Add New Job</h2>
<!-- Job Name -->
<p style="text-align: center;"><span class='mif-chat-bubble-outline'></span> Job Name</p>
<input type="text" data-role="input" data-prepend="Name: ">
<!-- Creator Name -->
<p style="text-align: center;"><span class='mif-user-check'></span> Job Creator</p>
<select data-role="select" id="mySelectList">
<optgroup label="Employees">
<option value="" selected> Loading... </option>
</optgroup>
</select>
<nl></nl>
<p style="text-align: center;">
<button class="button success cycle " id="confirmButton"><span class="mif-checkmark"></span></button>
</p>
<!-- Metro 4 -->
<script src="https://cdn.metroui.org.ua/v4/js/metro.min.js"></script>
<script>
$(function() {
$('#txt1').val('');
console.log("Running updateSelect");
google.script.run
.withSuccessHandler(updateSelect)
.getEmployeeNames();
});
function updateSelect(vA)//array of value that go into the select
{
var select = document.getElementById("mySelectList");
select.options.length = 0;
for (var i=0; i<vA.length;i++){
console.log("Creating items %s, %s", vA[i], vA[i]);
select.options[i] = new Option(vA[i],vA[i]);
console.log ("select.options[i] = %s, select.options = %s.", select.options[i], select.options);
}
}
console.log("My Code Ran");
</script>
</body>
</html>
Which provides this console feedback when ran.
Console Feedback
I have erased all my cookies and cache, disabled all addons, etc, and that did not fix the problem.
Why won't the options change from "Loading..." to the actual new values I've put in? For reference, I have the following code that does work, but it does so in an inefficient way (inside the template itself, instead of being called).
<!-- Creator Name -->
<? var employees = getEmployeeNames(); ?>
<p style="text-align: center;"><span class='mif-user-check'></span> Job Creator</p>
<select data-role="select" id="jobCreator">
<optgroup label="Employees">
<? for (var i = 0; i < employees.length; i++){ ?>
<option> <?= employees[i] ?></option>
<? } ?>
</optgroup>
</select>
Here is the backend script for getEmployeeNames(). I've tried it returning both a 1 dimensional or 2 dimensional array. While the function gives the expected output, the HTML select still does not update with new options.
function getEmployeeNames(){
var employeeSheet = SpreadsheetApp.getActive().getSheetByName('Dropdown Lists');
var names = employeeSheet.getRange(5, 4, employeeSheet.getLastRow(), 1).getValues();
names = removeBlankEntries(names);
Logger.log ('Employee Names "%s"', names);
return names;
}
function removeBlankEntries(arr){
var output = [];
arr.forEach(function(value){
if (value != "" & value != " "){
output.push(value[0]) // add [0] to make it 1d
};
});
return output;
}
Here's an example Logger.log output.
8:27:00 AM Info Employee Names "[PM, Employee 2, Employee 3, Employee 4]"
Or with it set to give a 2d array.
8:28:00 AM Info Employee Names "[[PM], [Employee 2], [Employee 3], [Employee 4]]"
Additionally, here is how the sidebar is loaded in the code.
function loadNewJobSidebar(){
var htmlTemplate = HtmlService
.createTemplateFromFile("addJob");
var htmlOutput = htmlTemplate.evaluate()
.setTitle('Add New Job');
var ui = SpreadsheetApp.getUi();
ui.showSidebar(htmlOutput);
}
Any guidance would be helpful. I thought the video above was the best, and I copied it three or four times without success. I've been coding for a decade - even though it is only my side gig - and I've never had a problem last so many months.
== Sorry this is becoming a monster thread. Below are exact steps for replication ==
Create a new blank Google Sheet.
Open the script editor and create an HTML file called addJob (it will automatically add the .html to the end)
In the HTML file, copy the code exactly from the first snippet in this post titled "I have this HTML"
In the code.gs file, copy the following code
function onOpen() {
var ui = SpreadsheetApp.getUi();
// Or DocumentApp or FormApp.
ui.createMenu('Manage Sheet')
.addSubMenu (ui.createMenu('Jobs')
.addItem('Add New Job', 'loadNewJobSidebar'))
.addToUi();
}
function getEmployeeNames(){
return ["Bob", "Jamie", "Ted"];
}
function loadNewJobSidebar(){
var htmlTemplate = HtmlService
.createTemplateFromFile("addJob");
var htmlOutput = htmlTemplate.evaluate()
.setTitle('Add New Job');
var ui = SpreadsheetApp.getUi();
ui.showSidebar(htmlOutput);
}
Run the onOpen() function from the script editor. Allow it permission to run.
Go back to the spreadsheet, and on custom menu, select "Manage Sheet -> Jobs -> Add New Job"
The sidebar will load and say "Loading" but will not use the names from the getEmployee() function.
Press F12 and note the console log statements showing the select options should be loaded.
From your updated question, I could understand your current issue. In the case of your script, how about modifying the function updateSelect as follows?
Modified script:
function updateSelect(vA) {
var select = Metro.getPlugin("#mySelectList", 'select');
select.data(Object.fromEntries(vA.map(e => [e, e])));
}
In this modification, it supposes that the value of vA is the one-dimensional array. Please be careful about this.
Reference:
Select of METRO_4

Creating a dialogue box to capture booking details in Google sheets App Script

I am using Google sheets with app script to build a reservations chart for a hotel
Can someone please tell me if there is a way to add a Dialogue box to a google sheet that can ask multiple questions? I have found the Prompt Dialogue box but that seems to allow only one text box for data entry. I have something like this
var result = ui.prompt(
"Let's get to know each other!",
"Please enter your name:",
ui.ButtonSet.OK_CANCEL
);
// Process the user's response.
var button = result.getSelectedButton();
var text = result.getResponseText();
if (button == ui.Button.OK) {
// User clicked "OK".
ui.alert("Your name is " + text + ".");
} else if (button == ui.Button.CANCEL) {
// User clicked "Cancel".
ui.alert("I didn't get your name.");
} else if (button == ui.Button.CLOSE) {
// User clicked X in the title bar.
ui.alert("You closed the dialog.");
}
If there isnt something pre-built, can you please recommend how else I can capture data which would then feed a second sheet within the same spreadsheet .
many thanks
You need to use the HTML service
The method you are using is quite limited. To go further than that you would need to create your own HTML file and serve it from Apps Script. The flow of that is:
Create an HTML file in the script editor
Create your HTML form
Write a script on the HTML that calls a function on your gs script.
Sample code
Code.gs
// Creates form on UI
function form() {
var htmlOutput = HtmlService
.createHtmlOutputFromFile('form')
.setWidth(250)
.setHeight(300);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Add your info');
}
// Uses info passed from rendered HTML to add data to sheet.
function addForm(data){
console.log(data)
SpreadsheetApp.getActiveSpreadsheet().getRange("A1:C1").setValues([data])
}
form.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
// function to run when server-side script is successful
function onSuccess(){
google.script.host.close()
}
// function to run when form is submitted
function sendForm(){
console.log("RUNNING")
let name = document.getElementById("name").value
let country = document.getElementById("country").value
let DOB = document.getElementById("DOB").value
let data = [name, country, DOB]
// call server side function
google.script.run.withSuccessHandler(onSuccess).addForm(data)
}
</script>
</head>
<body>
<form id="form" onsubmit="sendForm()">
<label for="name">First name:</label><br>
<input type="text" name="name" id="name">
<label for="country">Country:</label><br>
<input type="text" name="country" id="country">
<label for="DOB">DOB:</label><br>
<input type="text" name="DOB" id="DOB">
<input type="submit">
</form>
</body>
</html>
Explanation
When the function form() is run from the script editor, it displays your HTML in the Spreadsheet UI.
This shows a form with three text inputs and a submit button.
The submit button has a onsubmit="sendForm()" which is a function defined within the HTML.
It gets all the info from the form, and then calls google.script.run.withSuccessHandler(onSuccess).addForm(data). This is an asynchronous function that sends a request to the gs file to run the addForm function and then when successful, to run the onSuccess function in the HTML.
The onSuccess simply closes the form.
addForm adds the info to a range in the spreadsheet.
Reference
HTML service
Show Modal Dialog
google.script.run

Apps script get element by id

I've got the following in an apps script client side html template:
<body>
<form id="myForm" onsubmit="handleFormSubmit(this)">
<div>
<label for="optionList">Click me</label>
<select id="optionList" name="email" onchange="optionChange()">
<option>Loading...</option>
</select>
</div>
I want to capture the change using :
function optionChange() {
mySelect = document.getElementById("optionList");
console.log(mySelect)
};
But I do not see any elements in the console.
Following some other boilerplate code I got from some examples I tried:
mySelect = $(document).getElementById("optionList").value;
Now in the console I get:
TypeError: $(...).getElementById is not a function
At least I see something in the console. What is '$' and How can I get this working?
I've made the suggested change but still just see spreadsheet related post and get statements in the console.
You should specify the value property of the element, so
//based on your coding style
function optionChange() {
mySelect = document.getElementById("optionList");
mySelectValue = mySelect.value;
console.log(mySelectValue)
};
or
//based on your coding style
function optionChange() {
mySelect = document.getElementById("optionList").value;
console.log(mySelect)
};

Reload Add On from Code

I have a google apps-script add-on which is loaded in the side bar of a Google Spreadsheet.
How does one reload an apps script add-on ones the user switches sheets or from button click ?
The simplest way to reload an ad-on is to start/launch it again from the add-on's menu - this will re-load the sidebar. Or you can add a button to your sidebar which runs google.script.run.showSidebar() (or whatever your server-side function for showing the sidebar is called).
Since add-ons only have access to simple triggers (onInstall(), onOpen() and onEdit()) and can't (yet) tell what a user does outside the add-on, you will have to write your own javascript function in the sidebar's html page to re-set the your add-on's user interface to default state (I assume that is what you mean by "reload"), i.e to reset all form fields to default values, remove any injected help/status text, etc etc.
To have this function execute on button click is not too hard - just trigger the function from the button's onclick event. With a bit more work you can even add a 'Reset' menu item in your add-on's menu that does the same thing.
To make such function run 'automatically' when user switches sheet is also possible, but will require polling for spreadsheet changes. Basically you can write a javascript function in your add-on's sidebar page that runs on a certain interval and calls a server-side function that checks if currently active sheet is same as before (which you can store in userProperties, for example). If the sheet is different, call your js function that resets the ui of your add-on + update the userProperty with the name of currently active sheet. Keep in mind that there will be a bit of delay between user switching sheets and your add-on running its reset code and reloading its ui - if that is an issue, then reloading the ui from button click is a better option.
Here is some sample code to give you an idea of what you can do. You can view the working spreadsheet here
Code.gs
function onOpen(e) {
// Add this add-on to Add-ons menu
SpreadsheetApp.getUi().createAddonMenu()
.addItem('Start / Reset', 'showSidebar')
.addToUi();
// save current active sheet name in user properties for later checking if user switched sheets
saveSheetNameToProperty();
};
function onInstall(e) {
onOpen(e);
};
function showSidebar() {
var ui = HtmlService.createHtmlOutputFromFile('Sidebar').setTitle('Add-on Reset Test');
SpreadsheetApp.getUi().showSidebar(ui);
};
/**
* Saves current active sheet name to user property lastActiveSheet
* #param String sheetname Name of sheet to save to user property. If undefined (not passed), saves current active sheet name.
*/
function saveSheetNameToProperty(sheetname) {
var sheetName = sheetname || SpreadsheetApp.getActiveSheet().getName();
PropertiesService.getUserProperties().setProperty("lastActiveSheet", sheetName)
};
/**
* Checks if user has switched sheets by comparing current active sheet name to name stored in user property
* #return Boolean True/False flag denoting if sheet was switched. True=sheet was switched; False=still on same sheet
*/
function checkSheetChanged() {
var sheetChanged = false;
var sheetName = SpreadsheetApp.getActiveSheet().getName();
var lastActiveSheet = PropertiesService.getUserProperties().getProperty("lastActiveSheet");
if (sheetName!=lastActiveSheet) {
sheetChanged = true;
saveSheetNameToProperty(sheetName);
// if you'd rather just reload the whole sidebar, then un-comment the line below and delete the return statement
// showSidebar();
}
return sheetChanged;
};
Sidebar.html
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<!-- The CSS package above applies Google styling to buttons and other elements. -->
<div class="sidebar branding-below">
<form id="addonForm">
<div class="block">
<label for="selectBox">Select a value:</label>
<select name="selectBox" id="selectBox">
<option value="" selected>Select a value...</option>
<option value="Value 1">Value 1</option>
<option value="Value 2">Value 2</option>
<option value="Value 3">Value 3</option>
</select>
</div>
<div class="block">
<label for="textBox">Enter some text:</label>
<input type="text" name="textBox" id="textBox" placeholder="Enter some text...">
</div>
<div class="block" id="button-bar">
<button type="button" class="blue" id="simpleResetBtn" onclick="resetForm(true);" title="I reset the sidebar's form controls to their default state">Reset form</button>
<button type="button" class="red" id="reloadAddonBtn" onclick="google.script.run.showSidebar();" title="I completely reload the sidebar - fresh start!">Reload add-on</button>
</div>
</form>
<div class="block" id="statusText" style="color:#666; margin-top:10px;"></div>
</div>
<div class="sidebar bottom">
<span class="gray branding-text">Reset Add-on Sample by Azadi</span>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
/**
* On document load, set up interval-based execution of checkSheetChanged() function to check if user has switched sheets
*/
$(function() {
// run checkSheetChanged() function every 5000 miliseconds
var sheetChecker = window.setInterval(checkSheetChanged, 5000);
});
/**
* Resets the form in the add-on's sidebar and shows status text.
* #param Boolean fromButtonClick Boolean flag denoting if form reset was triggered from button click or via timed script execution
*/
function resetForm(fromButtonClick) {
var buttonClick = fromButtonClick || false;
var form = $("#addonForm")[0];
var statusDiv = $("#statusText");
form.reset();
if (buttonClick) {
statusDiv.text("Addon UI has been reset from [Reset form] button click");
}
else {
statusDiv.text("Addon UI has been reset automatically via timed script following sheet switch");
}
};
/**
* Runs the checkSheetChanged() server-side function (in Code.gs) to check if user has switched sheets
* and executes checkSheetChangedCallback() function on success
*/
function checkSheetChanged() {
google.script.run.withSuccessHandler(checkSheetChangedCallback).checkSheetChanged();
};
/**
* Callback for checkSheetChanged() function.
* Resets the form in the sidebar if user has switched sheets.
* #param Boolean isDifferentSheet Boolean flag returned from server-side script. True=sheet was switched. False=user is still on same sheet.
*/
function checkSheetChangedCallback(isDifferentSheet) {
if (isDifferentSheet) {
resetForm();
}
};
</script>
Hope this helps!

HtmlService form submit opens new tab with foo bar URL

I am attempting to build a UI for a spreadsheet using GAS HtmlService. The HTML below is a very simple form with a single text box that pulls a value ("Kristina") from the sheet, successfully. However, when I try to submit the form a new tab is opened in Chrome that attempts to open the URL "bffc95ee-ff64-4d2c-xxxx-19d9824eb4b4.foo.bar/?fname=Kristina" with "xxxx" replacing more random letters and numbers (just in case). At no point do I use the words "foo.bar" in my code, so I'm pretty sure that that part isn't coming from me. It does not change each time or after logging out and back in. I'm getting the same result on two different computers.
<html>
<body>
<div>
<form id="formtest1">
<label>First Name</label>
<input name="fname" type="text" maxlength="255" value="<?= fname ?>"/>
<input type="submit" value="Submit"
onclick="google.script.run.processForm(document.getElementById('formtest1'));
google.script.host.close()"/>
</form>
</div>
</body>
</html>
The above is being displayed using the following function:
function htmltest(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sht = ss.getActiveSheet();
var html = HtmlService.createTemplateFromFile("HTML");
html.fname = sht.getRange(2, 3).getValue();
ss.show(html.evaluate());
};
If I understand correctly, the "google.script.run.processForm(...)" script in the HTML should trigger the following function, as set up in the projects triggers:
function onFormSubmit(){
Browser.msgBox("Test");
};
But it doesn't appear to do so as the form doesn't close and the msgBox doesn't appear. Only the foo bar URL in a new tab.
Hopefully I've explained the issue clearly and am not making an embarrassing mistake.
You cannot use a real "submit" button with google.script.run (this is a documented restriction in the user guide). Change it to "button" and it should work fine.
The project trigger onFormSubmit() will be triggered by a submission via the Forms Service. There is no relationship between this trigger and your HTML code; they are two different ways to interact with users.
An html forms pattern is shown in the HTML Service documentation here, and the script below is an adaptation of it.
Code.gs
The only real change from your original is that onFormSubmit() has been replaced with processForm(form), which includes a parameter, for the object that will be passed from the html code.
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "htmltest",
functionName : "htmltest"
}];
sheet.addMenu("Custom Menu", entries);
};
function htmltest(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sht = ss.getActiveSheet();
var html = HtmlService.createTemplateFromFile("HTML");
html.fname = sht.getRange(2, 3).getValue();
//Logger.log( html.getCodeWithComments() );
ss.show(html.evaluate());
};
function processForm(form){
var fname = form.fname;
Browser.msgBox("Test - " + fname);
};
HTML.html
This is a modification of your original, echoing the pattern from the documentation. The form submission SuccessHandler is a one-liner, which closes the dialog. Once it completes, the server-side function is invoked with the form content, retrieved using this.parentNode (to refer to the form).
There are other ways - see Get value of html text box in Apps Script function for a different approach.
<html>
<script type="text/javascript">
// SuccessHandler to close form
function close() {google.script.host.close();}
</script>
<body>
<div>
<form>
<label>First Name</label>
<input name="fname" type="text" maxlength="255" value="<?= fname ?>"/>
<input type="button" value="Submit" onclick="google.script.run
.withSuccessHandler(close)
.processForm(this.parentNode)"/>
</form>
</div>
</body>
</html>
Just add this to your script tag on your html file.
// Prevent forms from submitting.
function preventFormSubmit() {
var forms = document.querySelectorAll('form');
for (var i = 0; i < forms.length; i++) {
forms[i].addEventListener('submit', function(event) {
event.preventDefault();
});
}
}
window.addEventListener('load', preventFormSubmit);
Source: HTML Service: Communicate with Server Functions