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

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

Related

Google sheet web app search data from sheet error

I want to search data from google sheet and show it in form by web app.
PAN is unique and 5 digit number. When we enter PAN ,5 digit number to( PAN) input form and click update button then it should search data for PAN in sheet and if match then bring entire row to the web form, otherwise it show unavailable.
When we enter PAN number and click update button , it show wrong data in form.
But when we check it by Logger.log() , it show right data .
I don't know and figure out why it show wrong data in web form when we click update button, Please help me and let me know what is the cause for this issue
function doGet(e)
{
return HtmlService.createHtmlOutputFromFile("page");
}
function searchData(pan)
{
var ss=SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
//var nameList=ss.getRange("A1:A").getValues().flat();
//var panList=ss.getRange("B1:B").getValues().flat();
//var aadharList=ss.getRange("c1:c").getValues().flat();
//var emailList=ss.getRange("d1:d").getValues().flat();
//var phnList=ss.getRange("e1:e").getValues().flat();
var data=ss.getRange(1,1,ss.getLastRow(),5).getValues();
var panList=data.map(function(r){return r[1];});
var nameList=data.map(function(r){return r[0];});
var aadharList=data.map(function(r){return r[2];});`enter code here`
var emailList=data.map(function(r){return r[3];});
var phnList=data.map(function(r){return r[4];});
var index=panList.indexOf((pan));
if(index>-1)
{
var name=nameList[index];
var aadhar=aadharList[index];
var email=emailList[index];
var phone=phnList[index];
return [name,pan,aadhar,email,phone]
}
else
{ return "unavailable"}
}
Logger.log(searchData(66666))
//html file..(page.html)
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<h1> Web App </h1>
<label> Name </label>
<input type="text" id="username">
<label> PAN</label>
<input type="number" id="userpan">
<label> Aadhar </label>
<input type="number" id="useraadhar">
<label> Email </label>
<input type="text" id="useremail">
<label> Telepnoe </label>
<input type="text" id="userphone">
<button id="btn"> Update </button>
<script>
//document.getElementById("userpan").addEventListener("input",search);
document.getElementById("btn").addEventListener("click",search);
function search()
{
var pan=document.getElementById("userpan").value;
if(pan.length==5)
{
google.script.run.withSuccessHandler(test).searchData(pan);
function test(s)
{
document.getElementById("username").value=s[0];
document.getElementById("userpan").value=pan;
document.getElementById("useraadhar").value=s[2];
document.getElementById("useremail").value=s[3];
document.getElementById("userphone").value=s[4];
}
}
}
</script>
</body>
</html>
From But when we check it by Logger.log() , it show right data . and your showing script, I thought that the reason of your issue might be due to that the values of panList are the number while var pan=document.getElementById("userpan").value is the string. In this case, even when the inputted value to the input tag is the number 66666, the value of var pan=document.getElementById("userpan").value is the string. By this, var index=panList.indexOf((pan)); is always -1.
If your script is simply modified, please modify it as follows.
From:
var data=ss.getRange(1,1,ss.getLastRow(),5).getValues();
To:
var data = ss.getRange(1, 1, ss.getLastRow(), 5).getDisplayValues();
By this, when pan of 66666, which is the string, is sent from Javascript to Google Apps Script, data retrieved by getDisplayValues() is the string. By this, var index=panList.indexOf((pan)); can be worked.
Note:
I thought that your Google Apps Script might be able to reduce the process cost as follows.
function searchData(pan) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var range = sheet.getRange("B1:B" + sheet.getLastRow()).createTextFinder(pan).matchEntireCell(true).findNext();
if (range) {
return sheet.getRange(range.getRow(), 1, 1, 5).getValues()[0];
}
return "unavailable";
}
Note:
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this.
You can see the detail of this in the report of "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
References:
getDisplayValues()
createTextFinder(findText)

google apps script : a select drop down list in google sheet

I have begun using google apps script in google sheets, and i want to create a dialogue box where the user will write an input that I will use later. the dialogue box shold have a drop list that will make suggestions or complete the input.
For anyone arriving here through a Google Search:
Drop downs in Google Sheets can be achieved with data validation. Create a column of entries. Let's say like this:
A4 = Apples
A5 = Tigers
A6 = Coriander
A7 = Forest
Then select a cell, say, B4. Now go to Data in the top menu. Choose Data validation. A module will open with options. Choose "List from a range." as criteria, then enter A4:A7 as the range. You will have the option to reject other input. Now hover over B4 and click the arrow. You will see that you now have an inline dropdown menu. It may be useful to know that you can add list sources an another tab and even hide that tab to users to keep the interface clean.
Now, to answer your actual question.
You want a dropdown to appear in a popup. This can be done! It's not as fast as using an inline dropdown with data validation, but it's much fancier.
Assuming you know at least the basics of Google Apps Script, here's the code:
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Custom Menu')
.addItem('Multiple choice', 'dropDownModal')
.addToUi();
}
function dropDownModal() {
var htmlDlg = HtmlService.createHtmlOutputFromFile('dropdown.html')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setWidth(350)
.setHeight(175);
SpreadsheetApp.getUi()
.showModalDialog(htmlDlg, 'A Title Goes Here');
};
function writeChoice(selection) {
const writeResponseLocation = "B4";
SpreadsheetApp
.getActiveSpreadsheet()
.getSheets()[0]
.getRange(writeResponseLocation)
.setValue(selection);
}
Then create a file called dropdown.html (in addition to the code.gs file above) and input the following:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<script>
function onSuccess() {
google.script.host.close();
}
function submit() {
const choice = document.getElementById('choice').value;
google.script.run
.withSuccessHandler(onSuccess)
.writeChoice(choice);
}
function setup() {
const button = document.getElementById('submitbutton');
button.addEventListener("click", submit)
}
</script>
<body onload="setup()">
<p>
There will be a slight delay on submission.
</p>
<form>
<select id="choice">
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="coriander">Coriander</option>
<option value="monkey">Monkey</option>
</select>
<button id="submitbutton">Submit</button>
</form>
</body>
</html>
Now save everything and reload the sheet. A menu will appear at the end of the menu bar called Custom Menu. Select that and choose Multiple choice. You'll have to give yourself permission to the run the code you entered for this to work (then choose the menu option again). That'll do it. Tweak the code to suit your needs.

Unable to pass HTML data into Code.gs function

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

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

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