Functions are not being executed as intended. I expect the page to switch to 'setup' but that doesn't happen - javascript

I'm creating a very basic log-in function on GAS HTML. In my HTML, I have a form that collects and stores user input with assigned variables (fn, ln, em, pw). It then sends the info to the sheet (which has formulas that verify the credentials) then a certain cell ('E2') will display "Found" or "Not Found". Then it will print the name in cell ('E1') if the if statement is false. This part does execute correctly, but the 2 functions, render('setup') and notFound() will not execute (regardless of when the if statement is true or false, neither one works.
I know the render('setup') function works perfectly fine in the doGet and when it is called in other functions.
function signIn(fn,ln,em,pw) {
var url = 'www.somelink.com'
var ss = SpreadsheetApp.openByUrl(url);
var login = ss.getSheetByName('login');
var fname = login.getRange('F1');
var lname = login.getRange('G1');
var email = login.getRange('H1');
var pword = login.getRange('I1');
fname.setValue(fn);
lname.setValue(ln);
email.setValue(em);
pword.setValue(pw);
var check = login.getRange('E2').getDisplayValue();
var name = login.getRange('F2').getDisplayValue();
if (check === "Not Found"){
return notFound(); // THIS DOESN'T EXECUTE
} else {
login.getRange('E1').setValue(name);
return render('setup'); // AND THIS DOESN'T EXECUTE
}
}
EDIT: HTML for the call signIn function.
<div class="button" align="center">
<button id="submit" class="btn waves-effect waves-light" onclick="var fn = document.getElementById('first_name').value;
var ln = document.getElementById('last_name').value;
var em = document.getElementById('email').value;
var pw = document.getElementById('password').value;
google.script.run.signIn(fn,ln,em,pw)">Submit
<i class="material-icons right">send</i>
</button>

If I understand you correctly, you want your web app to redirect the user to another HTML page after submitting the values, if some conditions are met.
You cannot do this by just calling a server-side function from the client-side: google.script.run is used to retrieve data that is not available on the client, but not to render a different HTML file.
What you could do instead is the following:
Instead of returning a function (like render or notFound), return a parameter that can be used on the client-side. For example, you could do:
function signIn(fn,ln,em,pw) {
// Rest of your code
if (check === "Not Found") {
return "notFound";
} else {
return "setup";
}
}
Your client-side function (which I've called callSignIn) can then pass this returned parameter to another function called redirect, thanks to withSuccessHandler(function) (this handler is necessary since google.script.run.signIn(fn,ln,em,pw) by itself will always return void):
function callSignIn() {
var fn = document.getElementById('first_name').value;
// Rest of your code
google.script.run.withSuccessHandler(refresh).signIn(fn,ln,em,pw);
}
Next, the value returned by signIn is passed as an argument of the function refresh, which refreshes the web app with window.open if the argument is setup, all the while passing setup as a query parameter (pageRedirect), which can later be used by the event parameter (you can retrieve the web app URL dynamically from the server-side instead of writing it manually — see this answer):
function refresh(redirectPage) {
if (redirectPage === "setup") {
window.open("https://script.google.com/macros/s/{your-web-app-id}/exec?pageRedirect=" + redirectPage, "_top");
} else {
// Whatever you want to do if "notFound"
}
}
Finally, back to the server-side, the function doGet gets called by the GET request from window.open. This function can check if there is a query parameter in the request via e.parameter, thanks to the event object, and render a different page depending on this parameter:
function doGet(e) {
if (!e.parameter.pageRedirect) {
return HtmlService.createHtmlOutputFromFile("index");
} else {
return HtmlService.createHtmlOutputFromFile(e.parameter.pageRedirect)
}
}
Reference:
Class google.script.run (Client-side API)
Window.open()
Web Apps: Request parameters

Related

How can I pass a server-side variable to HTML and have the HTML page return an auto-filled form?

I am building an HTML page in Google Apps Script with CRUD functionality. The user can currently add data, edit data, and search data (I am not adding a delete feature). I would like the user to receive the form url link with an ID that when they go BACK to that link, it auto-fills the form with the previously added data.
In my HTML file, I have the following button defined:
document.getElementById("sbtn").addEventListener("click",getTID);
Once a user has entered data, it gets sent to a Google Sheet. The user HAS to enter a unique ID that they've already been provided. Using this ID, they can enter it, hit search, and it runs getTID():
function getTID() { //TID CODE
var transID = document.getElementById("tid").value;
if (transID.length === 36) {
google.script.run.withSuccessHandler(updateAllData).getID(transID);
} else {
alert("Transaction ID is not long enough.\nPlease copy the Transaction ID EXACTLY!\n\nFor Example: https:/workwebsiteconcealedforprivacy/w?txid=36275284-2ed6-4868-97b2-16bc1fde1a08\n\nThe Transaction ID is: 36275284-2ed6-4868-97b2-16bc1fde1a08")
}
}
This takes the ID they gave, references the spreadsheet and then returns values it found by index. Now, I have in my server-side GS file, the following in doGet:
var urlValue = '';
function doGet(e) {
// Test Code
var ss = SpreadsheetApp.openById(id);
var ws = ss.getSheetByName("Options");
var list = ws.getRange(1, 1, ws.getRange("A1").getDataRegion().getLastRow(), 1).getValues();
var htmlListArray = list.map(function (r) { return '<option>' + r[0] + '</option>'; }).join('');
var title = "Please Work";
var vals = JSON.stringify(e);
if ('v' in e.parameter){
urlValue = String(e.parameter['v']);
//return HtmlService.createHtmlOutput(urlValue);
}
return render("page",{list: htmlListArray, title});
and the following:
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
function render(file, argsObject) {
var tmp = HtmlService.createTemplateFromFile(file);
if (argsObject) {
var keys = Object.keys(argsObject);
keys.forEach(function (key) {
tmp[key] = argsObject[key]
});
}
return tmp.evaluate();
}
If I uncomment the return HtmlService.createHtmlOutput(urlValue); line, I can see that IF an ID is in the URL, it returns the correct parameter.
My problem is that I cannot get the HTML to read the urlValue variable and autorun getTID() when the user enters the url with the correct parameter. The correct functionality is that IF the parameter is found, it auto populates the HTML form. If it doesn't, it returns the blank form.
There is an error on
return render("page",{list: htmlListArray, title});
On {list: htmlListArray, title} the name of the second property is missing.
To "read the urlValue variable" there are two options:
pass the parameters from the URL using the event object of the doGet function. For this you have two options, create the HtmlService.HtmlOutput object from an html string generated using "vanilla" JavaScript or create it from a HtmlService.HtmlTemplate object.
get the parameters from the URL directly on the client-side code using google.script.url.getLocation .
If you go for the first option, then you should pass someway the urlValue to the render function. In the question code urlValue is a global variable, so you might add the following before the render's return statement.
tmp.urlValue = urlValue;
Then you have to add a scriptlet on the html file to handle this value and "autorun" getTID. Scriptlets is a feature of Templated HTML.

Get element of a newly opened & loaded window - Javascript

While being on a twitter account's page (let's say StackOverflow: https://twitter.com/stackoverflow), I'm trying to get the username of the account, then open a new window querying the account's username on google search. Finally I'd like to get the query text of the newly opened tab.
To do so, I did the following:
function getUserName(callback) {
url = window.location.href;
console.log(url);
sn = url.split("/").slice(-1)[0];
console.log(sn);
window.location.href = `https://google.com/search?q=${sn}`;
callback();
};
function getQuery() {
console.log(window.location.href);
}
getUserName(getQuery);
The problem is that it doesn't wait for the new page to be loaded and thus the console.log() form getQuery() is the twitter's one, instead of the google's.
I know this is a matter of callback and await/async, I've been reading a lot about it but those subjects are confusing me.
Just pass https://google.com/search?q=${sn} into a global variable and use that.
Like this:
let windowHref = window.location.href;
function getUserName(callback) {
url = window.location.href;
console.log(url);
sn = url.split("/").slice(-1)[0];
console.log(sn);
windowHref = `https://google.com/search?q=${sn}`;
window.location.href = windowHref;
callback();
};
function getQuery() {
console.log(windowHref);
}
getUserName(getQuery);
As far as getting the query text of the newly opened tab. You need make use of the callback function within your getUserName() function.
function getUserName(callback) {
url = window.location.href;
console.log(url);
sn = url.split("/").slice(-1)[0];
console.log(sn);
windowHref = `https://google.com/search?q=${sn}`;
callback(windowHref); // Sending back the url to the callback as parameter
};
function getQuery(queryText) { // Value returned from the callback.
console.log(queryText);
}
getUserName(getQuery);
I tried your scenario with linkedin: Sharing the snippets below:

ServiceNow Client Script asynchronous query to stop submission onSubmit in new Service Portal

I have a Client Script that performs a GlideRecord query to check if a record already exists with the same name.
If a matching record is found, we need to STOP the form from being submitted.
We have this working fine on the CMS portal.
However, the new Service Portal does not support synchronous GlideRecord query.
So I can't use gr.query() I need to use a callback such as gr.query(callback).
The issue is that since the callback is asynchronous, it does not actually stop the form from being submitted!
g_form.submitted = false; DOES NOT work. That's because the script proceeds along to submit the form before the callback has a chance to retrieve the value.
How can we stop the submission of a form based on the value returned by an asynchronous callback? We can't use GlideAjax for the same reason, getXMLWait() is no longer supported.
Here is the client script that I am trying to get working in the new Service Portal.
function onSubmit() {
var group_name = g_form.getValue('u_group_name');
g_form.hideAllFieldMsgs('error');
/*Check if group already exists*/
var rec = new GlideRecord('u_auth_group');
rec.addQuery('u_group_name', u_group_name);
rec.query(getAccountResponse);
}
function getAccountResponse(rec) {
while (rec.next()) {
g_form.showFieldMsg('u_group_name', " Group Name exists already, please select another group name",'error');
g_form.submitted = false; //DOES NOT STOP THE FORM FROM BEING SUBMITTED
return false;
}
}
Here is the existing script that works in the CMS portal.
function onSubmit() {
var group_name = g_form.getValue('u_group_name');
g_form.hideAllFieldMsgs('error');
/*Check if group already exists*/
var rec = new GlideRecord('u_auth_group');
rec.addQuery('u_group_name', u_group_name);
rec.query(getAccountResponse);
while (rec.next()) {
g_form.showFieldMsg('u_group_name', " Group Name exists already, please select another group name",'error');
g_form.submitted = false; //Stops the form from being submitted if a result is returned
return false;
}
}
We're on Helsinki Patch 5 so we're going through similar growing pains. We've had luck using the following structure. There are still Glide System resources available Server Side, including Glide Record.
You might try wrapping your Submit action inside of a custom event handler.
Try:
Client side:
c.createGroup = function(groupName){
return c.server.get({
grpname : groupName
}.then(function(response){
if (response.data.result == true){
//don't submit
}
else{
//submit
}
}
Server Side
data.result = false
data.grpname = input.grpname
function checkGroupExists(data.grpname){
/*Check if group already exists*/
var rec = new GlideRecord('u_auth_group');
rec.addQuery('u_group_name', data.grpname);
rec.limit(1); //you only need to find one match
rec.query()
while (rec.next()){
data.result = true
}
}
Then you can bind this event handler to some action in the UI.
I was able to solve this by using an asyc callback with the glide record query.
function onSubmit() {
//If ServicePortal
if (!window) {
if (g_scratchpad.isFormValid) {
return true;
}
g_form.hideAllFieldMsgs("error");
var actionName = g_form.getActionName();
//Group Name contain letters numbers and dashes only
var group_name = g_form.getValue("u_group_name");
//Group name regular expression
var regGrpName = /^([A-Za-z0-9\-]+)$/;
//Check name against regular expression
validGroupName = regGrpName.test(group_name);
//Check if google group already exists
var gg = new GlideRecord("u_system_group");
gg.addQuery("u_group_name", group_name);
//Callback function to control stop submit asynchronously
gg.query(function() {
while (gg.next()) {
g_form.showFieldMsg("u_group_name","Group Name " + gg.u_group_name + " already exists! Please enter a different group name.", "error", true);
return false;
}
g_scratchpad.isFormValid = true;
g_form.submit(actionName);
});
return false;
}
}

getting responseID of latest form submission in google apps

I have a google form. Every time it is submitted, the answers go into a google spreadsheet. I am trying to write an app script that triggers every time the form is submitted, and adds an "edit" link in the column to the right of the data from the form. The link itself is easy to generate, google has a method called getEditResponseURL(). (https://developers.google.com/apps-script/reference/forms/form-response)
But everytime I run it, I am getting the error "TypeError: Cannot call method "getResponses" of null."
Here is my code:
function addeditlink(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
//get the form
var form = FormApp.getActiveForm();
//get latest form response
var responses = form.getResponses();
var lastResponse = responses[responses.length - 1];
//get edit URL
var editurl = lastResponse.getEditResponseUrl();
//build link
var editlink = "Edit";
//place edit link in column R (index 18)
if (sheet.getRange(row, 18).getValue() == "") {
sheet.getRange(row, 18).setValue(editlink);
}
}
Any help? Thanks!
The solution is to remove:
var form = FormApp.getActiveForm(); //this is WRONG
and replace with:
var form = FormApp.openByID(' ID here ')
There is no "active form", because this script is being run in sheets, not forms.
I think you can only call FormApp.getActiveForm() from a script attached to a form, whereas your script is contained in a GSheet. I couldn't find a way to easily gets forms that used this sheet as its destination so what I've did was get all of the forms and then looked at the destination id of each and checked if it is the same as this spreadsheet. Once you've got your Form object you can get the responses. Feels a bit long winded would love to know if anyone knows a quicker way.
There are also a few exceptions that FormApp throws that you have to cope with.
Here's the function I use:
/**
* Find the first form that is linked to a specific spreadsheet
*
* #param {string} spreadsheet id
* #return {object} Form or null
*/
function getFormByDestinationId_(spreadsheetId) {
var formFiles = DriveApp.getFilesByType('application/vnd.google-apps.form');
var form;
var formFile;
var formId;
var destinationId;
while (formFiles.hasNext()) {
formFile = formFiles.next();
formId = formFile.getId();
// Throws an error if ID invalid
try {
form = FormApp.openById(formId);
} catch (error) {
if (error.name === "Exception") {
// Just ignore it
} else {
throw error;
}
}
// Form.getDestinationId() throws an error if there is no destination id
try {
destinationId = form.getDestinationId();
} catch (error) {
if (error.name === "Exception") {
// Just ignore it
} else {
throw error;
}
}
if (destinationId !== spreadsheetId) {
continue;
}
return form;
}
return null;
} // getFormByDestinationId_()
The only line using: getResponses() method is this one:
var responses = form.getResponses();
Your error:
Cannot call method "getResponses" of null
Means that form is null. If form is null, then this line:
//get the form
var form = FormApp.getActiveForm();
is not working. So, why isn't it working? There is nothing wrong with the code, so it must be a different problem. If there was an active form, that code would return a form type. This means that there is no form bound to the script. getActiveForm()
Returns the form to which the script is container-bound.
Your script is not "container-bound" to the form. Your script is bound to the spreadsheet.
The documentation states:
To interact with forms to which the script is not container-bound, use openById(id) or openByUrl(url) instead.
You can bind your script to the form by opening the script editor from the edit page of the form. But, there's no need to do that if you want to keep your script bound to the spreadsheet.
The line var form = FormApp.getActiveForm(); isn't going to work in your spreadsheet script.
The problem with using the Event Object e with an installable trigger, is that it looks like you can't get the response URL.
google_sheets_events
This means that you need to use openById(id) or openByUrl(url) inside the script bound to the spreadsheet, or move all your script to the form.
Here is how to get the edit url from script in the spreadsheet:
// Open a form by ID.
var form = FormApp.openById('1234567890abcdefghijklmnopqrstuvwxyz');
Now the problem is, that you can only get the Edit Response URL: getEditResponseUrl() through the "FormResponse" class. So you need the Form Responses.
var formResponses = form.getResponses();
But that's all the responses, you need the last one.
var lastResponseIndex = formResponses.length - 1;
var lastResponse = formResponses[lastResponseIndex];
var editURL = lastResponse.getEditResponseUrl();
or:
function getEditURLofLastResponse() {
// Open a form by ID.
var form = FormApp.openById('Your Form ID');
var formResponses = form.getResponses();
//get last respnse
var lastResponseIndex = formResponses.length - 1;
var lastResponse = formResponses[lastResponseIndex];
var editURL = lastResponse.getEditResponseUrl();
Logger.log(editURL);
}
Just an observation:
You are using an e argument: function addeditlink(e) {. But I don't see it being used in your code. That makes me wonder if you are using an "installable" trigger, as opposed to a "simple" trigger.
It's possible to get the values that were just submitted with e.values or e.namedValues. But you can't get the Edit URL with the Event Object.

creating a drop down list in javascript from a text file

Is this possible? If so, I could really use some help on this. I'm very new to JavaScript and thus hardly know any of the syntactical specifications, nor proper methodology.
Some functions I wrote in an external file.
var base = base || {};
base.requestAjax = function () {
try{
return new XMLHttpRequest();
} catch (e) {
try {
return new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
return new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
alert("Something is wrong with your browser! Please ensure that you have javascript functionality enabled");
return false;
}
}
}
}
base.onReadyStateChange = function(ajaxRequest, formName, formArray, method, controller) {
var methodVerified = verifyMethod(method);
if (!methodVerified) {
throw new Exception("Error: please make sure the method passed matches that of \'GET\' or \'POST\'.");
}
for (input in formArray) {
document.formName.input.value = ajaxRequest.responseText;
}
ajaxRequest.open(method, controller, true);
file.send(null);
}
base.writeDropDownList = function(file, method, path) {
var file = upload.requestAjax();
file.open(method, path, true);
if (file.readyState != 4) {
throw new Exception("Error: text file ready state not equal to 4!");
}
if (file.status != 200) {
throw new Exception("Error: text file status not equal to 200!");
}
var lines = file.responseText.split("\n");
document.writeln("<select>");
for(line in lines) {
document.writeln("<option>" + line + "</option>");
}
document.writeln("</select>");
file.send(null);
}
base.verifyMethod = function(method) {
var methodLower = method.toString().toLowerCase();
switch(methodLower) {
case "get":
case "post":
return true;
default:
return false;
}
}
The html code which attempts to implement it
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="../includes/css/adsmanager.css" />
<script type="text/javascript" src="../includes/js/base.js">
<!--
function createCountriesList() {
var file = base.requestAjax;
var path = "../includes/_notes/countries.txt";
base.writeDropDownList(file, "GET", path);
}
//-->
</script>
</head>
<body>
<form name='adsManager'>
<button type="text" value="test" onclick="createCountriesList()" />
</form>
</body>
</html>
I plan to use the functions for other things, so I figured I'd create a namespace. I found this as a reference and based most of my model off of it.
Thanks to all who can help.
What is your point of failure? Is your Javascript sending the Ajax request and receiving the response?
Is lines getting data in this line of your code?
var lines = file.responseText.split("\n");
If you are getting this far, iterate through lines and add options like this:
var select = document.getElementById('id');
select.options.add(new Option(lines[i]));
In your writeDropDownList method, I made a few changes:
added a method that will be called when your Ajax call has completed. In that method, you should check the response string and add the options to your select box
base.writeDropDownList = function(file, method, path) {
var file = upload.requestAjax();
file.open(method, path, true);
file.onreadystatechange = requestComplete;
file.send(null);
}
requestComplete()
{
if (file.readyState == 4)
{
if(file.readyState == 200)
{
var lines = file.responseText.split("\n");
//foreach loop to populate select
}
}
}
In your code, you are checking and using files.responseText before you have even sent the Ajax request at files.send(null)
EDIT:
Some more comments regarding your code:
In the createCountriesList function, you create file and assign
it a value by calling requestAjax. You then pass it to
writeDropDownList function, where you assign it a value again by
calling requestAjax. You see that this is redundant? There is no
need to create file in createCountriesList and pass it as an
argument. Create it just once in writeDropDownList.
in writeDropDownList you call upload.requestAjax(). What is
upload. I don't see you initializing upload anywhere. Do you mean
to call base.requestAjax()?
you have a function base.OnReadyStateChange but at no point are you
telling your AJAX request to call that function when state changes. See the code I posted above. The function I added called
requestComplete does that, and I tell the AJAX request to call it using file.onreadystatechange = requestComplete;
You set method to GET, yet you are not passing any GET values in your URL
in file.open(method, path, true);, path is supposed to be the URL of the script the AJAX request will call. You have set path to ../includes/_notes/countries.txt. An AJAX request cannot call a .txt file since they do not execute.
I just had a look at the source of your code, and it is all sorts of broken. Please do not use it.
What is countries.txt ? Are you attempting to populate a dropdown with a list of all countries, or some countries depending on user's input?
If the former, there is no need for Javascript / AJAX. You need to add PHP code in your html to populate the select box.
If the latter, your AJAX request should be sending the user input as a GET variable.
Some comments:
Very nice code; it's readable and looks neat.
I'd use a different name than base as namespace - for my liking, the word is too "common" . Chances are that you're going to define a variable base somewhere and it will break.
To create HTML from JavaScript, first create a small example in pure HTML to see how it should look like. After that, create a script which produces the same HTML using document.createElement().
Look at frameworks like jQuery or Prototype because they make many boring tasks much more simple.

Categories