GAS Nesting Scriplets in Templated Pages - google-apps-script

Trying to figure out why this doesn't work. The rest of the page loads just fine but where the menu items would be expected it just says "HtmlOutput" - no buttons, no links, nothing but text. Nothing in the mBar div displays - just text ("HtmlOutput"). I've also tried printing and force printing the scriplet in the menuBar.html file with no luck.
Code.gs
function doGet(e) {
var html = HtmlService
.createTemplateFromFile('index')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
return html;
}
function include(file, appData) {
var component = HtmlService.createTemplateFromFile(file);
component.data = appData;
return component.evaluate();
}
index.html
<div id="mainContent" class="w3-hide">
<!-- Menu Bar -->
<?= include('menuBar', oUser)?>
</div>
menuBar.html
<div class="mBar">
Select Period
<? if (data.isAdmin == true) { ?>
Manage Periods
Manage Users
<? } ?>
</div>

include function should return type string, so change it this way:
function include(file, appData) {
var component = HtmlService.createTemplateFromFile(file);
component.data = appData;
return component.evaluate().getContent();
}
edit:
Also you need to use force-printing scriptlets to avoid escaping of your included html code (note "!" symbol):
<?!= include('menuBar', oUser)?>

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

How to insert HTML code into HTML file from a .gs using Google Apps Script

My variables I'm inserting into an HTML file using the code below inserts the variables as plaintext and does not register code within my variables.
function sendEmail(){
var htmlBody = HtmlService.createTemplateFromFile('emailFormat');
var variableOne = "text";
htmlBody.example = '<p>' + variableOne + '</p>';
var email_html = htmlBody.evaluate().getContent();
MailApp.sendEmail({
to: "email#email.com",
subject: "subject",
htmlBody: email_html
});
}
The code above makes anywhere I put <?= example ?> within the file 'emailFormat' become "<p>text</p>". The problem is it does not display it as "text" it displays "<p>text</p>" entirely. How do I get it to show register the <p>'s as code?
The tags <?= ?> will escape the output, thus <?= <p> ?> will "print" the string <p> and not a paragraph tag.
You can use <?!= ?> to not escape the output. Those are useful for including additional HTML and inline JavaScript, CSS files into your templates. Using the exclamation mark should fix your issue.
But in your case, try just putting the <p> tags outside <?= =?> tags like this:
<p><?= example ?></p>
That's the typical way to do it. Or if you need to switch the tags, you could use the <?!= option, but that puts HTML in your JavaScript... it's a subjective choice, but you can also control options by using the <? ?> tags like this:
Code.gs
// html refers to a template
html.type = "p";
html.content = "some paragraph content!";
template.html
<? if (type === "p") { ?>
<p> <?= content ?> </p>
<? } ?>
Try this:
function sendEmail(){
var v1="Hello World";
var html='<p>' + v1 + '</p>';
MailApp.sendEmail({to: "email#email.com", subject: "Email Test", htmlBody: html});
}
We don't really load html into html files from gs file scripts except possibly via the Google Apps Script Advanced API. We can however load html directly into the Document Object Model of a browser by using google.script.run and then it's just a matter of getting the element by id and loading it's text or innerHTML via Javascript. I don't think even scriplets actually load it into the html file but I'm not sure about that.

Prevent HTML Page Refresh

At this stage I'm mostly used to backend Javascript and server side Java, so my HTML is not as savvy as it needs to be.
I've built several applications that require user input with Apps script, but I was using the now deprecated UI service, as I'm not a designer and this provided an easy way to design simple pages to pass data back and forth. With the UI service having been deprecated for some time, I'm begging the arduous task of migrating these services to the HTML service, and I'm noticing some difference in behavior.
For example, when submitting a form, the entire page refreshes to a blank page, and I can't seem to prevent that. The UI service would remain static for information re-entry, and I can't find a working method to get the HTML service to either stop refreshing or reload the form.
Simple code to reproduce my issue:
function doGet() {
return HtmlService.createHtmlOutputFromFile('test')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function logValues(value){
Logger.log('Something worked....');
}
With the index file being:
<form>
<input type="submit" value="Book Meeting" onclick="google.script.run
.logValues()">
</form>
Some things I've tried:
1) Adding a callback to the 'doGet' function, to attempt to get the page to load again.
2) Adding a whole new function to try and call a NEW HTML page.
The issue here is my poor understanding of the HTML service, but is there a simple way for me to just clear the form for re-submission, or alternatively just reload the page? None of the other questions I've found on SO adequately answer this question in a way I can understand.
Since you're technically submitting your form by clicking the submit button, then that creates the page refresh. You need to cancel the submit event with the preventDefault function, which "Cancels the event if it is cancelable, without stopping further propagation of the event."
See the docs here: https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault
So maybe you can try something along these lines (straight from the docs):
function stopDefAction(evt) {
evt.preventDefault();
}
document.getElementById('my-checkbox').addEventListener('click', stopDefAction, false);
Another option is to remove the form/input elements and simply use a button element instead, which doesn't trigger a page refresh on click.
It's an interesting ride switching old UI services across, I just did that with one of my applications and it has really improved the readability of the code. I posted a copy of a basic version of what I was doing in another question
Once you get your head around it all it becomes a lot simpler. This is a really basic example of using multiple HTML files similar to your example using the HTMLService when submitting forms (you can pass in parameters instead)
Code.gs
function doGet() {
return HtmlService.createTemplateFromFile('Main')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.NATIVE);
}
function onLogin(form) {
if (form.username == "fuzzyjulz") {
var template = HtmlService.createTemplateFromFile('Response');
//Setup any variables that should be used in the page
template.firstName = "Fuzzy";
template.username = form.username;
return template.evaluate()
.setSandboxMode(HtmlService.SandboxMode.NATIVE)
.getContent();
} else {
throw "You could not be found in the database please try again.";
}
}
function include(filename) {
return HtmlService.createTemplateFromFile(filename)
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.getContent();
}
Main.html
<?!= include('CSS'); ?>
<script>
function loadPage(htmlOut) {
var div = document.getElementById('content');
div.innerHTML = htmlOut;
document.getElementById('errors').innerHTML = "";
}
function onFailure(error) {
var errors = document.getElementById('errors');
errors.innerHTML = error.message;
}
</script>
<div id="errors"></div>
<div id="content">
<?!= include('Login'); ?>
</div>
CSS.html
<style>
p b {
width: 100px;
display: inline-block;
}
</style>
Login.html
<script>
function onLoginFailure(error) {
var loginBtn = document.getElementById('loginBtn');
loginBtn.disabled = false;
loginBtn.value = 'Login';
onFailure(error);
}
</script>
<div class="loginPanel">
<form>
<p>
<b>Username: </b>
<input type="text" name="username"/>
</p>
<input type="button" id="loginBtn" value="Login" onclick="this.disabled = true; this.value = 'Loading...';google.script.run
.withSuccessHandler(loadPage)
.withFailureHandler(onLoginFailure)
.onLogin(this.parentNode)"/>
</form>
</div>
Response.html
<div class="text">
Hi <?= firstName ?>,<br/>
Thanks for logging in as <?= username ?>
</div>

Running a AS3 function from inside a htmlloader object triggered by several links <a>

Using this code im able to run ONE AS3 function from a click event on a link inside the content loaded in my HtmlLoader:
The html loaded content:
<html>
<body>
Click me.
</html>
The AS3
var html:HTMLLoader = new HTMLLoader( );
var urlReq:URLRequest = new URLRequest("test.html");
html.load(urlReq);
html.addEventListener(Event.COMPLETE, completeHandler);
function completeHandler(event:Event):void {
html.window.document.getElementById("testLink").onclick = clickHandler;
}
function clickHandler( event:Object ):void {
trace("Event of type: " + event.type );
}
The problem now is, I want to pass parameters to the clickHandler function from the html content, and also I want to have several links:
<a class="fireAS3" href="someData1">bla bla bla</a>
<a class="fireAS3" href="someData2">bla bla bla</a>
<a class="fireAS3" href="someData3">bla bla bla</a>
so using Ids is not an option, I guess i need classes but using getElementByIdClassName gives me an error (function does not exist). So, how can i solve this?
Hi You could use the LocationChangeEvent to do opposite JS->AS3 "conversation" this way you could prepare the links in such a way that will provide loads of arguments passed using JSON. Folloowing is an excerpt from my program that renders long list within the StageWebView (HTMLLoader has the same event) and get back click events with some details about it:
webView.addEventListener(LocationChangeEvent.LOCATION_CHANGE, onWebLocationUpdate, false, 0, true);
webView.addEventListener(LocationChangeEvent.LOCATION_CHANGING, onWebLocationUpdate, false, 0, true);
protected function onWebLocationUpdate(e:LocationChangeEvent):void
{
trace(e);
//JS->AS
if (e.location.indexOf("message://") != -1)
{
var jsonString:String = e.location.substr("message://".length);
var myObject:Object = JSON.parse( jsonString );
for (var name:String in myObject)
{
trace(name + ":" + myObject[name]);
}
e.preventDefault();
}
}
JS
jQuery(document).ready(function() {
// do something here
//alert("Ready");
$(".rowItem").click(function(e)
{
$(this).toggleClass("rowItemSelected");
$(this).bridgeSend($(this).attr("id"),$(this).hasClass("rowItemSelected"));
});
});
(function( $ ){
$.fn.bridgeSend = function(id, selected)
{
var method = "select";
var json = "{\"item\":{\"selected\":"+selected+",\"name\":\""+id+"\"},\"method\":\""+method+"\"}";
window.location = "message://"+json;
};
})( jQuery );
HTML
<div class="rowItem" id="entry2">
<div class="top">
<div class="icon"><img src="css/clock.png" /></div>
<div class="date"><h3>Fri, 04-09-2010</h3></div>
</div><!-- end of the top -->
<div class="desc">
<p><strong>Title</strong></p>
<p>The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.</p>
</div><!-- end of the desc -->
<div class="times">
<div class="col1">09:00</div>
<div class="col2">09:30</div>
<div class="col3">0.50</div>
</div>
</div><!-- end of the rowItem -->
For easy communication between air and js, i think u can easily use RichWebView ANE that we have coded https://github.com/myflashlab/webView-ANE

Refresh Page only time

I write a meta tag for refreshing web page. Now i want to refresh a page only one time.
What is the code for refreshing page only one time. Please help me to fix the problem...
Thanks in Advance..
Using javascript you could set a cookie with a "refreshed" variable in it and check if it's set, if not then refresh the page. Of course this involves quite a lot of code for setting and reading from the cookie plus the function to be called when you reload.
My approach would be url vars, then again it's php not meta tags, it would be something like this:
<?php
if($_GET['r'] != 1) header('refresh: 0; url=/index.php?r=1');
?>
Which reloads the page setting a variable in the url ,in this case r for refreshed, as true.
So the next time it loads it will not reload . It works, it's just one line of code and it will save you some coding time and get the job done.
Update: (User wanted it in asp)
Should work but I haven't tried it nor can I try it at the moment (I'm at the airport)
<%
dim refreshOnce
refreshOnce = request.querystring("r")
if refreshOnce <> 1 then Response.AddHeader "Refresh", "0;URL=/index.php?r=1"
%>
Javascript solution, using a form field to store the state:
<html>
<head>
<script language="javascript">
function init() {
var form = document.getElementById('theform');
var input = form.refreshed;
function reload() {
location.reload(false);
}
function isRefreshed() {
return !!input.value;
}
function doDisplay() {
var el = document.getElementById(isRefreshed() ? 'two' : 'one');
el.style.display = 'block';
}
function conditionalRefresh() {
if (!isRefreshed()) {
input.value = 'true';
setTimeout(reload, 1000);
}
}
doDisplay();
conditionalRefresh();
}
</script>
</head>
<body onload="init();">
<form id="theform">
<input type="hidden" name="refreshed" />
</form>
<div id="one" style="display: none;">
one
</div>
<div id="two" style="display: none;">
two
</div>
</body>
</html>