Multiple dependent dynamic dropdowns with repeating column dropdowns in Google Sheets - validation

The Google Sheet I have uses code made by user Max Makhrov, code here, to make multiple dependent dynamic dropdowns in columns D-F (for location) and columns H-L (for objectives & activities) in my sample sheet here.
I would like help to modify the script to do two things:
Whatever activity is selected from the dropdown menu in Column I, I would like the same dropdown menu options to be available (to repeat) for columns J-L. As you can see I found a way to do it, but to me it seems clunky and not ideal, and leaves too much room for errors. Users should not select the activity twice, but I've put conditional formatting in to flag that if they do. However:
Ideally, but less importantly, if the dropdown menu items could still repeat for columns J-L but once an activity is selected in previous cells, that option is removed from each of the following repeated dropdown menus in additional columns, up to and including column L. This would help avoid accidentally repeating an activity.
NB: Reference question "How do you do dynamic / dependent drop downs in Google Sheets?"
Thank You!

When one of the drop-down cells is edited you can use an onEdit trigger [1] to iterate through the 4 columns (I-L) and update the drop-downs in each cell removing the option selected in the edited cell. You also need to add the old selected value (previously deleted from other options) to the other drop-downs. For this, you can use getDataValidation [2] and getCriteriaValues [3] functions chained to a Range object to retrieve the current drop-down values array on that range and delete the option matching with the selected option.
Use newDataValidation() [4] function to create a new rule using your updated drop-down values array and setDataValidation [5] function to set the rule to the range.
function onEdit(event) {
var range = event.range;
var sheetName = range.getSheet().getSheetName();
var col = range.getColumn();
var newValue = event.value;
var oldValue = event.oldValue;
//If the edited range is in sheet '3W' and beetween columns I-L
if(sheetName == '3W') {
if(col>=9 && col<=12) {
for(var i=9; i<13; i++) {
//Don't change anything for edited cell
if(col == i) { continue; }
else {
//Get range to update and current dropdown values for that range
var rangeToUpdate = range.getSheet().getRange(range.getRow(), i, 1, 1);
var dropdownValues = rangeToUpdate.getDataValidation().getCriteriaValues()[0];
//Find new edited value and delete it from options array
var index = dropdownValues.indexOf(newValue);
if (index > -1) {
dropdownValues.splice(index, 1);
}
//If previous selected value is not beetween the options, add it
if(oldValue && dropdownValues.indexOf(oldValue) == -1) {
Logger.log(oldValue)
dropdownValues.push(oldValue);
}
//Set new dropdown values to range
var updatedRule = SpreadsheetApp.newDataValidation().requireValueInList(dropdownValues, true).setAllowInvalid(false);
rangeToUpdate.setDataValidation(updatedRule);
}
}
}
}
}
Run just the first time to set all the drop-downs in columns I-L, which are get it from range E1:E10:
function setDropdownsInitially() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
//Range with the dropdown values
var sheet = ss.getSheetByName("indicators");
var dropdownValues = sheet.getRange("E1:E10").getValues();
//Data validation rule
var rule = SpreadsheetApp.newDataValidation().requireValueInList(dropdownValues, true).setAllowInvalid(false);
//Range where the dropdowns will be created
var targetSheet = ss.getSheetByName("3W");
var cells = targetSheet.getRange("I2:L");
//Set data validation rule
cells.setDataValidation(rule);
}
[1] https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
[2] https://developers.google.com/apps-script/reference/spreadsheet/range#getdatavalidation
[3] https://developers.google.com/apps-script/reference/spreadsheet/data-validation-builder.html#getcriteriavalues
[4] https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#newdatavalidation
[5] https://developers.google.com/apps-script/reference/spreadsheet/range#setdatavalidationrule

Related

GAS: copy one data validation (dropdown) in N number of rows and set unique value in each dropdown

Edited for clarity and to add images
Using Google Apps Script, how can I:
copy a range from Section 2 sheet (G11:H11, with a checkbox & dropdown) into range G12:G25 N number of times (based on the number of non-empty rows in MASTER DROPDOWN sheet under same header title as 'Section 2'!A2) and then,
set a different value in each dropdown (each unique value listed in MASTER DROPDOWN sheet under the correct header).
For example, first image is "MASTER DROPDOWN" sheet.
This second image is "Section 2" sheet. The user can add or delete items on the list using the buttons on the right side of the page.
And this last image is "Section 2" sheet. I cannot understand how to write the code for this... When user presses "Reset list" button, I want to copy checkbox and dropdown menu (from G11:H11) N number of times (N=3 based on number of items from MASTER DROPDOWN under Section 2). In each dropdown, I want to set value with each item from the original list in the MASTER DROPDOWN sheet. This process should be dynamic and work on Section 1 and Section 3 sheet (not in worksheet currently).
Any advice on the script verbage to search/learn about this type of functionality, or some direction on the script for this is much appreciated. Here's a link to my code that I have so far...
https://docs.google.com/spreadsheets/d/1ZdlJdhA0ZJOIwLA9dw5-y5v1FyLfRSywjmQ543EwMFQ/edit?usp=sharing
function newListAlert (){
var ui = SpreadsheetApp.getUi();
var response = ui.alert("Are you sure you want to delete your current list and create a new one?",ui.ButtonSet.YES_NO);
if(response == ui.Button.YES) {
newList();
} else {
}
}
function newList() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var range = ss.getRange("G11:H25");
var options = {contentsOnly: true, validationsOnly: true};
//clear current list
range.clear(options);
//add new item to list in first row of range
addNewItem();
//copy new datavalidation row above based on number of non-empty rows in MASTER DROPDOWN with same header as active sheet (-1)
var datass = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("MASTER DROPDOWN");
var range = ss.getRange("A2");
if (range.getCell(1,1)){
var section = datass.getRange(1,1,1,datass.getLastColumn()).getValues();
var sectionIndex = section[0].indexOf(range.getValue()) + 1;
var validationRange = datass.getRange(4,sectionIndex,19);//19 columns: checklist has a maximum of 18 rows (+ 1 for "select option")
}
}
In your situation, how about modifying newList() as follows?
Modified script:
function newList() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var sheetName = sheet.getSheetName();
sheet.getRange("G11:H25").clear({ contentsOnly: true, validationsOnly: true });
var srcSheet = ss.getSheetByName("MASTER DROPDOWN");
var values = srcSheet.getDataRange().getValues();
var obj = values[0].map((_, c) => values.map(r => r[c])).reduce((o, [h, , , ...v], i) => {
if (h != "") {
v = v.filter(String);
v.shift();
o[h] = { values: v, range: srcSheet.getRange(4, i + 1, v.length + 1) };
}
return o;
}, {});
if (obj[sheetName]) {
var validationRule = SpreadsheetApp.newDataValidation()
.setAllowInvalid(false)
.setHelpText('Select an option from the menu. To add more options to the dropdown list, go to MASTER DROPDOWN tab.')
.requireValueInRange(obj[sheetName].range, true)
.build();
var d = obj[sheetName].values.map(_ => [validationRule]);
var v = obj[sheetName].values.map(e => [e]);
sheet.getRange(sheet.getLastRow() + 1, 8, obj[sheetName].values.length).setDataValidations(d).setValues(v).offset(0, -1).insertCheckboxes();
}
}
When this script is run, the values for DataValidations are retrieved from the sheet "MASTER DROPDOWN", and using the sheet name, the dataValidation rules are created, and put to the column "H". And also, the checkboxes are put to the column "G" of the same rows of the dataValidations.
In this case, for example, when you add a new sheet of "Section 1" and run newList(), the dropdown list including "Engineering" and "Design" is put to the column "H" and the checkboxes are also put to the column "G".
Note:
In this modification, the sheet name like "Section 2" is used for searching the column of "MASTER DROPDOWN" sheet. So please be careful about this.
And, from your current script, the last row is used for putting to the dropdown list and checkboxes. So when you want to modify this, please modify the above script.
This sample script is for your sample Spreadsheet. So when your actual Spreadsheet is changed, this script might not be able to be used. Please be careful this.
References:
setDataValidations(rules)
insertCheckboxes()

Google Sheets - QUERY, adding a cell to hide a row

I'm hoping to use a Google Form to have my students sign up for activities. I added a column to the right of the form data where I can put an "x" once I have met with them. I have a separate tab called "Ordered" where I use QUERY to sort and show only the entries without an x. Once I meet with a student, I can put an x on the original data tab, and the entry will hide from the Ordered tab (kind of a queue for my students).
I would love to not have to switch between the 2 tabs every time I have to check someone off. Is there a way to add a similar column on the Ordered tab that will hide the finished entry?
Here is the sheet I'm referring to:
https://docs.google.com/spreadsheets/d/1fQHF0EoGLk5NEI6GvyRk4InBl-uEq0jq7qqmyOmFjy8/edit#gid=379798836
I'm sharing this sheet with other teachers who aren't familiar with spreadsheets, so the solution has to be ludite friendly.
As #Aerials said, you are likely to run into circular dependency issues here. Because of this, I'd suggest using Apps Script and get rid of the QUERY formula. You want to do the following:
Every time someone submits the form, the Ordered sheet gets updated with the new submission data.
Every time an x is added to the Form Responses 1 sheet, the corresponding row in Ordered gets removed.
Every time an x is added to the Ordered sheet, the corresponding row in Ordered gets removed, and an x gets added to the corresponding row in Form Responses 1.
A possible way to go would be something along the following lines (open the script bound to your spreadsheet by clicking Tools > Script editor):
Install an onFormSubmit trigger so that Ordered gets updated with new data every time the form is submitted. The trigger can be installed manually or programmatically, copying this function to your script and running it once:
function onFormSubmit(e) {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger("copyAndFilterData")
.forSpreadsheet(ss)
.onFormSubmit()
.create();
}
Once the trigger is installed, the function copyAndFilterData will execute every time the form is submitted. This function should updated Ordered based on the data in Form Responses 1, removing all the submission that have been marked as complete x. This function could be the following:
function copyAndFilterData() {
var ss = SpreadsheetApp.getActive();
var sheet1 = ss.getSheetByName("Form Responses 1");
var sheet2 = ss.getSheetByName("Ordered");
var sourceData = sheet1.getDataRange().getValues();
var filteredData = sourceData.filter(sourceRow => sourceRow[5] !== "x")
.sort((a, b) => a - b)
.map(sourceRow => {
sourceRow.pop();
sourceRow.splice(1, 1);
return sourceRow;
});
sheet2.getRange(1, 1, sheet2.getLastRow(), 4).clear({contentsOnly: true});
var destRange = sheet2.getRange(1, 1, filteredData.length, filteredData[0].length)
destRange.setValues(filteredData);
}
At this point, another piece of functionality would be needed: update the spreadsheet based on the addition or removal of x on any of both sheets. For this, an onEdit trigger would be needed, like this:
function onEdit(e) {
var editedSheet = e.range.getSheet();
if (editedSheet.getName() === "Form Responses 1") copyAndFilterData();
else if (editedSheet.getName() === "Ordered") removeRows(e, editedSheet);
}
Once this function is copied and the project saved, every time Form Responses 1 is edited, the Ordered sheets gets updated (function copyAndFilterData), and every time Ordered is edited, the function removeRows is fired. This function should do two things: (1) remove the row which was marked as complete in Ordered (x added) and (2) add the corresponding x to Form Responses 1. The submission dates can be compared to identify the row:
function removeRows(e, editedSheet) {
var range = e.range;
var column = range.getColumn();
var row = range.getRow();
var value = range.getValue();
if (column == 5 && row > 1 && value == "x") {
var date = editedSheet.getRange(row, 1).getValue();
var formSheet = e.source.getSheetByName("Form Responses 1")
var values = formSheet.getRange(2, 1, formSheet.getLastRow() - 1).getValues();
var markedRow = values.findIndex(value => value[0].getTime() === date.getTime());
formSheet.getRange(markedRow + 2, 6).setValue("x");
editedSheet.deleteRow(row);
}
}
Reference:
Overview of Google Apps Script
Simple Triggers
Installable Triggers

Google Sheets - Multiple dependent drop-down lists

I am trying to create a dependent list as described and answered (with a script) here.
I would like to achieve that if selecting a certain value (e.g. "First") from a cell in column 1, then the drop-down options from the next cell in the same row should offer a range of values from the column in a different sheet with the same heading as the value in the first - left - cell (i.e. the first sheet is called "Selector" - in which there are dropdowns, in the second sheet called "KAT" I have the options for these dropdowns). This should then be possible for every row depending on the value of each first cell of the row.
I have tried to use and adapt the suggested script and have reviewed the sample files in the article but I apparently lack some basic understanding of the script to be able to adapt and implement it properly.
Could anybody kindly help me with making this dynamic dropdown work properly?
Just to clarify my final intention: I would like to have this script working first to be able to use it on multiple files. My final goal, though, is to make self-filling dropdown lists and selectors, so that I could simply fill in the data in the "Selector" sheet and would then be able to select these same values later in the cells below (depending on the name (value) of the first cell in the row = first cell of the column holding validation range). I hope to be able to achieve this by using either Pivot table or any other formula in the "KAT" sheet that would aggregate my data from "Selector" sheet and feed them back as drop-down options ...).
Thank you for your help.
See the example sheet here
Code I used (as above):
function onEdit()
{
var ss = SpreadsheetApp.getActiveSpreadsheet(),
sheet = ss.getActiveSheet(),
name = sheet.getName();
if (name != 'Selector') return;
var range = sheet.getActiveRange(),
col = range.getColumn();
if (col != 1) return;
var val = range.getValue(),
dv = ss.getSheetByName('KAT'),
data = dv.getDataRange().getValues(),
catCol = data[0].indexOf(val),
list = [];
Logger.log(catCol)
for (var i = 1, len = 100; i < len; i++) // Problem is here, you have too many items in list! Cannot have more 500 items for validation
list.push(data[i][catCol]);
var listRange = dv.getRange(2,catCol +1,dv.getLastRow() - 1, 1)
Logger.log(list)
var cell = sheet.getRange(range.getRow(), col-1)
var rule = SpreadsheetApp.newDataValidation()
.requireValueInRange(listRange) // Use requireValueIn Range instead to fix the problem
.build();
cell.setDataValidation(rule);
Logger.log(cell.getRow())
}
This question deals with dynamic dropdown lists. A previous question and answer on StackOverflow (Google Sheets - Dependent drop-down lists) were referenced, and code from that answer was being unsuccessfully re-purposed.
The code in the question was not working for one reason: Line 20
var cell = sheet.getRange(range.getRow(), col-1)
In the referenced code, the dropdown list begins in Column F (col=6). The dependant dropdowns ranged to the left so the definition of the dependant column was "col-1". In the questioner's scenario, the dropdown list begins in Column A (col=1) and the dependant dropdowns range from left to right. However, this line of code was not changed to take into account the different layout. Rather than "col-1", it should be "col+1".
Other matters
In addition to this, lines 16 and 17 perform a loop to create an array that might be used for the dependant dropdown. However the loop is redundant because the dropdown is actual defined by creating and assigning a range on the "KAT" sheet.
Cell A2 of KAT includes a formula:
=sort(unique(Selector!$A$2:$A),1,true)
This may appear to be useful because it automatically adds any new dropdown value entered in "Selector" to a list of values in KAT. In reality it is unproductive, because the dependant dropdown build by the code works vertically rather than horizontally. So an additional row added to KAT does not, of itself, contribute to building the dependant dropdown.
The following code works to build the dependant drop down list. I have deliberately left a number of "Logger" entries in the code to assist the questioner in understanding how the code works.
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var name = sheet.getName();
if (name != 'Selector') return;
var range = sheet.getActiveRange();
var col = range.getColumn();
var dropdownrow = range.getRow(); // added for debugging and informationm
if (col != 1) return;
var val = range.getValue();
Logger.log("the cursor is in 'Selector' in cell = " + range.getA1Notation()); //DEBUG
Logger.log("That's row " + dropdownrow + ", and column " + col + ". The value selected = " + val); // DEBUG
var dv = ss.getSheetByName('KAT');
var data = dv.getDataRange().getValues();
var catCol = data[0].indexOf(val);
var list = [];
var KAT_data = dv.getDataRange();
var KAT_data_len = KAT_data.getLastRow(); // added to give 'for' loop a sensible range
Logger.log("The data range on KAT is " + KAT_data.getA1Notation() + ", and the last row of data = " + KAT_data_len); //DEBUG
Logger.log("KAT data = '" + data + "'"); // DEBUG
Logger.log("Found the dropdown cell value of '" + val + "' in KAT as item #" + catCol); //DEBUG
for (var i = 1, len = KAT_data_len; i < len; i++) { // Irrelevant because the data validation range is obtained by defining a range on KAT
// Problem is here, the unique command in A2 creates a blank row
// Logger.log("i="+i+", data = "+data[i][catCol]); // DEBUG
list.push(data[i][catCol]);
}
var listRange = dv.getRange(2, catCol + 1, dv.getLastRow() - 1, 1);
Logger.log("FWIW, this is the list after the loop= " + list); // DEBUG
Logger.log("The contents for the new data validation range (taken from KAT) is " + listRange.getA1Notation()); // DEBUG
Logger.log("The new validation range gets added to col = " + (col + 1)); // DEBUG
//var cell = sheet.getRange(range.getRow(), col-1); // governs the next validation range. Example validation worked right to left, but this sheet works left to right. So must ADD 1, not subtract 1.
var cell = sheet.getRange(range.getRow(), col + 1);
Logger.log("The cell to be assigned the new validation range will be " + cell.getA1Notation()); // DEBUG
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(listRange).build(); // Build validation rule
cell.setDataValidation(rule); // assign validation range to new cell
}
Is this code worthwhile?
The code, as written and referenced, is limited to creating only one level of dependant dropdowns. To this extent it has very limited value. A different approach to creating dependant dropdowns is justified.
"How do you do dynamic / dependent drop downs in Google Sheets?" on StackOverflow has been a meeting place for discussing and updating techniques for dynamic dependant dropdowns since 2014. The latest update was in February 2018 by Max Makhrov. Thye code described here may be useful for the questioner.

Is there a way to mass input data validation in google sheets

I'm trying to create a drop down menu with contents based on a another cell in the same row. For example if A1 = 'yes' then the drop down in B2 gives you the options of 'yes' or 'no'. I can do this I have the list data set up and to code works. The problem is I need to do this 155 times in 4 different sheets. Is there a faster way to do this than right clicking and editing the data validation rules for each cell. Here's a link to the test sheet I'm working on :
https://docs.google.com/spreadsheets/d/1rd_Ig_wpof9R_L0IiA1aZ9syO7BWxb6jvBhPqG8Jmm4/edit?usp=sharing
You can set data validation rules with a script, as documented here. Here's a reference for starting with Apps scripts.
I wrote a function that does approximately what you described. It works with the range B3:B157 of the sheet '9th grade' in the current spreadsheet. For each of them, it sets the validation rule to be: a value in the same row, columns B and C of sheet 'List Data'. The line with
....... = listData.getRange(i+3, 2, 1, 2);
will need to be modified if the source range of validation is to be different. Here, the parameters are: starting row, starting column, number of rows, number of columns. So, 2 columns starting with the second, in row numbered i+3.
function setRules() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var grade = ss.getSheetByName('9th Grade');
var listData = ss.getSheetByName('List Data');
var range = grade.getRange('B3:B157');
var rules = range.getDataValidations();
for (var i = 0; i < rules.length; i++) {
var sourceRange = listData.getRange(i+3, 2, 1, 2);
rules[i][0] = SpreadsheetApp.newDataValidation().requireValueInRange(sourceRange).build();
}
range.setDataValidations(rules);
}
I land in this issue for a diferent reason: "Just mass DataValidation copy (or update) in one column". Thanks, to user3717023 that bring me a light.
I hope that helps someone this simplification.
function setRules() {
//select spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var leads = ss.getSheetByName('Leads');
//Select correct Datavalidation
var rangeNewDataValidation = leads.getRange('M2:M2');
var rule = rangeNewDataValidation.getDataValidations();
//Copy (or Update) Datavalidation in a specific (13 or 'M') column
var newRule = rule[0][0].copy();
Logger.log(leads.getMaxRows())
for( var i=3; i <= leads.getMaxRows(); i++){
var range = leads.getRange(i, 13);
range.setDataValidations([[newRule.build()]]);
}
}

Google charts column filtering

I am trying to filter a Google line chart columns and using the code shared here in Google Charts-Code for Category Filter
It all works well however I have a number of columns and would like to have the chart start with just one column displayed and allow the user to add in any of the additional columns as needed.
What I've found is that if I play with the initState variable to set it to the one column I want to display initially, it will have that column shown in the selector section but still displays all the columns initially until I select an additional column when it hides the rest and just displays the two I have selected.
So then I tried turning off this part of the code:
// put the columns into this data table (skip column 0)<br>
for (var i = 1; i < data.getNumberOfColumns(); i++) {
columnsTable.addRow([i, data.getColumnLabel(i)]);
initState.selectedValues.push(data.getColumnLabel(i));
}
and replacing it with
columnsTable.addRow([1, data.getColumnLabel(16)]);
initState.selectedValues.push(data.getColumnLabel(16));
which sets the column i'm after (column 16) as the selected column in the selection list but removes the other columns from the list of available columns and still displays all 16 columns.
How can I set this up so it displays the single selected column's data initially yet still gives the ability to pick other columns from the selector?
You want to keep the columnstable.addRow call inside the for loop, as it populates the DataTable used to provide the list of columns. You can set the selectedValue variable as you have it:
// put the columns into this data table (skip column 0)<br>
for (var i = 1; i < data.getNumberOfColumns(); i++) {
columnsTable.addRow([i, data.getColumnLabel(i)]);
}
initState.selectedValues.push(data.getColumnLabel(16));
In order to make the chart draw properly with the initial selection, we need to make a small adjustment to the structure, putting all of the updating code into its own function, and then calling that as necessary:
function setChartView () {
var state = columnFilter.getState();
var row;
var view = {
columns: [0]
};
for (var i = 0; i < state.selectedValues.length; i++) {
row = columnsTable.getFilteredRows([{column: 1, value: state.selectedValues[i]}])[0];
view.columns.push(columnsTable.getValue(row, 0));
}
// sort the indices into their original order
view.columns.sort(function (a, b) {
return (a - b);
});
chart.setView(view);
chart.draw();
}
google.visualization.events.addListener(columnFilter, 'statechange', setChartView);
setChartView();
Here's a working example: http://jsfiddle.net/asgallant/WaUu2/157/.

Resources