How to refresh google spreadsheets by clicking a "button"? - google-apps-script

I tried writing an AppsScripts script to refresh sheet each time I press a button that the script is assigned to.
The only thing that must change each time is the cell with formula "=INDIRECT("A"&RANDBETWEEN(1;20))"

In your situation, I thought that these threads might be useful. "https://stackoverflow.com/q/56893480", "https://stackoverflow.com/q/61300428" But, in this case, the event object is used and a part of the formula is used. So, in this answer, I would like to introduce a sample script for refreshing the cells including the specific function =INDIRECT("A"&RANDBETWEEN(1;20)) by clicking a button on the Spreadsheet.
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please assign the function name of myFunction to a button on the Spreadsheet. By this, when you click the button, the script is run.
function myFunction() {
const formula = '=INDIRECT("A"&RANDBETWEEN(1;20))'; // This is from your question.
const temp = formula.replace(/([=()])/g, "\\$1");
const dummyFormula = "=sample";
const obj = [
{ "from": `^${temp}|^${temp.replace(/;/g, ",")}`, "to": dummyFormula },
{ "from": `^\\${dummyFormula}`, "to": formula }
];
const sheet = SpreadsheetApp.getActiveSheet();
obj.forEach(({ from, to }) =>
sheet.createTextFinder(from).matchFormulaText(true).useRegularExpression(true).replaceAllWith(to)
);
}
When this script is run, the cells including a formula of =INDIRECT("A"&RANDBETWEEN(1;20)) in the active sheet are refreshed.
Note:
This sample script is for refreshing your provided formula of =INDIRECT("A"&RANDBETWEEN(1;20)) on the active sheet. When you change the formula, this script might be required to be modified. Please be careful about this.
Reference:
createTextFinder(findText) of Class Sheet

Related

How to set a named range for a data validation programmatically (in Google apps script) in a Google spreadsheet?

Use Case
Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D".
It works from UI
I can set manually a named range as a data source of data validation.
In this case, the data validation of a cell will always refer to the named range Apples with updated the data range.
How to make it in Google Apps Script?
GAS Limits
The code, for setting data validation, should look like this, if you have a namedRange object:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireValueInRange(
namedRange.getRange()
)
.setAllowInvalid(false)
.build()
);
DataValidationBuilder.requireValueInRange() does not work here as it requires only class Range (it cannot get NamedRange), and no reference to a named range will be used.
Is there a workaround or so?
UPD1 - Spreadsheet.getRangeByName() does not work
Getting range by name does not help, the data validation will get actual range address.
SpreadsheetApp.getActive().getRangeByName("Apples")
UPD2 No way to make it so far in GAS
As #TheMaster posted, it's not possible at this moment.
Please set +1 for posts:
https://issuetracker.google.com/issues/143913035
https://issuetracker.google.com/issues/203557342
P.S. It looks like the only solution will work is Google Sheets API.
I thought that in your situation, I thought that when Sheets API is used, your goal might be able to be used.
Workaround 1:
This workaround uses Sheets API.
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please enable Sheets API at Advanced Google services.
function myFunction() {
const namedRangeName = "Apple"; // Please set the name of the named range.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet10");
const requests = [{ updateCells: { range: { sheetId: sheet.getSheetId(), startRowIndex: 0, endRowIndex: 1, startColumnIndex: 0, endColumnIndex: 1 }, rows: [{ values: [{ dataValidation: { condition: { values: [{ userEnteredValue: "=" + namedRangeName }], type: "ONE_OF_RANGE" }, showCustomUi: true } }] }], fields: "dataValidation" } }];
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
In this request, the name of the named range is directly put to userEnteredValue.
3. Testing.
When this script is run to the above sample Spreadsheet, the following result is obtained.
When this demonstration is seen, first, you can see the named range of "Apple" which has the cells "B1:B1000". When a script is run, data validation is put to the cell "A1" with the named range of "Apple". In this case, the values of data validation indicate "B1:B1000". When the range named range "Apple" is changed from "B1:B1000" to "D1:D1000" and the data validation of "A1" is confirmed, it is found that the values are changed from "B1:B1000" to "D1:D1000".
Workaround 2:
This workaround uses the Google Spreadsheet service (SpreadsheetApp). In the current stage, it seems that the Google Spreadsheet service (SpreadsheetApp) cannot directly achieve your goal. This has already been mentioned in the discussions in the comment and TheMaster's answer. When you want to achieve this, how about checking whether the range of the named range is changed using OnChange as following workaround 2?
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please install OnChange trigger to the function onChange.
First, please run createDataValidation. By this, data validation is put to the cell "A1" of "Sheet10". In this case, the set range is the range retrieved from the named range "Apple". So, in this case, the range is Sheet10!B2:B1000.
As the next step, please change the range of the named range from Sheet10!B2:B1000 to Sheet10!D2:D1000. By this, onChange` function is automatically run by the installed OnChange trigger. By this, the data validation of "A2" is updated. By this, the values of data validation are changed.
const namedRangeName = "Apple"; // Please set the name of the named range.
const datavalidationCell = "Sheet10!A2"; // As a sample. data validation is put to this cell.
function onChange(e) {
if (e.changeType != "OTHER") return;
const range = e.source.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
const prop = PropertiesService.getScriptProperties();
const previousRange = prop.getProperty("previousRange");
if (previousRange != a1Notation) {
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(e.source.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
e.source.getRange(datavalidationCell).setDataValidation(rule);
}
prop.setProperty("previousRange", a1Notation);
}
// First, please run this function.
function createDataValidation() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
ss.getRange(datavalidationCell).setDataValidation(rule);
const prop = PropertiesService.getScriptProperties();
const range = ss.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
prop.setProperty("previousRange", a1Notation);
}
References:
Method: spreadsheets.batchUpdate
UpdateCellsRequest
DataValidationRule
Currently, This seems to be impossible. This is however a known issue. +1 this feature request, if you want this implemented.
https://issuetracker.google.com/issues/143913035
Workarounds from the tracker issue creator:
If a validation rule is manually created with a NamedRange via the Sheets GUI, it can then be copied programmatically using Range.getDataValidations(), and subsequently used to programmatically create new DataValidations. DataValidations created this way maintain their connection to the NamedRange, and behave like their manually created counterparts. This demonstrates that the functionality to 'use' NamedRanges for data validation rules is already possible with Apps Scripts, but not the option to 'create' them.
As a half-answer, if you want just validation and can live without the drop-down list of valid values, you can programmatically set a custom formula that references the named range. This reference to the named range will not get expanded in the AppsScript, so future changes to the Named Range's actual range will percolate to the validator. Like so:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireFormulaSatisfied(
'=EQ(F5, VLOOKUP(F5, ' + namedRange.getName() + ', 1))'
)
.setAllowInvalid(false)
.build()
);
(The formula just checks that the value in the cell being tested is equal to what VLOOKUP finds for that cell, in the first column -- I'm assuming the named range content is sorted.)
Use getRangeByName()
function lfunko() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0");
var cell = sh.getRange(1, 10);//location where datavalidation is applied
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName("MyList")).build();
cell.setDataValidation(rule);
}

Is there a way to execute a script on unhide sheet in Google Sheets?

When I open a hidden sheet by a link, the system shows me notification that it's hidden with Unhide button:
When I click on Unhide I want to execute a script. Is it possible?
Issue and workaround:
In the current stage, there are no methods for directly running the script by clicking "Unhide" button in your situation. So, in this case, it is required to use a workaround. In this workaround, onSelectionChange of the simple trigger is used. When onSelectionChange is used, the change of tab can be detected. Ref When "Undide" button is clicked, the tab is changed. This workaround uses this situation.
Usage:
1. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet.
function onOpen(e) {
const prop = PropertiesService.getScriptProperties();
const sheetName = e.range.getSheet().getSheetName();
prop.setProperty("previousSheet", sheetName);
}
function onSelectionChange(e) {
const prop = PropertiesService.getScriptProperties();
const previousSheet = prop.getProperty("previousSheet");
const range = e.range;
const sheetName = range.getSheet().getSheetName();
if (sheetName != previousSheet && range.getRichTextValue().getLinkUrl() != "") {
// When the tab is changed, this script is run.
range.setBackground("red");
}
prop.setProperty("previousSheet", sheetName);
}
2. Testing.
In order to use this script, please reopen Google Spreadsheet. By this, the tab name is stored to PropertiesServices. The detection of change of tab uses this information.
And, in this case, as a sample situation, it supposes that "Sheet2" is hidden. And, the cell link of "Sheet2" is put in "Sheet1". When this link is clicked, a dialog is opened. When "Unhied" button is clicked, the tab is changed. And this change of tab is detected by onSelectionChange. By this, the script can be run.
The demonstration is as follows.
Note:
Unfortunately, the simple trigger of onSelectionChange cannot use all methods of Google Apps Script. Ref Although I'm not sure about your script, I thought that this is an important point of this workaround.
References:
onSelectionChange(e)
Related thread.
Is there a google spreadsheet trigger that fires after a tab is changed?

Duplicate google form responses to another sheet as value automatically

is there any script or formula that can help me do the data cleaning. For example in the sheet below, https://docs.google.com/spreadsheets/d/1mLZ8TYNpNHVN1Kc8QF80Ns3RX-EXdSz5s9WmHoIS8Pw/edit?usp=sharing, I have "Form responses 1" which capture the data entry from google form and "cleaning data" sheet. I tried to use Arrayformula and Importrange but i cannot edit the data in "Cleaning data" sheet. Currently, I just copy and paste manually to the other sheet.
What I wanna do
Respondent enter data in google form.
Data appear in "Form responses 1" sheet.
Updated data in "Form responses 1" automatically copied and paste in the "Cleaning data" sheet by row.
Thank you!
In your situation, how about the following sample script?
Sample script:
Please copy and paste the following script to the script editor of Google Spreadsheet. And, please install OnSubmit trigger to the function onSubmit. When you use this script, please submit your Google Form. By this, the script of onSubmit is run by firing the OnSubmit trigger.
function onSubmit(e) {
const range = e.range;
const srcSheet = range.getSheet();
const dstSheet = e.source.getSheetByName("Cleaning Data");
const srcRange = srcSheet.getRange(range.rowStart, 1, 1, srcSheet.getLastColumn());
const dstRange = dstSheet.getRange(dstSheet.getLastRow() + 1, 1);
srcRange.copyTo(dstRange, {contentOnly: true});
}
Note:
In this script, please don't directly run this function with the script editor. Because this script uses the event object of OnSubmit trigger. When you directly run this script an error like TypeError: Cannot read property 'range' of undefined occurs. Please be careful this.
References:
Installable Triggers
Event Objects

Print multiple data per ID in Google Sheet

In this spreadsheet, I have a sheet called Form Responses 1 that contains the students' data and a sheet called TEMPLATE that contains the template that needs to be printed per student. The problem is that I can only print the spreadsheet one at a time by changing the data in the C2 cell. Is there any way for me to print it quickly and easily to save time and effort?
In Google Cloud Print, I want it to say Page 1 = Student 1, Page 2 = Student 2, Page 3 = Student 3, and so on. Is it possible?
In your situation, how about creating a new Google Spreadsheet including each page? When this is reflected in a sample script, it becomes as follows.
Sample script:
function myFunction() {
const srcSs = SpreadsheetApp.getActiveSpreadsheet();
const sheet = srcSs.getSheetByName("TEMPLATE");
const values = sheet.getRange("C2").getDataValidation().getCriteriaValues()[0].getValues().flat().filter(String);
const dstSs = SpreadsheetApp.create("tempSpreadsheet");
values.forEach(v => {
sheet.getRange("C2").setValue(v);
SpreadsheetApp.flush();
const tempSheet = sheet.copyTo(srcSs);
const range = tempSheet.getDataRange();
range.copyTo(range, {contentsOnly: true});
tempSheet.getRange("B2:2").clear().clearDataValidations();
tempSheet.getDrawings().forEach(e => e.remove());
tempSheet.copyTo(dstSs);
srcSs.deleteSheet(tempSheet);
});
dstSs.deleteSheet(dstSs.getSheets()[0]);
}
When this script is run, a new Google Spreadsheet of tempSpreadsheet is created to the root folder. When you see it, each tab has each sheet without using the formulas. By this, you can print out the Spreadsheet by one manual process.
Note:
When I saw your sample Spreadsheet, /** #OnlyCurrentDoc */ is used in your script. When you use my proposed script, please remove /** #OnlyCurrentDoc */. By this, the scope of https://www.googleapis.com/auth/spreadsheets is automatically detected and used it. Please be careful about this.
This sample script is for your sample Spreadsheet. So when the spreadsheet is changed, this script might not be able to be used. Please be careful about this.
References:
create(name)
copyTo(spreadsheet) of Class Sheet
copyTo(destination) Class Range

Hide / show specific Sheets tabs for specific users on shared file

My google doc should prompt a login box upon opening the Google Sheets file and, based on the password, it should display a specific tab. E.g. for "password1" it should allow the user to see only tab "Sheet1" and other sheets should be hidden. Similarly, for "password2" it should allow the user to see and work on tab "Sheet2" only.
I tried to run the following code, however it shows some errors.
function showLoginDialog() {
var sheet3 = SpreadsheetApp.getActiveSheet('Sheet3');
sheet3.hideSheet();
var sheet2 = SpreadsheetApp.getActiveSheet('Sheet2');
sheet2.hideSheet();
var sh = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet('Sheet1');
var ui = SpreadsheetApp.getUi();
var prompt = ui.prompt('Password','Enter Password',ui.ButtonSet.OK_CANCEL);
var response = prompt.getResponseText();
var button = prompt.getSelectedButton();
if(button==ui.Button.OK) {
if(response=='pwd3') {
sheet3.activate();
}//end of inner if
if(response=='pwd2'){
sheet2.activate();
}
}//end of main if
}//end of function
The Google Sheets file should ask for the password upon opening it, and it should display the respective sheet based on the password.
You want to open only one of all sheets in the Spreadsheet by the inputted value from the dialog.
For example, when the value of pwd2 is inputted, you want to open only "Sheet2".
You want to open the dialog when the Spreadsheet is opened.
If my understanding is correct, how about this modification? Please think of this as just one of several answers.
The flow of the modified script is as follows.
Open only "Sheet1" as a default, when the Spreadsheet is opened.
Open a dialog.
When pwd2 is inputted, only "Sheet2" is opened.
When pwd3 is inputted, only "Sheet3" is opened.
If you want to open the dialog when the Spreadsheet is opened, please install OnOpen event trigger to RunOnOpen().
How to install OnOpen trigger:
Open the script editor.
Edit -> Current project's triggers.
Click "Add Trigger".
Set RunOnOpen for "Choose which function to run".
Set "From spreadsheet" for "Select event source".
Set "On open" for "Select event type".
Modified script:
function RunOnOpen() {
// Updated
var openSheet = function(ss, sheets, sheet) {
var s = ss.getSheetByName(sheet);
s.showSheet();
ss.setActiveSheet(s);
for (var i = 0; i < sheets.length; i++) {
if (sheets[i].getSheetName() != sheet) {
sheets[i].hideSheet();
}
}
};
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
openSheet(ss, sheets, "Sheet1"); // Open only "Sheet1" when this function is run.
var ui = SpreadsheetApp.getUi();
var prompt = ui.prompt('Password','Enter Password',ui.ButtonSet.OK_CANCEL);
var response = prompt.getResponseText();
var button = prompt.getSelectedButton();
if (button == ui.Button.OK) {
var sheet = "";
if(response == 'pwd3') {
openSheet(ss, sheets, "Sheet3");
} else if(response == 'pwd2') {
openSheet(ss, sheets, "Sheet2");
}
}
}
Note:
In this modified script, when RunOnOpen() is manually run at the script editor, the script works. In order to do this, I didn't use the event object of OnOpen event trigger.
References:
hideSheet()
showSheet()
Installable Triggers
If I misunderstood your question and this was not the result you want, I apologize.
Updated:
When I tested several times for this script, there are the case that "Sheet1" sheet and another sheet are opened. In order to avoid this situation, I updated the script. Could you please confirm above script again?
Edit:
You want to make users use the specific sheet of Spreadsheet by inputting each password.
You don't want to publish the script for users.
You want to make several users use the Spreadsheet, simlutaneously.
If my understanding for your goal is correct, how about this workaround?
Create each Spreadsheet for each user.
By this, the simultaneous access for the Spreadsheet can be achieved.
Use Web Apps in order to open each Spreadsheet by inputting the password.
The flow of this workaround is as follows.
An user, who logged in Google, open Web Apps.
The user input the password and click "OK".
Checking the password at the server side and return the URL of Spreadsheet corresponding to the password.
Open the Spreadsheet from the URL.
By this flow, I think that top 3 aims can be achieved.
Sample script:
Before you use this script, please split the Spreadsheet for each sheet, and retrieve each URL of Spreadsheet.
When you use this, please deploy Web Apps with the following condition. If you want to know how to deploy Web Apps, please check this.
Execute the app as: Me.
Who has access to the app: Anyone
By this condition, users are required to log in to Google for accessing to Web Apps.
Users accesses to the URL of Web Apps.
Google Apps Script: Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile("index");
}
function selectSpreadsheet(value) {
var url = "";
if (value == "pwd2") {
url = "### URL of Spreadsheet for password of pwd2 ###";
} else if (value == "pwd3") {
url = "### URL of Spreadsheet for password of pwd3 ###";
}
return url;
}
HTML & Javascript: index.html
<input type="text" id="value" >
<input type="button" value="ok" onclick="openSpreadsheet()">
<script>
function openSpreadsheet() {
var value = document.getElementById("value").value;
google.script.run.withSuccessHandler((url) => {
if (url) window.open(url,'_top');
}).selectSpreadsheet(value);
}
</script>
Note:
When you modified the script, please deploy the project as new version. By this, the latest script is reflected to Web Apps. Please be careful this.
I think that when the each Spreadsheet is shared for only each user, the security will be high.
The maximum number of the simultaneous access for Web Apps is 30. Ref
This is a simple sample script. So if you use this, please modify it for your situation.
From TheMaster's somment, Although it's highly unlikely that users are able to retrieve the passwords from source code, I recommend using private functions and/or properties service for storing of passwords for a extra layer of security.
As a sample, you can use like below by saving the passwords and URLs to PropertiesService.
function selectSpreadsheet(value) {
return PropertiesService.getUserProperties().getProperty(value);
}
References:
Class google.script.run
Web Apps
Taking advantage of Web Apps with Google Apps Script
I don't think this is secure for the following reasons:
Sheets' hidden/visible property is global and not restricted to a single user. If user1 and user2 login one after another at almost the same time, user1's sheet is visible after user1's login; As soon as user2 logs in, user1's sheet will be hidden for both users and user2's sheet will be visible to both user1 and user2
It's easy to unhide sheets. user1 can unhide user2's sheet at any time he wishes to, provided he has edit access to the spreadsheet(which is needed, if you want to display the login dialog).