Add refresh button to a Google Scripts Web App - google-apps-script

I came across the following script last night and it works really nicely to drop files into a Google Drive folder, however I've noticed that there's no clear way to get back to the front page of the app after uploading a file.
https://script.google.com/macros/d/1URDuve8yT1EpDj_WKLHPAuiVt1LWDdUN2kzH-ERUnuxVQqXbi-9I9EfU/edit?usp=drive_web
I realised that this can be achieved by refreshing the page, but my end users are people who are not very computer savvy, and I would like to add a button that refreshes the form to make it a bit easier on them. Unfortunately, I have no idea how to go about doing this.
Can anybody help me out?

Once your file is uploaded successfully this function is called :
function fileUploaded(status) {
document.getElementById('myForm').style.display = 'none';
document.getElementById('output').innerHTML = status;
}
As we can see, this function is hiding the form and putting status in output div, so if we don't hide the form and only update status in output[or maybe you can so a popup/alert on success ?] I think your purpose will be solved.
Something like this should work [Maybe you'll need to style your html a bit]:
function fileUploaded(status) {
document.getElementById('output').innerHTML = status;
}
You can add a button with href to the self page[web app], this is a hacky way to refresh.

The form is has id="myForm" and the status is shown on a div with id="output".
To show the form set is display style property to block. You could do this my using something like
document.getElementById('myForm').style.display = 'block';
To clear the status just add use something like
document.getElementById('output').innerHTML = '';
Example:
The following examples use HTML/CSS and pure JavaScript to show how to "reset a page" on Google Apps Script
//Initializes the html elements as they are shown after a file is uploaded
document.getElementById('myForm').style.display = 'none';
document.getElementById('output').innerHTML = "File uploaded successfully.";
function resetPage() {
document.getElementById('myForm').style.display = 'block';
document.getElementById('output').innerHTML = '';
}
input {
display:block; margin: 20px;
}
<form id="myForm">
<input type="text" placeholder="Input 1">
<input type="text" placeholder="Input 2">
</form>
<div id="output"></div>
<input type="button" onClick="resetPage();" value="Reset">

Related

HTML form File input is empty in Google Script - Web

I have created a web application in Google Script with a custom HTML form. The form looks like below:
<form id="msForm">
.......
<input type="file" class="custom-file-input" name="certificateFile" accept="application/pdf" required >
.....
<input type="button" name="next" class="next action-button" value="Submit" />
</form>
JS has the following
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);
$(document).ready(function(){
$(".next").click(function(){
if($(this).val()=="Submit") {
google.script.run
.withSuccessHandler(successFormSubmit)
.withFailureHandler(failedFormSubmit)
.processForm($(this).closest("form")[0]);
}
}
});
The google script in the Code.gs
function processForm(formObject) {
console.log(formObject.certificateFile);
}
The log trace shows as empty in the execution's log, when I select a file and then click the submit button.
{}
---- Edit to have more details ----
The form is having 4 file upload field including the given above and all are getting empty at server side.
The form was working fine and I added the validation later. Now the form is not submitting the file contents even they are shown at client side.
The form submitting code is :
google.script.run.withSuccessHandler(successFormSubmit).withFailureHandler(failedFormSubmit).processForm(document.getElementById("msForm"));
Code.gs
function processForm(formObject) {
console.log(formObject.certificateFile);
}
Further more, I added Google Apps Script GitHub Assistant and removed as it was giving errors and not working properly.
--- Edit 02 ---
I have found the root cause for the problem. This happens when there are more than one file input field in the html form. Can anyone tell me to over come this? Is it with default GCP project settings?
--- Edit 03 ---
This issue has been fixed by Google.enter link description here
This issue is reported and now it is fixed by the Google.
You can find more details here.

HTMLService won't display return value

I created a sidebar to have a basic UI for searching my Google sheet. I'm following this tutorial exactly to make sure the first step works, except that it doesn't! I even took out the userObject part to make it simpler (honestly, because I don't know what that part does).
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function updateButton(email, button) {
button.value = 'Clicked by ' + email;
}
</script>
</head>
<body>
<input type="button" value="Not Clicked"
onclick="google.script.run
.withSuccessHandler(updateButton)
//.withUserObject(this)
.testMe()" />
<input type="button" value="Not Clicked"
onclick="google.script.run
.withSuccessHandler(updateButton)
//.withUserObject(this)
.testMe()" />
</body>
</html>
It calls this function:
function testMe() {
Logger.log("Test log.");
return ContentService.createTextOutput("Jackpot!");
}
If it matters, the HTML runs in a sidebar via onOpen as follows:
function showGradingSidebar() {
var html = HtmlService.createHtmlOutputFromFile('testSidebar')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('Testing Module')
.setWidth(300);
SpreadsheetApp.getUi()
.showSidebar(html);
}
When I click the button, it does nothing (that I can see). By changing various aspects, I can get it to Logger.log() a simple message but even that doesn't work reliably if I change the HTML side.
I was reading about the security restrictions that require sanitizing what the function returns, but both HtmlService.createHtmlOutput() and ContentService.createTextOutput() were also unsuccessful. Please advise.
UPDATE: Thanks to #Bryan P, I got it to work. The testMe() is simply:
return "Jackpot";
...and the HTML page looks like this:
[html, head, etc.]<body>
<input type="button" value="Ready"
onclick="google.script.run
.withSuccessHandler(updateButton)
.withUserObject(this)
.testMe()" --->
<br><div id="output">Output goes here: </div>
<br><div id="papa">Papa goes here: </div>
<br><p></p>
<script>
function updateButton(result) {
var div = document.getElementById('output')
div.innerHTML = 'It finally works!' + result;
}
</script>
</body>
</html>
I don't know how much it helped, but I did move the script tag down to the bottom of the body, fwiw, after reading this SO post.
In Chrome, if you right-click in the sidebar area >> Inspect >> in the Console it should show a message that there wasn't a valid return type after clicking on one of the buttons.
.createTextOutput(content) returns a TextOutput type (which isn't the same as just plain text type)
It's only used when you've deployed a the web app URL and some external service calls that URL. It only gets handled with doGet() too.
Did you try just return "Jackpot"; instead?
.withUserObject(this) - this refers to button element and the whole method passes it on to the successHandler(). So you can consider keeping it. Otherwise you'd have to reference the button from within the successHandler another way:
function updateButton(email) {
document.getElementById('myButton').value = 'Clicked by ' + email;
}
...which requires you add an ID attribute into the button.
You can always do:
function updateButton(email, button) {
console.log('Success hit');
button.value = 'Clicked by ' + email;
}
...to check whether the successHandler was even called in that Chrome dev console too.

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>

Google CSE open in new window

I would like to integrate the Google Search bar into my site, and using the default code by Google CSE I have:
<div id="cse-search-form" style="width: 100%;">Loading</div>
<script src="https://www.google.com/jsapi" type="text/javascript"></script>
<script type="text/javascript">
google.load('search', '1', {language : 'en'});
google.setOnLoadCallback(function() {
var customSearchOptions = {};
var imageSearchOptions = {};
imageSearchOptions['layout'] = google.search.ImageSearch.LAYOUT_POPUP;
customSearchOptions['enableImageSearch'] = true;
customSearchOptions['imageSearchOptions'] = imageSearchOptions;
var customSearchControl = new google.search.CustomSearchControl(
'003243520079760326318:WMX-1462312306', customSearchOptions);
customSearchControl.setResultSetSize(google.search.Search.FILTERED_CSE_RESULTSET);
var options = new google.search.DrawOptions();
options.setSearchFormRoot('cse-search-form');
options.setAutoComplete(true);
customSearchControl.draw('shop.htm/cse', options);
}, true);
Followed by the style and the </div>
But I do not want the results to open on the same page, I want them to open in searchresults.htm which has the container div
<div id="cse" style="width:100%;"></div>
if I put in this form:
<form action="http://www.amberantiques.com/searchresults.htm" id="cse-search-box">
<fieldset style="border:none;">
<input type="hidden" name="cx" value="003243520079760326318:WMX-1462312306" />
<input type="hidden" name="ie" value="UTF-8" />
<input type="text" name="q" size="31" />
<input type="submit" name="sa" value="Search" />
</fieldset>
</form>
Then the form sends it to the page but doesnt run the search, but if you then use the Google bar on the page, it runs the search fine.
Basically, how do you get the google bar to open the results page?
Cheers
If you upgrade to the latest Google Code V2 then you can achieve this by editing code you paste to show results.
<gcse:search></gcse:search>
Change this to
<gcse:search linktarget="_parent"></gcse:search>
When you're building the code for your Google CSE, one of the Look and Feel options is "Two Page" - which will allow you to search on one page, and display the results on another.
The V2 code for the Custom Search (free) or Site Search (paid) gives you a range of options for searching and displaying results on the same page or having it's own result page.
By default this WILL open all result links in a new tab or window.
I had the issue where I needed the search results to open on the same tab/window.
I adjusted the following code
<gcse:search></gcse:search>
to this
<gcse:search linktarget="_self"></gcse:search>
I guess if for some reason your default behavior is not opening in a new tab/window and you need it to then you could try the following
<gcse:search linktarget="_blank"></gcse:search>
Hope this helps.
Can you test by putting this code?
options.enableSearchboxOnly("http://www.amberantiques.com/searchresults.htm");
between this line
var options = new google.search.DrawOptions();
and this line
options.setSearchFormRoot('cse-search-form');
Then put the following code in searchresults.htm
<div id="cse" style="width: 100%;">Loading</div>
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
<script type="text/javascript">
function parseQueryFromUrl() {
var queryParamName = "q";
var search = window.location.search.substr(1);
var parts = search.split('&');
for (var i = 0; i < parts.length; i++) {
var keyvaluepair = parts[i].split('=');
if (decodeURIComponent(keyvaluepair[0]) == queryParamName) {
return decodeURIComponent(keyvaluepair[1].replace(/\+/g, ' '));
}
}
return '';
}
google.load('search', '1', {language : 'en'});
google.setOnLoadCallback(function () {
var customSearchControl = new google.search.CustomSearchControl(
'003243520079760326318:WMX-1462312306', customSearchOptions);
customSearchControl.setResultSetSize(google.search.Search.FILTERED_CSE_RESULTSET);
customSearchControl.draw('cse');
var queryFromUrl = parseQueryFromUrl();
if (queryFromUrl) {
customSearchControl.execute(queryFromUrl);
}
}, true);
</script>
If this doesn't work you can simply read the documentation provided by google. You'll get your desired information in Designing the Look and Feel with the Control Panel section. Or you may find it in Designing the Look and Feel with XML section. I think you are looking for two page layout.
Another option is to go to http://www.google.com/cse/manage/all and then use the control panel there to customize your search engine as you desire.
Building on the code above, you could use:
<gcse:search newWindow="true"></gcse:search>
According to Google's documentation.
Not obvious from looking at the Google documentation (a familiar story) but you can do this very simply using the v2 Custom Search code by selecting the 'Results only' option in the 'Look and Feel' section:
Click 'Save and Get Code' and paste into your searchresults.htm page.
You now just need to create a simple search box that points to that page which you can put in your page header.
e.g.
<form action="http://www.amberantiques.com/searchresults.htm">
<input type="search" name="q"/>
<input type="submit" value="Go"/>
</form>

Manually Triggering Form Validation using jQuery

I have a form with several different fieldsets. I have some jQuery that displays the field sets to the users one at a time. For browsers that support HTML5 validation, I'd love to make use of it. However, I need to do it on my terms. I'm using JQuery.
When a user clicks a JS Link to move to the next fieldset, I need the validation to happen on the current fieldset and block the user from moving forward if there is issues.
Ideally, as the user loses focus on an element, validation will occur.
Currently have novalidate going and using jQuery. Would prefer to use the native method. :)
TL;DR: Not caring about old browsers? Use form.reportValidity().
Need legacy browser support? Read on.
It actually is possible to trigger validation manually.
I'll use plain JavaScript in my answer to improve reusability, no jQuery is needed.
Assume the following HTML form:
<form>
<input required>
<button type="button">Trigger validation</button>
</form>
And let's grab our UI elements in JavaScript:
var form = document.querySelector('form')
var triggerButton = document.querySelector('button')
Don't need support for legacy browsers like Internet Explorer? This is for you.
All modern browsers support the reportValidity() method on form elements.
triggerButton.onclick = function () {
form.reportValidity()
}
That's it, we're done. Also, here's a simple CodePen using this approach.
Approach for older browsers
Below is a detailed explanation how reportValidity() can be emulated in older browsers.
However, you don't need to copy&paste those code blocks into your project yourself — there is a ponyfill/polyfill readily available for you.
Where reportValidity() is not supported, we need to trick the browser a little bit. So, what will we do?
Check validity of the form by calling form.checkValidity(). This will tell us if the form is valid, but not show the validation UI.
If the form is invalid, we create a temporary submit button and trigger a click on it. Since the form is not valid, we know it won't actually submit, however, it will show validation hints to the user. We'll remove the temporary submit button immedtiately, so it will never be visible to the user.
If the form is valid, we don't need to interfere at all and let the user proceed.
In code:
triggerButton.onclick = function () {
// Form is invalid!
if (!form.checkValidity()) {
// Create the temporary button, click and remove it
var tmpSubmit = document.createElement('button')
form.appendChild(tmpSubmit)
tmpSubmit.click()
form.removeChild(tmpSubmit)
} else {
// Form is valid, let the user proceed or do whatever we need to
}
}
This code will work in pretty much any common browser (I've tested it successfully down to IE11).
Here's a working CodePen example.
You can't trigger the native validation UI (see edit below), but you can easily take advantage of the validation API on arbitrary input elements:
$('input').blur(function(event) {
event.target.checkValidity();
}).bind('invalid', function(event) {
setTimeout(function() { $(event.target).focus();}, 50);
});
The first event fires checkValidity on every input element as soon as it loses focus, if the element is invalid then the corresponding event will be fired and trapped by the second event handler. This one sets the focus back to the element, but that could be quite annoying, I assume you have a better solution for notifying about the errors. Here's a working example of my code above.
EDIT: All modern browsers support the reportValidity() method for native HTML5 validation, per this answer.
In some extent, You CAN trigger HTML5 form validation and show hints to user without submitting the form!
Two button, one for validate, one for submit
Set a onclick listener on the validate button to set a global flag(say justValidate) to indicate this click is intended to check the validation of the form.
And set a onclick listener on the submit button to set the justValidate flag to false.
Then in the onsubmit handler of the form, you check the flag justValidate to decide the returning value and invoke the preventDefault() to stop the form to submit. As you know, the HTML5 form validation(and the GUI hint to user) is preformed before the onsubmit event, and even if the form is VALID you can stop the form submit by returning false or invoke preventDefault().
And, in HTML5 you have a method to check the form's validation: the form.checkValidity(), then in you can know if the form is validate or not in your code.
OK, here is the demo:
http://jsbin.com/buvuku/2/edit
var field = $("#field")
field.keyup(function(ev){
if(field[0].value.length < 10) {
field[0].setCustomValidity("characters less than 10")
}else if (field[0].value.length === 10) {
field[0].setCustomValidity("characters equal to 10")
}else if (field[0].value.length > 10 && field[0].value.length < 20) {
field[0].setCustomValidity("characters greater than 10 and less than 20")
}else if(field[0].validity.typeMismatch) {
field[0].setCustomValidity("wrong email message")
}else {
field[0].setCustomValidity("") // no more errors
}
field[0].reportValidity()
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<input type="email" id="field">
Somewhat easy to make add or remove HTML5 validation to fieldsets.
$('form').each(function(){
// CLEAR OUT ALL THE HTML5 REQUIRED ATTRS
$(this).find('.required').attr('required', false);
// ADD THEM BACK TO THE CURRENT FIELDSET
// I'M JUST USING A CLASS TO IDENTIFY REQUIRED FIELDS
$(this).find('fieldset.current .required').attr('required', true);
$(this).submit(function(){
var current = $(this).find('fieldset.current')
var next = $(current).next()
// MOVE THE CURRENT MARKER
$(current).removeClass('current');
$(next).addClass('current');
// ADD THE REQUIRED TAGS TO THE NEXT PART
// NO NEED TO REMOVE THE OLD ONES
// SINCE THEY SHOULD BE FILLED OUT CORRECTLY
$(next).find('.required').attr('required', true);
});
});
I seem to find the trick:
Just remove the form target attribute, then use a submit button to validate the form and show hints, check if form valid via JavaScript, and then post whatever. The following code works for me:
<form>
<input name="foo" required>
<button id="submit">Submit</button>
</form>
<script>
$('#submit').click( function(e){
var isValid = true;
$('form input').map(function() {
isValid &= this.validity['valid'] ;
}) ;
if (isValid) {
console.log('valid!');
// post something..
} else
console.log('not valid!');
});
</script>
Html Code:
<form class="validateDontSubmit">
....
<button style="dislay:none">submit</button>
</form>
<button class="outside"></button>
javascript( using Jquery):
<script type="text/javascript">
$(document).on('submit','.validateDontSubmit',function (e) {
//prevent the form from doing a submit
e.preventDefault();
return false;
})
$(document).ready(function(){
// using button outside trigger click
$('.outside').click(function() {
$('.validateDontSubmit button').trigger('click');
});
});
</script>
Hope this will help you
For input field
<input id="PrimaryPhNumber" type="text" name="mobile" required
pattern="^[789]\d{9}$" minlenght="10" maxLength="10" placeholder="Eg: 9444400000"
class="inputBoxCss"/>
$('#PrimaryPhNumber').keyup(function (e) {
console.log(e)
let field=$(this)
if(Number(field.val()).toString()=="NaN"){
field.val('');
field.focus();
field[0].setCustomValidity('Please enter a valid phone number');
field[0].reportValidity()
$(":focus").css("border", "2px solid red");
}
})
$('#id').get(0).reportValidity();
This will trigger the input with ID specified. Use ".classname" for classes.
When there is a very complex (especially asynchronous) validation process, there is a simple workaround:
<form id="form1">
<input type="button" onclick="javascript:submitIfVeryComplexValidationIsOk()" />
<input type="submit" id="form1_submit_hidden" style="display:none" />
</form>
...
<script>
function submitIfVeryComplexValidationIsOk() {
var form1 = document.forms['form1']
if (!form1.checkValidity()) {
$("#form1_submit_hidden").click()
return
}
if (checkForVeryComplexValidation() === 'Ok') {
form1.submit()
} else {
alert('form is invalid')
}
}
</script>
Another way to resolve this problem:
$('input').oninvalid(function (event, errorMessage) {
event.target.focus();
});