Google Apps Script not properly updating - google-apps-script

I'm working on creating a system for other teachers to easily track their students' progress. I've got a spreadsheet with individual sheets for each student and then a sheet for an overview of all students. The spreadsheet has the following script attached to it:
function SheetNames() {
try {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets()
var out = new Array( sheets.length+1) ;
for (var i = 1 ; i < sheets.length ; i++ ) {
out[i] = [sheets[i-1].getName()];
}
return out
}
catch( err ) {
return "#ERROR!"
}
}
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Student List')
.addItem('Update Student List', 'SheetNames')
.addToUi();
}
In the "Overview" sheet, I have a cell that just contains =SheetNames(). When I first enter the custom function, the list populates. When I open the spreadsheet, the menu is added as it should be. However, when I click the menu item, the list of students on the "Overview" sheet is not updated. Is there anyway to make this function automatically update?

It will not update because all that function does is return an array. When you use the =SheetNames() notation, you are giving it a range to write to (the cell where you put the formula).
When you run the function via menu click, it doesn't know anything about the target range in the spreadsheet. It simply creates a variable ('out'), stores it in memory, then destroys it when the function finishes executing.
If you'd like to write to specific range, you should reference it in your function. Here's a quick example of the function that writes a random number from 0 to 100 to cell A1 of the first sheet
function populateCell() {
var number = Math.random() * 100;
var cell = SpreadsheetApp.getActiveSpreadsheet()
.getSheets()[0]
.getRange("A1");
cell.setValue(number);
}
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('menu')
.addItem('run', 'populateCell')
.addToUi();
}
It will not work as a custom function though. As per GAS documentation, if a function is being called from a cell (is custom), that function is read-only and can't set values https://developers.google.com/apps-script/guides/sheets/functions
Also, there's no need to use custom function because you seem to only need this code for specific range. If you intend to write to whatever cell the cursor is currently in, you should remove the reference to specific cell and use sheet.getActiveCell() instead. Only use custom functions for general functionality not tied to specific range.

Related

Google Apps script on cell background change recalculation [duplicate]

I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.

Show/Hide Columns Using A Checkbox

I am a wanna-be developer who is trying to figure out how to hide a set of columns based off a checkbox being clicked.
Would anyone want to help with this code?
I have 12 different sheets(one for each month) and I would like to hide columns A-H with the checkbox in I being clicked.
Ideally I can implement on each individual sheet.
Link to spreadsheet
There are few ways one can do it.
Easiest and most recommended among all is to group those column and it will have pretty much same use which you're looking for.
If you're willing to use appscript for it. Here how it should be done:-
Open Script Editor from your spreadsheet.
Declare the onEdit simple trigger which will run every time when sheet will be edited.
So whenever you'll click on tickbox on I1 this function will trigger.
When a trigger fires, Apps Script passes the function an event object as an argument, typically called e.
For this object, we're gonna have the information we need to do our task, and also to restrict our operation to only to those months sheet and range belongs to it.
Here is the code, I tried my best to explain what happening in the code:-
function onEdit(e)
{
var rangeEdited = e.range; // This will us range which is edited
var sheetEdited = rangeEdited.getSheet().getName() // from range we can get the sheetName which is edited
var mySheets = ["Jan List","Feb List"] // Put all the month sheet name in this array where you want to have this functionality
var rowEdited = rangeEdited.getRow() // From Range we can get Row which is edited
var columnEdited = rangeEdited.getColumn() // From Range we can get Column which is edited
if(mySheets.indexOf(sheetEdited) > -1) // Now we want to only restrict the operation on those sheets,so if other sheet is edited, we shouldn't run our hide function
{
if(rowEdited === 1 && columnEdited === 9) // we're further restricting the range of operation to run this function when only I1 is edited that is Row:- 1 and Col:- 9
{
hideUnhide(sheetEdited) // calling our hide function within OnEdit and passing sheetName as an argument in it
}
}
}
function hideUnhide(sheetEdited) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssname = ss.getSheetByName(sheetEdited) // accessing the sheet which is edited
var isItHidden = ssname.isColumnHiddenByUser(1) // checking if range is already hidden
if(isItHidden === false) // if No, hide that range
{
ssname.hideColumns(1, 6)
}
else // if Yes, unhide that range
{
var hideThisRange = ssname.getRange('A:H')
ssname.unhideColumn(hideThisRange)
}
}
Documentation:-
AppScript Events

Using onEdit installable trigger, how do I call a function when a specific value is selected in any of a range of cells(all with dropdowns)?

I have a range of cells in a sheet with a list of dropdown values.
I have setup an installable trigger to use onEdit.
I written simple functions to call different google forms in a sidebar depending on the which value is selected in the dropdown.
Range of cells : J4:P20
Each cell has a dropdown with three options A, B and C
If I choose A, I want to call function A which will load sidebar A
If I choose B, I want to call function B which will load sidebar B etc
The sidebars are created.
I just need a complete example of an onEdit function that will call sidebar A,B or C depending on which value is selected in each of the dropdowns in each cell of the specified range.
This is my poor pseudo attempt.
function onEdit(e) {
var ss = SpreadsheetApp.getActive()
var sheet = SpreadsheetApp.getActiveSheet()
var values = sheet.getRange("J4:P20").getValues();
Logger.log(values);
var cellContent = values.getValue()
if cellcontent = SQL {
loadsqlSideBar();
}
else if cellcontent = ORACLE {
loadoracleSideBar();
}
else if cellcontent = IMANIS {
loadimanisSideBar();
}
}
function loadsqlSideBar()
{
var userInterface=HtmlService.createHtmlOutputFromFile('sql');//sidebar for html and formBar for form
SpreadsheetApp.getUi().showSidebar(userInterface);
}
function loadoracleSideBar()
{
var userInterface=HtmlService.createHtmlOutputFromFile('oracle');//sidebar for html and formBar for form
SpreadsheetApp.getUi().showSidebar(userInterface);
}
function loadimanisSideBar()
{
var userInterface=HtmlService.createHtmlOutputFromFile('imanis');//sidebar for html and formBar for form
SpreadsheetApp.getUi().showSidebar(userInterface);
}
function onEdit(e) {
if(e.value=='SQL') loadsqlSideBar();
if(e.value=='ORACLE') loadoracleSideBar();
if(e.value=='IMANIS') loadimanisSideBar();
}
In order for you to solve your issue, you will need to use event objects in order to monitor the edited cell.
Snippet
function onEditTrigger(e) {
var cellEdited = e.range.getValue();
if (cellEdited == "SQL") {
loadsqlSideBar();
}
else if (cellEdited == "ORACLE") {
loadoracleSideBar();
}
else if (cellEdited == "IMANIS") {
loadimanisSideBar();
}
}
function loadsqlSideBar()
{
var userInterface=HtmlService.createHtmlOutputFromFile('sql');//sidebar for html and formBar for form
SpreadsheetApp.getUi().showSidebar(userInterface);
}
function loadoracleSideBar()
{
var userInterface=HtmlService.createHtmlOutputFromFile('oracle');//sidebar for html and formBar for form
SpreadsheetApp.getUi().showSidebar(userInterface);
}
function loadimanisSideBar()
{
var userInterface=HtmlService.createHtmlOutputFromFile('imanis');//sidebar for html and formBar for form
SpreadsheetApp.getUi().showSidebar(userInterface);
}
Explanation
The onEditTrigger function is an installable trigger which means that it will trigger once an edit action is detected. The main difference between an installable and a simple trigger is that they can call services which require authorization and they offer more flexibility. There is no need to specify the range J4:P20 since you will gather the edited cell and you will check if its value is one of the wanted ones.
Therefore, the above script makes use of the e event object in order to detect which cell has been edited and then based on its value, it loads the sidebar.
Installing the Trigger
In order to have an installable trigger, you will need to install it. To do so, you will need to go to your project's triggers by clicking this icon.
Afterwards, you will need to create a new trigger with the following options:
Note
Also don't forget to use the correct way for an if structure.
Reference
Event Objects Apps Script;
Installable Triggers Apps Script;
if...else Structure JavaScript.

Force Google Spreadsheet formula to recalculate

I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.

How to click an empty google sheet cell and get the real time, NOW()?

I work for a transportation company and What I am trying to achieve is that every time a driver login or mobiles over the two-way radio, I want to click the empty cell and get the real time. I know there is a Now() function, in google sheets but if I use that all cells will have the same value, I want to get the current time when I click the empty cell.
I used the script editor to create buttons for a workaround that will show me in and out times, but I want to keep the original format of the sheet, here is the code below:
function setValue(cellName, value){
SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).setValue(value);
}
function getValue(cellName){
return SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).getValue();
}
function getNextRow(){
return SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;
}
function setGeorge(){
setValue('M1','George,CSH2501')
}
function setTerry(){
setValue('M1','Terry,CSH2502')
}
function setDavid(){
setValue('M1','David,PSH3502')
}
function setSandra(){
setValue('M1','Sandra,PSH3508')
}
function setElioCalle(){
setValue('M1','ElioCalle,PSH3504')
}
function setWarren(){
setValue('M1','Warren,PSH3501')
}
function setShu(){
setValue('M1','Shu,PSH3503')
}
function setPaul(){
setValue('M1','Paul Prkash,PSH3507')
}
function setMei(){
setValue('M1','Mei Tsang,PSH3506')
}
function setJames(){
setValue('M1','James Clark,PSH3505')
}
function setElvira(){
setValue('M1','Elvira,PSH3509')
}
function addRecord(b, c, d){
var row = getNextRow();
setValue('A'+ row, b);
setValue('B'+ row, c);
setValue('C'+ row, d);
}
function punchIn(){
addRecord(getValue('M1'), new Date(), 'IN');
}
function punchOut(){
addRecord(getValue('M1'), new Date(), 'Out');
}
And the results are this:
=Now() is a volatile function, every time you change some data in the spreadsheet or refresh the sheet excel recalculates it. The only way to overcome is through a script, I created buttons using the drawing tool and assigned functions punchIn and punchOut for every time a driver mobiles and clears:
function punchIn(){
var sheet = SpreadsheetApp.getActiveSheet();
var now = new Date();
sheet.getRange('E3').setValue(now);
}
function punchOut(){
var sheet = SpreadsheetApp.getActiveSheet();
var now = new Date();
sheet.getRange('H3').setValue(now);
}
The only thing was I have to do this for every cell and every button as you can see from the image below. If there is a way to simplify the code any suggestion would be appreciated.
Nowadays it's possible to insert a timestamp when clicking a cell by using the onSelectionChange simple trigger.
The following example will add a timestamp to any cell / range selected
function onSelectionChange(e) {
e.range.setValue(new Date());
}
You can control on which cells the timestamp be added, i.e., by using if-statements and the event object.
After adding onSelectionChange trigger you have to reload the spreadsheet to make it start working.
When opening a spreadsheet from Google Drive or Google Sheets apps, the default active cell is the first cell of the first sheet, onSelectionChange will not run in this case but if you use a range link , i.e. https://docs.google.com/spreadsheets/d/spreadheetId/edit#gid=0&range=I5:I9, onSelectionChange will run.