Trying out the new library feature in a Google spreadsheet. I have include a library with the identifier of "Test" and the library implements the function "foo()"
entering =Test.foo() into a spreadsheet cell gives the error : "unknown function name TEST.FOO"
If I create a function in my spreadsheet to wrap the library function:
function foo()
{
return Test.foo();
}
then use =foo() in my speadsheet cell, all is well. Creating wrapper functions for all library functions so they can be used in a spreadsheet cell makes using libraries less than ideal. Is there a way to call a library function from a spreadsheet cell?
There's not currently a way to call these library functions directly as a custom function from a cell. Creating a wrapper, as you've done, is the way to do this currently. If you'd like to be able to call library functions directly as custom functions, please raise that as an issue on the Issue Tracker.
Here is a workaround that allows you to call any library function if you paste in this one generic wrapper function. Then you can call this from the spreadsheet.
For example, if I had a library called MyLib with a function add(x, y) (pretend x is in cell A1 and y is in cell A2) I could call it like this:
=LIB_FUNC("MyLib", "add", A1, A2).
It's a little ugly but at least allows me to only have to paste this one function and then access any library function. Note that this depends on undocumented structure of the "this" object that is in scope when calling the wrapper function. Small chance this could break over time. Might see if I can publish this as an add on.
function LIB_FUNC(libraryName, functionName) {
var result;
var lib = this[libraryName];
var extraArgs = [];
if (lib) {
var func = lib[functionName];
if (func) {
if (arguments.length > 2) {
extraArgs = Array.apply(null, arguments).slice(2);
}
result = func.apply(this, extraArgs);
} else {
throw "No such function: " + functionName;
}
} else {
throw "No such library: " + libraryName;
}
return result;
}
Related
I have many urls I want to pull some info from with importxml on a google sheet.
I'm trying to get review scores of monitors from a website.
I want to create a function so that when I give it a cell as reference (that contains a product's url) it will create a string for an importxml function (with a fixed and proper xpath) and pass it to the cell it is called. (or an adjacent one)
function puan(x) {
var cellFunction = '=IMPORTXML("' + x + '";"//div[#id=\'puan\']//#data-percent")';
return cellFunction;
}
I tried something like this but it didn't work. It just returns the value like a string.
The string looks ok and it returns the value I want if it is directly passed to the cell.
Then I tried this to select the active cell and pass the value there but it didn't work.
I think I'm using it wrong. In the documentation of app script it says custom funcions can only change the value of the cell it is called (or adjacent ones)
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange().setValue(cellFunction);
When I add this to my function and delete "return cellFunction;" it returns "0"
I think I'm using it wrong.
Can you guide me because I couldn't find a solution?
I'm also open to suggestions for better ways of extracting same info.
You should be using setFormula() for this purpose: https://developers.google.com/apps-script/reference/spreadsheet/range#setformulaformula
If you still can't get it to work, share a copy of your sheet and I will try to help further.
You have two options:
Either return cellFunction to the main function by calling puan(x) inside the main function
Sample:
function myFunction() {
var URL = "Your URL"
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange().setValue(puan(URL));
}
function puan(x) {
var cellFunction = '=IMPORTXML("' + x + '";"//div[#id=\'puan\']//#data-percent")';
return cellFunction;
}
Or set the value into the spreadsheet inside puan(x)
Sample:
function myFunction() {
var URL = "Your URL"
puan(URL);
}
function puan(x) {
var cellFunction = '=IMPORTXML("' + x + '";"//div[#id=\'puan\']//#data-percent")';
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange().setValue(cellFunction);
}
Note:
Using setFormula() instead of setValue() is in your case possible, but optional.
I have 2 Google Apps Script Projects.
SheetsLib - This is a Library which I created, it contains my go-to functions.
TestFile - This is a container-bound script, which utilizes the SheetsLib.
SheetsLib contains the following functions which are relevant for this question:
displayDraftsSelector - Displays the draftSelector Html Page in the Sidebar.
draftSelector - Html file, which contains a js script as well that calls back-end function to populate a <select>
getDraftsArr - Server function which returns all gmail drafts from the user.
The SheetsLib function(s) do work, i.e. I have test functions to confirm that. My goal is to enhance this library, so that I can use it in multiple projects with the functionality to allow a user to choose an existing Gmail Draft and send it to selected users (in the active Spreadsheet).
PROBLEM
In my new container-bound script, which has access to the Library, I can only show the sidebar but not call a back-end function (which resides in the Library) when I press a button in sidebar:
I load the view successfully using displayDraftsSelector() which shows the view draftSelector. This is all functionality from the Library.
Then, the view calls the getDraftsArr() and this is what gets the error. But that function does exist in the Library (and it does work as intended).
The following is the error I see in the console when the sidebar loads:
Uncaught TypeError: google.script.run.withSuccessHandler(...).withFailureHandler(...).getDraftsArr is not a function
What should happen ideally is that, the back-end function getDraftsArr() is called and its result populates the select item. Then the user can select one draft in the sidebar. When the user confirms using a button, the active rows are the recipients. Overall, this all works when I copy-> paste, but what cannot figure out is how to keep the functionality in a library.
The following is the function located in the Library which I am trying to call.
// back-end in Library Project
function getDraftsArr(){
let output = [];
const messages = GmailApp.getDraftMessages();
messages.forEach( message => {
output.push({
id: message.getId(),
subject: message.getSubject()
});
});
return JSON.stringify(output)
}
The following is in the back-end of the library
// front-end in Library Project
<select id="draftsSelect"></select>
<script>
function getDrafts(){
const draftsSelect = document.getElementById("draftsSelect");
google.script.run
.withSuccessHandler( updateDrafts )
.getDraftsArr();
function updateDrafts( drafts ){
var options = "";
var draftsParsed = JSON.parse(drafts);
draftsParsed.forEach( draft => {
options += "<option value='" + draft.id + "'>" + draft.subject + "</option>";
});
draftsSelect.innerHTML = options; }
}
</script>
Thanks to #Rubén for the link to this: stackoverflow.com/q/59271952/1595451
Basically the solution was to create a function in my container-script with the same name as the back-end function which are in the library, which then call the library.
So in my Library I had getDraftsArr(){}before, and now I added the following to my container-bound project:
function getDraftsArr(){
return SheetsLib.getDraftsArr()
}
That did the trick.
If you can create a dummy function. Then the dummy function to create is this one:
function executeLibraryFunctionsByName(funcname) {
libraryname[funcname]();
}
This function will allow you to call all of your library functions by name. I'm not sure that this will work if your libary name has a space in it. In that case change it's name. Note: don't put quotations around the libraryname. Just write the libaryname with no quotes and I would avoid spaces and it becomes the 'this' for your function.
As an example I have a library xyzSUS1 and I can use a button like this:
<input type="button" value="test" onClick="google.script.run.callLibraryFunctionsByName('selectColumnsSkipHeader');" />
in my sidebar to run the library function xyzSUS1.selectColumnsSkipHeader() .
the command in function declaration in my gs code is this:
function callLibraryFunctionsByName(funcname) {
xyzSUS1[funcname]();//the this for funcname because xyzSUS1
}
I am working with different Google spreadsheets which all use the same sheets and structure. All of them do also use an identical set of functions which are executed by using custom menu items.
To maintain the functions only once for all spreadsheets, one serves as a Library for all others. Now here is the problem: All spreadsheets have a set of individual properties which need to be passed to Library function calls. That's why I am currently doing something like this:
Sample function in the library which is made available as lib as well as the menu construction for all sheets:
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Actions')
.addItem("My Library Function", "trackingHelper")
.addItem("My 2nd Library Function", "anotherFunction")
.addToUi();
}
function trackingHelper(sheetProperties) {
do something and use the sheetProperties
}
And now, in the "child" sheets I am adding something like this:
var sheetProperties = {key: value, ...}
function onOpen() {
lib.onOpen();
}
function trackingHelper() {
lib.trackingHelper(sheetProperties);
}
function anotherFunction() {
lib.anotherFunction(sheetProperties);
}
The problem is, that I always need to edit all sheets if I add new functions. That's why I'd like to do something like this with the menu in the library spreadsheet:
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Actions')
.addItem("My Library Function", "lib.trackingHelper")
.addItem("My 2nd Library Function", "lib.anotherFunction")
.addToUi();
}
So I only want to add one onOpen with this menu to all child sheets to spare the need for adding all functions individually. But how can I pass my child properties now? I tried to use the PropertiesService but executing a script from a child sheet always yielded the properties scope of the library sheet.
Is there any possibility to avoid the need for remapping all local functions to library functions in order to pass some sheet-specific variables? Thanks a lot.
Are the sheet properties in the child sheets variable or are they static?
If static, store them in the property service with the child spreadsheet ID as the key, then use the getactivespreadsheet().getID to pull the properties out from within the lib file.
Much faster than a get value read off the sheet but I can’t say that I’m free of functions hat pull variables from a spreadsheet as well. It was easy at the time and just works. Unless someone edits the formatting or accidentally deletes values or the sheet.
EDIT: I have the same problem as you. I still have to remap all library functions in the child script, BUT I have figured out how to manage individual properties as below.
--
In the library, you can manage the variables for each child:
PROJECT_NAME1 = {
name: 'abc',
visibleOverride: false
};
PROJECT_NAME2 = {
name: 'xyz',
visibleOverride: true
};
In the child script, you can have a global variable like so:
PROJECT = lib.PROJECT_NAME1;
In the child script, you pass the global PROJECT variable to the library functions:
function hideSheet() {
lib.Group.toggleSheetVisibility(PROJECT, false);
}
function showSheet() {
lib.Group.toggleSheetVisibility(PROJECT, true);
}
In the library, you can access the individual settings as customised by the originating child script:
Group = {
toggleSheetVisibility: function (PROJECT, visible) {
var sheet = SpreadsheetApp
.getActive()
.getActiveRange()
.getSheet();
if (!PROJECT.visibleOverride) {
visible
? sheet.showSheet()
: sheet.hideSheet();
}
}
}
After searching through various questions and the documentation I still cannot edit the calling cell of a custom function. According to the docs:
Returns the range of cells that is currently considered active. This
generally means the range that a user has selected in the active
sheet, but in a custom function it refers to the cell being actively
recalculated.
Although this description is for the getActiveRange() function one assumes the getActiveCell() functions in the same way.
I am trying to call a helper function from within my custom function which should return the 'active cell' which, to my knowledge, should be the calling cell of the custom function. Not working code:
// returns the current active (calling) cell for use within custom functions
function callingCell() {
return SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getActiveRange()
}
// change background colour of calling cell
function getInfoFromPublicApi(type) {
... // get prices using UrlFetchApp
callingCell().setBackground('green')
return averagePrice(prices, type)
}
I have also tried using the SpreadsheetApp from within the custom function directly, as well as using:
SpreadsheetApp.getActiveSpreadsheet().getActiveCell()
SpreadsheetApp.getActiveSpeadsheet().getActiveRange()
You can go about it two ways. First, set calling cell as a variable. You cannot chain methods together like you're trying to do in the larger function. Or, you can drop the callingCell() function because the information is handled in the event object of onEdit().
Method 1
// returns the current active (calling) cell for use within custom functions
function callingCell() {
return SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getActiveRange();
}
// change background colour of calling cell
function getInfoFromPublicApi(type) {
var active = callingCell();
... // get prices using UrlFetchApp
active.setBackground('green')
return averagePrice(prices, type)
}
Method 2
function onEdit(e) {
// Not sure what your `type` param is, so you'd need to test for that somehow.
... // get prices using UrlFetchApp
e.getRange().setBackground('green');
...
}
I have following custom function in google sheets, I tried to call a built-in function "TEXT" in my custom function but it is not successful. The Google sheets will prompt "unknown" function "TEXT". Is there a solution for this?
function NextMonth(StockTradeDate) {
var DeltaDate;
if (**TEXT**(StockTradeDate,"mmm") = "JAN" ) {
DeltaDate = 30;
}
return DATEVALUE(StockTradeDate) + 31;
}
Google Apps Script has the Utilities library which includes the formatDate method
Utilities.formatDate(date, timeZone, format)
For details see https://developers.google.com/apps-script/reference/utilities/utilities#formatdatedate-timezone-format
It's worth to say that in Google Sheets it's no possible call a built-in function within the script. If a service like Utilities doesn't include the functions that you are looking for, then the alternative is to build your own version, get it borrowed from a library, a open source project or any other place.
I made an attempt to use the spreadsheet function library from Ethercalc and shared about this on my answer to Is there a way to evaluate a formula that is stored in a cell?
Try using javascript date methods (as below) to drive the date and conditionals you need. Could not locate documentation that supports the Sheets built-in function calls from within an apps script function. Javascript is supported.
function NEXTMONTH(StockTradeDate) {
var DeltaDate
if (StockTradeDate.getMonth() === 0 ) { //Jan:0 Feb:1 ... Dec:11 these will need more conditionals.
DeltaDate = 30;
}
var date2 = new Date(StockTradeDate.valueOf()+ DeltaDate*24*60*60*1000)
// valueOf() is millisec time since 1/1/1970
return date2
}
If you need more info regarding the date methods and implementation, w3schools has an efficient reference.