I want to test calling an API in a custom function for Google Sheets. code.gs is as follows:
function TApi(input) {
var url = "https://api.nytimes.com/svc/search/v2/articlesearch.json";
url += '?' + $.param({
'api-key': "cdaa59fea5f04f6f9fd8fa551e47fdc4",
'q': "MIT"
});
$.ajax({
url: url,
method: 'GET',
}).done(function(result) {
return result;
console.log(result);
}).fail(function(err) {
throw err;
});
}
But when I call =TAPI() in a sheet cell, it returns an error ReferenceError: "$" is not defined. (line 22). I guess we need to add a link to JQuery. Does anyone know how to do this?
You can only use JQuery on client side scripts which use the HTML service. It is not available server side. There is a blurb about using it in the HTML Services Best Practices.
It's not possible. You must build either a web app or custom UI (sidebar or dialog) using HtmlService and do the processing on the client. Because your code runs on Google servers, there are no 'window' or 'document' objects. DOM and BOM are only accessible on the client.
In fact, feel free to do the following little experiment. Open your browser console (I'm using Chrome developer tools) and type in
console.log(this); //this logs global object
Here's the output
This is the 'window' object used by jQuery for navigating the DOM tree. jQuery is simply a JS library that builds on top of existing DOM manipulation methods and CSS selectors.
Next, open any GAS file, run the following function and check the Logs (Ctrl + Enter):
function test() {
Logger.log(this);
}
And here's the output.
As you can see, the global object in this context consists of Google-defined pseudo classes (GAS services).
You can use urlFetch app. Try the below snippet
function fetchURL() {
try {
var url = "https://api.nytimes.com/svc/search/v2/articlesearch.json";
url += '?api-key=cdaa59fea5f04f6f9fd8fa551e47fdc4&q=MIT';
var params = {
'method': 'get',
'contentType': 'application/json',
'muteHttpExceptions': true
}
var response = UrlFetchApp.fetch(url, params);
Logger.log(response)
} catch (e) {
Logger.log(e)
}
}
Related
After much discussion and R&D, image cropping is not possible with Google APP scripts. So I decided to try one using the Canvas API.
I am trying to pass the value from server script(.gs) to the HTML file and get back the value in the server side script without opening HTML output as in sidebar or model/modelLess dialog box. You can say silently call HTML, complete the process and return the value to server script method.
I tried but getFromFileArg() is not running when i am running the callToHtml().
Is this possible with below script? what you will suggest?
Server side (.gs)
function callToHtml() {
var ui = SlidesApp.getUi();
var htmlTemp = HtmlService.createTemplateFromFile('crop_img');
htmlTemp["data"] = pageElements.asImage().getBlob();
var htmlOutput = htmlTemp.evaluate();
}
function getFromFileArg(data) {
Logger.log(data);
}
crop_img.html template :
<script>
var data = <?= data ?>;
//call the server script method
google.script.run
.withSuccessHandler(
function(result, element) {
element.disabled = false;
})
.withFailureHandler(
function(msg, element) {
console.log(msg);
element.disabled = false;
})
.withUserObject(this)
.getFromFileArg(data);
</script>
You cannot "silently" call the HTML this way, no.
The HTML needs to go to the user and the user is not inside of your web app, but Google's web app (Slides), so you have to play by their rules.
You need to use one of the available UI methods such as showSidebar. You could have the displayed HTML be a spinner or message like "processing..." while the JavaScript runs.
function callToHtml() {
var ui = SlidesApp.getUi();
var htmlTemp = HtmlService.createTemplateFromFile('crop_img');
htmlTemp["data"] = pageElements.asImage().getBlob();
ui.showSidebar(htmlTemp.evaluate());
}
I run a Google Apps script that uploads a file to the user's Google Drive file:
function doGet(e) {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
return HtmlService.createHtmlOutput("DONE!");
}
My site loads a popup window that runs a Google Apps Script with that code. Works fine.
Now, how do I communicate back to my site that they user has successfully uploaded the file? As in, how can I communicate back to my server that the user has run doGet()?`
Some type of response handling must exist?
Full working code (test it out on JSBin):
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
</head>
<body>
<div class="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">
<span style="background-color: #ddd">Upload</span>
</div>
<script>
$(function() {
$(".google-upload").click(function() {
var url = "https://script.google.com/macros/s/AKfycbwsuIcO5R86Xgv4E1k1ZtgtfKaENaKq2ZfsLGWZ4aqR0d9WBYc/exec"; // Please input the URL here.
var withQuery = url + "?url=";
window.open(withQuery + $('.google-upload').attr("data-url"), "_blank", "width=600,height=600,scrollbars=1");
});
});
</script>
</body>
</html>
So to clarify, I want a way to find out whether if the user has successfully uploaded the file. Something like:
request.execute(function(response) {
if (response.code == 'uploaded') {
// uploaded, do stuff
} else {
// you get the idea...
}
});
Adding a bounty for a complete solution to this.
Rather than returning a HtmlService object, you can pass data using jQuery's $.getJSON method and retrieve data from the doGet function with ContentService. Google Apps Script does not accept CORS, so using JSONP is the best way to get data to and from your script. See this post for more.
Working CodePen Example
I split your HTML and scripts for clarity. None of the HTML changed from your original example.
Code.gs
function doGet(e) {
var returnValue;
// Set the callback param. See https://stackoverflow.com/questions/29525860/
var callback = e.parameter.callback;
// Get the file and create it in Drive
try {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
// Structure this JSON however you want. Parsing happens on the client side.
returnValue = {status: 'okay'};
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
returnValue = {status: e.message}
}
// Returning as JSONP allows for crossorigin requests
return ContentService.createTextOutput(callback +'(' + JSON.stringify(returnValue) + ')').setMimeType(ContentService.MimeType.JAVASCRIPT);
}
Client JS
$(function() {
$(".google-upload").click(function() {
var appUrl = "https://script.google.com/macros/s/AKfycbyUvgKdhubzlpYmO3Marv7iFOZwJNJZaZrFTXCksxtl2kqW7vg/exec";
var query = appUrl + "?url=";
var popupUrl = query + $('.google-upload').attr("data-url") + "&callback=?";
console.log(popupUrl)
// Open this to start authentication.
// If already authenticated, the window will close on its own.
var popup = window.open(popupUrl, "_blank", "width=600,height=600,scrollbars=1");
$.getJSON(popupUrl, function(returnValue) {
// Log the value from the script
console.log(returnValue.status);
if(returnValue.status == "okay") {
// Do stuff, like notify the user, close the window
popup.close();
$("#result").html("Document successfully uploaded");
} else {
$("#result").html(returnValue);
}
})
});
});
You can test the error message by passing an empty string in the data-url param. The message is returned in the console as well as the page for the user.
Edit 3.7.18
The above solution has problems with controlling the authorization flow. After researching and speaking with a Drive engineer (see thread here) I've reworked this into a self-hosted example based on the Apps Script API and running the project as an API executable rather than an Apps Script Web App. This will allow you to access the [run](https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run) method outside an Apps Script web app.
Setup
Follow the Google Apps Script API instructions for JavaScript. The Apps Script project should be a standalone (not linked to a document) and published as API executable. You'll need to open the Cloud Console and create OAuth credentials and an API key.
The instructions have you use a Python server on your computer. I use the Node JS server, http-server, but you can also put it live online and test from there. You'll need to whitelist your source in the Cloud Console.
The client
Since this is self hosted, you'll need a plain HTML page which authorizes the user through the OAuth2 API via JavaScript. This is preferrable because it keeps the user signed in, allowing for multiple API calls to your script without reauthorization. The code below works for this application and uses the authorization flow from the Google quickstart guides.
index.html
<body>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize-button" style="display: none;">Authorize</button>
<button id="signout-button" style="display: none;">Sign Out</button>
<button onclick="uploadDoc()" style="margin: 10px;" id="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">Upload doc</button>
<pre id="content"></pre>
</body>
index.js
// Client ID and API key from the Developer Console
var CLIENT_ID = 'YOUR_CLIENT_ID';
var API_KEY = 'YOUR_API_KEY';
var SCRIPT_ID = 'YOUR_SCRIPT_ID';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://script.googleapis.com/$discovery/rest?version=v1"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/script.external_request';
var authorizeButton = document.getElementById('authorize-button');
var signoutButton = document.getElementById('signout-button');
var uploadButton = document.getElementById('google-upload');
var docUrl = uploadButton.getAttribute('data-url').value;
// Set the global variable for user authentication
var isAuth = false;
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
// uploadButton.onclick = uploadDoc;
});
}
/**
* Called when the Upload button is clicked. Reset the
* global variable to `true` and upload the document.
* Thanks to #JackBrown for the logic.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn && !isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block'
uploadButton.onclick = uploadDoc;
} else if (isSignedIn && isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block';
uploadDoc();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
uploadButton.style.display = 'none';
isAuth = false;
}
}
/**
* Sign in the user upon button click.
*/
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
isAuth = true; // Update the global variable
}
/**
* Sign out the user upon button click.
*/
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
isAuth = false; // update the global variable
}
/**
* Append a pre element to the body containing the given message
* as its text node. Used to display the results of the API call.
*
* #param {string} message Text to be placed in pre element.
*/
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
/**
* Handle the login if signed out, return a Promise
* to call the upload Docs function after signin.
**/
function uploadDoc() {
console.log("clicked!")
var docUrl = document.getElementById('google-upload').getAttribute('data-url');
gapi.client.script.scripts.run({
'scriptId':SCRIPT_ID,
'function':'uploadDoc',
'parameters': [ docUrl ]
}).then(function(resp) {
var result = resp.result;
if(result.error && result.error.status) {
// Error before the script was Called
appendPre('Error calling API');
appendPre(JSON.parse(result, null, 2));
} else if(result.error) {
// The API executed, but the script returned an error.
// Extract the first (and only) set of error details.
// The values of this object are the script's 'errorMessage' and
// 'errorType', and an array of stack trace elements.
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
// There may not be a stacktrace if the script didn't start
// executing.
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
// The structure of the result will depend upon what the Apps
// Script function returns. Here, the function returns an Apps
// Script Object with String keys and values, and so the result
// is treated as a JavaScript object (folderSet).
console.log(resp.result)
var msg = resp.result.response.result;
appendPre(msg);
// do more stuff with the response code
}
})
}
Apps Script
The Apps Script code does not need to be modified much. Instead of returning using ContentService, we can return plain JSON objects to be used by the client.
function uploadDoc(e) {
Logger.log(e);
var returnValue = {};
// Set the callback URL. See https://stackoverflow.com/questions/29525860/
Logger.log("Uploading the document...");
try {
// Get the file and create it in Drive
var blob = UrlFetchApp.fetch(e).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
var msg = "The document was successfully uploaded!";
return msg;
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
return e.message
}
}
I had a hard time getting CodePen whitelisted, so I have an example hosted securely on my own site using the code above. Feel free to inspect the source and take a look at the live Apps Script project.
Note that the user will need to reauthorize as you add or change scopes in your Apps Script project.
In my app I have a namespaced application and there's information or metadata myApp carries on it that might be useful to devpane.
window.myApp = new App();
How can I relay or send the following information to the devtool.js?
window.myApp.metadata; // information
And can I send a request from the devtool with a function that customizes the serialization of that metadata?
I've seen similar posts with the solution below, which returns null when I tried it.
chrome.devtools.inspectedWindow.eval("window.myApp", {
useContentScriptContext: true
})
NOTE: If a sample template can be provided that would be wonderful.
This is how I've solved this. It feels more complicated than necessary, but it does work.
In the context of the inspected window
Based on this question.
This is where you've got access to window.myApp.metadata and can put it into the data object.
var event = new CustomEvent("RebroadcastExtensionMessage", {data: ""});
window.dispatchEvent(event);
In the content script
This just forwards the data to the background page.
window.addEventListener("RebroadcastExtensionMessage", function(evt) {
chrome.runtime.sendMessage(evt)
}, false);
In the background page
Based on the Chrome docs.
chrome.runtime.onConnect.addListener(function(devToolsConnection) {
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
devToolsConnection.postMessage(request)
});
})
In devtools.js
var backgroundPageConnection = chrome.runtime.connect({
name: "devtools-page"
});
backgroundPageConnection.onMessage.addListener(function (message) {
// Data has arrived in devtools page!!
});
I'm trying to get JSONP working with a server running on an Arduino.
This is my JS code:
window.onload = init;
function init()
{
//alert("Test");
SendRequest();
}
function SendRequest()
{
alert("Sending request");
var url = "http://192.168.1.177";
var request = new XMLHttpRequest();
request.open("GET", url);
request.error = function(e) {
alert("ERROR");
};
request.send(null);
}
function ArduinoJSONP()
{
alert("Callback received!!!");
}
The callback function is never reached.
But if I just direct my browser directly to the Arduino IP I see the following displayed in the browser:
ArduinoJSONP({"data": 12345})
So it seems the server is sending the response with correct JSONP format but somehow the function is not invoked. Is there anything else I need to for JS to call the function? I even tried moving the function to the HTML body but it didn't help either.
Thank you.
You are not handling server response at all. If you doing it without any libraries you need to eval result that was returned by the server.
And actually JSONP implementation is not XHR, you have to inject it as a script tag into html with correct src attribute.
Just use a library that already have all this logic abstracted for you.
Simply inject script tag into HTML tree:
function SendRequest()
{
var element = document.createElement('script');
var s = document.getElementsByTagName('script')[0];
element.type = 'text/javascript';
element.async = true;
element.src = 'http://192.168.1.177';
s.parentNode.insertBefore(element, s);
}
You can mark it with unique id. Hookup to onload event and once executed remove that script.
The JavaScript example to list files of Google Drive uses gapi.client.drive.files.list().
When trying to use this method I get the error "Cannot read property 'files' of undefined".
The issue and a workaround are already described under Google Drive API javascript
Is the documentation under https://developers.google.com/drive/v2/reference/files/list#try-it incorrect? Or is there a way to use the API as described.
The JavaScript example is correct, but you have to make sure that you only use gapi.client.drive.files (and the other Drive-specific resources) when the Drive library is loaded, i.e. after:
gapi.client.load('drive', 'v2', callback);
If it works fine after writing gapi.client.load('drive', 'v2', callback), then all good. In my case it does not work so I wrote below code.
gapi.load('client', function () {
gapi.client.load('drive', 'v2', function () {
var file = gapi.client.drive.files.get({ 'fileId': fileId });
file.execute(function (resp) {
//Write you code with resp variable
});
});
});