(How) can I use the module pattern in Google Apps scripts? - google-apps-script

I tried to put my menu functions inside a module like this:
var mainmenuModule = function() {
return {
menuItemX: function() {...},
menuItemY: function() {...}
};
}();
But when I create a menu item, my function is not found:
{ name: "X", functionName: "mainMenuModule.menuItemX"}
I'm doing this inside a Spreadsheet container.

You can't currently do this but I see that Issue 1355 has been logged as an enhancement to support this for libraries. You can "star" the issue to receive updates.

Related

Display vertically an embedded combo chart

I am trying to display a bar chart combined to a line using an EmbeddedComboChartBuilder.
For this I'm using the following code and data:
function createChartSerie() {
var ss = SpreadsheetApp.openById('XXX...');
var sheet = ss.getSheetByName("sheet1");
var chartTableXRange = sheet.getRange("A1:A24");
var chartTableYRange = sheet.getRange("B1:C24");
var chart = sheet.newChart()
.setOption('useFirstColumnAsDomain', true)
.setOption('hAxis', {
title: 'X',
format: '#,##'
})
.setOption('vAxes', {0: {
title: 'Y', format: '#,##'
}})
.setChartType(Charts.ChartType.COMBO)
.addRange(chartTableXRange) // x axis
.addRange(chartTableYRange) //first y data
.setPosition(5, 5, 0, 0)
.setOption('orientation', 'vertical')
.setOption('series', {
0: {
type: 'line',
color: 'red'
},
1: {
type: 'bars',
color: 'blue'
}
})
.build();
sheet.insertChart(chart);
}
The data range used to display the combo chart:
A B C
1 0.0001 0
1.5 0.0009 0.0006
2 0.0044 0.0037
2.5 0.0175 0.0133
3 0.054 0.0236
3.5 0.1296 0.0533
4 0.242 0.0073
4.5 0.3522 0.2468
5 0.399 0.0843
5.5 0.3522 0.3352
6 0.242 0.2201
6.5 0.1296 0.0607
7 0.054 0.0256
7.5 0.0175 0.0006
8 0.0044 0.003
8.5 0.0009 0.0005
9 0.0001 0.0001
It seems that the option .setOption('orientation', 'vertical') has no effect as you can see in the following picture:
This option is working well when using with google visualization like you can see in this JSFiddle.
Answer
Unfortunately Sheets API does not support this option (see the embedded charts resource).
There seems to be a feature request in Google’s Issue Tracker. You can click the white start (☆) so Google knows that you want this to be done.
Workarounds
There are 2 workarounds: use Google Charts itself on a web (works with WebApps and modals), and generate the chart with Google Charts, convert the SVG on the page to PNG and embed that to the spreadsheet. Generating a PNG requires making a modal, so this set instructions will work for both workarounds.
Step 1: Make a page that uses Google Charts to make a chart
Get some mocking data and make your chart, design it to your taste, and make sure everything works fine.
Step 2: Show it using a modal
Add a new HTML file named Chart (you can change the name but you’ll need to change it in the code) and paste the code you made.
Once you have a mock chart, we need to show it using a modal. We first need to add a menu to the spreadsheet:
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Chart generator')
.addItem('Create and insert chart', 'showChart')
.addToUi()
}
Then we add the function that shows the modal:
function showChart() {
const html = HtmlService.createHtmlOutputFromFile('Chart')
.setWidth(900)
.setHeight(600)
SpreadsheetApp.getUi().showModalDialog(html, 'Chart')
}
You can also set up a WebApp with the same template:
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('Chart')
.setWidth(900)
.setHeight(600)
}
Step 3: Get the data from the spreadsheet
Now it’s time to remove the mock data and get the actual data. To do this we’ll use the google.script.run function to asynchronously load the data. This example uses asynchronous logic so it can load the charts library and the data in parallel.
google.charts.load('current', {'packages':['corechart']})
const readyPromise = toAsync(resolve => google.charts.setOnLoadCallback(resolve))
const dataPromise = toAsync((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getData()
})
toAsync is a helper function that makes sure to reject if there is an error that’s not handled by a failure callback:
function toAsync(callback) {
return new Promise((resolve, reject) => {
try {
callback(resolve, reject)
} catch(e) {
reject(e)
}
})
}
Then we need to add the function in Google Apps Script that actually reads the data from the spreadsheet:
function getData() {
return SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('Sheet1')
.getRange('A1:C18')
.getValues()
}
Then simply use both in the main function:
async function drawVisualization() {
const el = document.getElementById('chart_div')
await readyPromise
const data = google.visualization.arrayToDataTable(await dataPromise)
const options = { … }
new google.visualization.ComboChart(el).draw(data, options)
}
Make sure that the call handles the promise rejection otherwise you may silence errors unknowingly:
document.addEventListener('DOMContentLoaded', () => drawVisualization().catch(console.error))
(Optional) Step 4: Embedding chart as PNG
Google Charts generate SVGs. Unfortunately we cannot embed a SVG so we need to convert it to a PNG. The easiest way to do it is using an external library like canvg which allows us to draw an SVG into a canvas. Then we can export the canvas as PNG.
Add a canvas to your body and the library to your head.
Then you can draw on the canvas using (append to drawVisualization):
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
v = canvg.Canvg.fromString(ctx, el.getElementsByTagName('svg')[0].outerHTML)
v.start()
await v.ready()
This will draw it and wait until it’s done.
Now we need a way of sending the generated image to GAS so it can embed it. Thankfully we can get a base64 of the PNG and send it to there:
const base64 = canvas.toDataURL('image/png').split(';base64,')[1]
await toAsync((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.insertChart(base64)
})
And in Google Apps Script, we define insertChart:
function insertChart(base64png) {
const blob = Utilities.newBlob(
Utilities.base64Decode(base64png, Utilities.Charset.US_ASCII),
'image/png',
'chart.png'
)
SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('Sheet1')
.insertImage(blob, 5, 5, 0, 0)
}
(Optional) Step 5: Close modal and hide charts while being generated
Now that it’s being inserted, I like to automatically close the modal. To do so just append this to drawVisualization:
google.script.host.close()
I also like to not show the chart while being processed. A bit of CSS should be enough:
canvas, #chart_div {
visibility: hidden;
}
body {
overflow: hidden;
}
You cannot use display: none because then it will generate charts of the wrong size. Overflow is set to avoid the scroll bars into nothing.
You can also change the screen title and even a loading animation to the modal.
References
Custom Menus in Google Workspace (Google Developers)
Serve HTML as a Google Docs, Sheets, Slides, or Forms user interface (Google Developers)
HtmlService.createHtmlOutputFromFile (Google Developers)
Serve HTML as a web app (Google Developers)
google.script.run (Google Developers)
Range getValues() (Google Developers)
Utilities.newBlob(data, contentType, name) (Google Developers)
Utilities.base64Decode(encoded, charset) (Google Developers)
Sheet insertImage(blobSource, column, row) (Google Developers)
google.script.host close() (Google Developers)
Canvas toDataURL (MDN)
Promise (MDN)

how to trigger a function in vuejs after the page is loaded?

I am trying to trigger a function which hides or show the images on the basis of data i have written two function one which calls the api which is in created hook and second function which renders the image . The problem is how do i call that second function after the dom is loaded , right now when i am trying to call in the first function or created it is returning me error that css cannot be changed of null.I have tried using mounted function with newtick but its still firing the render_badges function first and hence values are null inside
created:function(){
this.loadlike()
},
methods:{
loadlike:function(){
var self = this
this.$http.get('/api/user_profile').then(function (res) {
self.tasksdata = res.body
self.badges = self.tasksdata.data2
console.log(self.badges)
console.log(this.tasksdata)
console.log(this.max)
})
},
getHumanDate : function (date) {
return moment(date, 'YYYY-MM-DD hh-mm-ss').locale("en-gb").format('LL');
},
render_badges:function(){
var self = this
var counter = 0;
self.badges.map(function(e){
counter ++;
console.log(counter)
if(counter <=self.max){
document.getElementById("i").style.display = "initial";
}
else{
document.getElementById("i").style.display = "none";
}
})
},
mounted: function () {
this.$nextTick(function () {
this.render_badges();
})
}
Rather than manipulating the DOM, you should use v-if on the element itself that turns it on or off based on data. It is a different way of thinking than direct DOM manipulation learned in the jQuery days. Read more in the Vue docs about conditional rendering.
If you are wanting to trigger once the DOM is available, use mounted()

Add JQuery reference to custom function

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)
}
}

How to send information from window to the devtool in chrome extension

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!!
});

HTML5 File upload with AngularJS

I'm trying to get angular to read the contents of a file that the user selects through an <input type="file" control. Even though angular does not have directives for file upload controls, it should be easy to fix that with a call to $apply:
function MyController($scope) {
$('#myFile').on('change', function() {
var that = this;
$scope.$apply(function() { $scope.files = that.files });
});
}
Unfortunately, the event is never fired. It's like the selector is unable to refer to the correct DOM element: even though the selector finds the element, the list of files is always empty. This also happens if i poke around with the js console. The DOM inspector instead has the file list among its properties.
It's driving me crazy, but the only way I've got it to work so far is to use an inline event handler that assigns to a global variable. Why is the jquery selector returning another item? Is there some template compilation mumbo-jumbo that angular does which confuses selectors?
Here is what I do:
http://plnkr.co/edit/JPxSCyrxosVXfZnzEIuS?p=preview
app.directive('filelistBind', function() {
return function( scope, elm, attrs ) {
elm.bind('change', function( evt ) {
scope.$apply(function() {
scope[ attrs.name ] = evt.target.files;
console.log( scope[ attrs.name ] );
});
});
};
});
template:
<input type="file" filelist-bind name="files"/>
<p>selected files : <pre>{{ files | json }}</pre></p>
This kind of task, you definitely want to make use of directive.
But I think that your main concern is how to access the selected file
objects and my example should clarify that.
If you are looking for file upload with angular you can use this plugin
https://github.com/danialfarid/angular-file-upload
It is basically a directive like tosh's answer that takes care of non-HTML5 browsers with FileAPI flash polyfill and has $http.uploadFile function to upload the actual file via AJAX.
This site uses Angular service for HTML5 File Upload. A simple way is to setup a controller which calls the service and updates the UI when the asynchronous call is completed.
controller:
myapp.controller('fileUploadCtrl', ['$scope', '$q', 'FileInputService', function ($scope, $q, FileInputService) {
$scope.fileInputContent = "";
$scope.onFileUpload = function (element) {
$scope.$apply(function (scope) {
var file = element.files[0];
FileInputService.readFileAsync(file).then(function (fileInputContent) {
$scope.fileInputContent = fileInputContent;
});
});
};
}]);
service:
myapp.service('FileInputService', function ($q) {
this.readFileAsync = function (file) {
var deferred = $q.defer(),
fileReader = new FileReader(),
fileReader.readAsText(file);
fileReader.onload = function (e) {
deferred.resolve(e.target.result);
};
return deferred.promise;
};
});
template:
Choose File <input type="file" onchange="angular.element(this).scope().onFileUpload(this)">
<br />
{{fileInputContent}}
Reference: You can find the full source code and reference on this site.