Friday, May 20, 2016

Google Sheet: Create Dynamic/Nested DropDown List

Step by step on how to create dynamic or nested dropdown list in google sheet.
You can create ordinary dropdown list in Google Sheet. But, how about creating sub-category dropdown list? Google Sheet does not provide capability to do this. You have to create it by your own.

So, in this tutorial, I will guide you on how to create nested dropdown list like image below:

Credit: This is an improved version of Max Makhrov which fix bug when there is same sub-category name across parent categories.

To start, you have to create sheet contain category data.

1Click "Add Sheet" on lower left corner of window.

2Right click on new sheet and click "Rename".

3You can name it to be anything you want.

4Inside this sheet, you can set header to be anything you want.

5Then add category data into it.

6For the next 2 columns after the rightmost, set column title = "ROW NUMBER".

7Insert row number into this column. This column must match with row number of the sheet like this.

8Select categories column.

9Click "Data" => "Sort range ...".

10Set sort condition to sort from first column to last column. Then click "OK".

11The category data will be sorted.

Now, you finished creating category sheet. Next step is to create sub-category dropdown list.

12Go back to first sheet.

13Select the cell you want to create dropdown list.

14Right click and click "Data validation ...".

15Click "Select data range" button.

16Then select the first column data of your category sheet, and click "OK".

17The result will be like this. Click "Save".

18The dropdown list will be generated.

At this time, you can only select the first category. Next step will guide you on how to created dynamic dropdown list.

19Click "Tools" => "Script editor ...".

20Delete all default code in the file.

21Paste this code
function SmartDataValidation(event, TargetSheet, LogSheet, NumOfLevels, lcol, lrow) 
{
  //--------------------------------------------------------------------------------------
  // The event handler, adds data validation for the input parameters
  //--------------------------------------------------------------------------------------
  
  // ===================================   key variables  =================================
  //
  //  ss   sheet we change (TargetSheet)
  //   br    range to change
  //   scol   number of column to edit
  //   srow   number of row to edit 
  //   CurrentLevel level of drop-down, which we change
  //   HeadLevel  main level
  //   r    current cell, which was changed by user
  //   X           number of levels could be checked on the right
  //
  //  ls   Data sheet (LogSheet)
  //
  //    ======================================================================================
  
  
  // [ 01 ].Track sheet on which an event occurs
  var ts = event.source.getActiveSheet();
  var sname = ts.getName();
  
  if (sname == TargetSheet) 
  {
    
    // ss -- is the current book
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    
    // [ 02 ]. If the sheet name is the same, you do business...
    var ls = ss.getSheetByName(LogSheet); // data sheet
    
    // [ 03 ]. Determine the level
    
    //-------------- The changing sheet --------------------------------
    var br = event.source.getActiveRange();
    var scol = br.getColumn(); // the column number in which the change is made
    var srow = br.getRow() // line number in which the change is made
    // Test if column fits
    if (scol >= lcol) 
    {
      // Test if row fits
      if (srow >= lrow) 
      {  
        var CurrentLevel = scol-lcol+2;
        // adjust the level to size of
        // range that was changed
        var ColNum = br.getLastColumn() - scol + 1;
        CurrentLevel = CurrentLevel + ColNum - 1; 
        
        // also need to adjust the range 'br'
        if (ColNum > 1) 
        {
          br = br.offset(0,ColNum-1);
        } // wide range
        
        var HeadLevel = CurrentLevel - 1; // main level
        
        // split rows
        var RowNum = br.getLastRow() - srow + 1;
        
        var X = NumOfLevels - CurrentLevel + 1;

        
        // the current level should not exceed the number of levels, or 
        // we go beyond the desired range
        if (CurrentLevel <= NumOfLevels ) 
        {
          // determine columns on the sheet "Data"
          var KudaCol = NumOfLevels + 3;
          var KudaNado = ls.getRange(1, KudaCol);
          var lastRow = ls.getLastRow(); // get the address of the last cell
          var ChtoNado = ls.getRange(1, KudaCol, lastRow, KudaCol);

          // ============================================================================= > loop >    
          for (var j = 1; j <= RowNum; j++)
          {  
            for (var k = 1; k <= X; k++) 
            {
               
              HeadLevel = HeadLevel + k - 1; // adjust parent level
              CurrentLevel = CurrentLevel + k - 1; // adjust current level
              
              var r = br.getCell(j,1).offset(0,k-1,1);
              var SearchText = r.getValue(); // searched text

              // if anything is choosen!
              if (SearchText != '') 
              {
                
                //(Piyapan added) get first row number that match the category
                var code = "2";
                var rowNumberCol = NumOfLevels + 2;
                for (var i = 1; i < CurrentLevel; i++) 
                {
                  var selectCategory = ts.getRange(srow+j-1, lcol+i-1, srow+j-1, lcol+i-1).getCell(1,1).getValue();
                  code = 'VLOOKUP("' + selectCategory + '",INDIRECT("R" & (' + code + ') & "C' + i + ':R' + lastRow + 'C' + rowNumberCol +'",0), '+ (rowNumberCol-i+1) +', FALSE)';
                }
                code = 'IFERROR(' + code + ', 0)';
                
                KudaNado.setFormulaR1C1(code);
                var startRowNumber = ChtoNado.getCell(1,1).getValue();
                
                if(startRowNumber > 0)
                {
                  var categoryRange = ls.getRange(1, CurrentLevel-1, lastRow, CurrentLevel-1);
                  var firstValue = categoryRange.getCell(startRowNumber,1).getValue();
                  var categoryCount = 1;
                  for (var i = startRowNumber+1; i <= lastRow; i++) 
                  {
                    var value = categoryRange.getCell(i,1).getValue();
                    
                    if(firstValue != value)
                      break;
                    
                    categoryCount++;
                  }
                  
                  // Piyapan the formula
                  code = 'UNIQUE(INDIRECT("R' + startRowNumber + 'C' + CurrentLevel + ':R'+ (categoryCount+startRowNumber-1) + 'C' + CurrentLevel+'",0))';
                  
                  //-------------------------------------------------------------------
                  
                  // [ 04 ]. define variables to costumize data
                  // for future data validation
                  //--------------- Sheet with data --------------------------           
                  // combine formula 
                  // repetitive parts
                  
                  
                  //var IndCodePart = 'INDIRECT("R1C' + HeadLevel + ':R' + lastRow + 'C';
                  //IndCodePart = IndCodePart + HeadLevel + '",0)';
                  // the formula
                  // code = '=UNIQUE(INDIRECT("R" & MATCH("';
                  // code = code + SearchText + '",';
                  // code = code + IndCodePart;
                  // code = code + ',0) & "C" & "' + CurrentLevel
                  // code = code + '" & ":" & "R" & COUNTIF(';
                  // code = code + IndCodePart;   
                  // code = code + ',"' + SearchText + '") + MATCH("';
                  // code = code + SearchText + '";';
                  // code = code + IndCodePart;
                  // code = code + ',0) - 1'; 
                  // code = code + '& "C" & "' ;   
                  // code = code + CurrentLevel + '",0))';
                  // Got it! Now we have to paste formula
                  
                  KudaNado.setFormulaR1C1(code);   
                  
                  // get required array
                  var values = [];
                  for (var i = 1; i <= lastRow; i++) 
                  {
                    var currentValue = ChtoNado.getCell(i,1).getValue();
                    if (currentValue != '') 
                    { 
                      values.push(currentValue);
                    } 
                    else 
                    {
                      var Variants = i-1; // number of possible values
                      i = lastRow; // exit loop
                    }       
                  }
                  //-------------------------------------------------------------------
                  
                  // [ 05 ]. Build daya validation rule
                  var cell = r.offset(0,1);
                  var rule = SpreadsheetApp
                  .newDataValidation()
                  .requireValueInList(values, true)
                  .setAllowInvalid(true)
                  .build();
                  cell.setDataValidation(rule); 
                  if (Variants == 1) 
                  {
                    cell.setValue(KudaNado.getValue());  
                  } // the only value
                  else
                  {
                    k = X+1;
                  } // stop the loop through columns
                  
                }
                else
                {
                  // kill extra data validation if there were 
                  // columns on the right
                  if (CurrentLevel <= NumOfLevels ) 
                  {
                    for (var i = 1; i <= NumOfLevels; i++) 
                    {
                      var cell = r.offset(0,i);
                      // clean
                      //cell.clear({contentsOnly: true});
                      // get rid of validation
                      cell.clear({validationsOnly: true});
                    }
                  } // correct level
                } // empty row
                
              } // not blanc cell
              else
              {
                // kill extra data validation if there were 
                // columns on the right
                if (CurrentLevel <= NumOfLevels ) 
                {
                  for (var i = 1; i <= NumOfLevels; i++) 
                  {
                    var cell = r.offset(0,i);
                    // clean
                    //cell.clear({contentsOnly: true});
                    // get rid of validation
                    cell.clear({validationsOnly: true});
                  }
                } // correct level
              } // empty row
            } // loop by cols
          } // loop by rows
          // ============================================================================= < loop < 
          
        } // wrong level
        
      } // rows
    } // columns... 
  } // main sheet
}

function isNumber(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}

function removeDuplicates() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var newData = new Array();
  for(i in data){
    var row = data[i];
    var duplicate = false;
    for(j in newData){
      if(row.join() == newData[j].join()){
        duplicate = true;
      }
    }
    if(!duplicate){
      newData.push(row);
    }
  }
  sheet.clearContents();
  sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}

function onEdit(event) 
{
  // Declare some variables:
  //--------------------------------------------------------------------------------------
  var TargetSheet = 'Sheet1' // name of the sheet where you want to verify the data
  var LogSheet = 'Category' // name of the sheet with information
  var NumOfLevels = 3 // number of levels in LogSheet
  var lcol = 2; // number of the leftmost column in TargetSheet, in which the changes are checked; A = 1, B = 2, etc.
  var lrow = 4; // line number in TargetSheet from which the rule will be valid
  //--------------------------------------------------------------------------------------
  
  SmartDataValidation(event, TargetSheet, LogSheet, NumOfLevels, lcol, lrow);
}

22In function onEdit(event), edit the variables as follows:
  • Target Sheet: Name of the sheet where you want to verify the data. 
  • LogSheet: Name of the sheet with information. 
  • NumOfLevels: Number of levels in LogSheet. 
  • lcol: Number of the leftmost column in TargetSheet, in which dropdown list will applied; (A = 1, B = 2, etc.). 
  • lrow: Line number in TargetSheet from which dropdown list will applied.

23Click "File" => "Save" button.

24You can name the file to be anything you want. Click "OK".

25Now everything is setup! Go back to first sheet, you can now select drop down list and see that sub-category dropdown list is generated.




Note: If you want to add more category data. Make sure that category data is sorted (see step 7 - 11).



Happy Coding!

2 comments:

  1. How can I insert an offset between the first dropdown and the next ones (have a few columns betwee)

    ReplyDelete
  2. Can we utilize this in multiple sheets within the same worksheet?

    ReplyDelete