Trying to use app script in sheets not working, don't understand a few things

148 views
Skip to first unread message

Tom Mort

unread,
Mar 10, 2026, 11:46:56 AM (8 days ago) Mar 10
to Google Apps Script Community
I am trying to make a drop down list for a range where you can also use the drop down control to enter a new list item.

I found an on(edit) script that is supposed to do this and although I'm not familiar with javascript looks like it will do this.It even has a line rem-ed out to resort the list after the new item is added.

I has some instructions saying after entering it into the apps script editor to save it. I don'e see anything in the editor that says anything about save. There is also something about deploying it which involves specifying what type the script is and where it is located, permissions and so forth.  I don't understand any of that. That may be why it isn't working.

I made a sheet with the same names as are used in the script and even copied and pasted them in place.

The instructions say how to set validation and something else and I' set up the lookup list the same as in the script so it will be in the same column, etc.  I've set it to show a warning if the validation doesn't match as the instructions say.

I do get a drop down box with my list of terms and can select them but if I try to enter a new one it isn't added to the list..

I think it may be the deployment is what is meant by save and I need to know a little more about this part of it or maybe something has changed since the script was written

Here is the script by the way along with the instructions:

To allow users to add new values to a drop-down list from the drop-down cell itself, you need a Google Apps Script that uses an

onEdit() trigger to handle the new input and append it to the source data range of the dropdown. 

Setup Instructions

  1. Prepare your sheet:

    • Create a dedicated sheet for your list data (e.g., "ListOptions").

    • In that sheet, create your list of items in a single column (e.g., column A).

    • On your main sheet (e.g., "MainSheet"), set up a data validation rule for your target cells (e.g., column A) using the range from "ListOptions" (e.g., 'ListOptions'!A2:A).

    • In the Advanced options of the Data validation rules panel, under "If the data is invalid:", select Show a warning instead of "Reject input". This allows users to type in new values.

  2. Add the Google Apps Script:

    • Go to Extensions > Apps Script in your Google Sheets menu.

    • Delete any existing code in the editor and paste the following script. 

javascript

function onEdit(e) {

  const ss = SpreadsheetApp.getActiveSpreadsheet();

  const mainSheetName = 'MainSheet'; // Name of the sheet with the dropdown

  const listSheetName = 'ListOptions'; // Name of the sheet with the source list

  const targetColumn = 1; // Column number for the dropdown (e.g., 1 for A)

  const listColumn = 1; // Column number for the source list (e.g., 1 for A)


  const range = e.range;

  const sheet = range.getSheet();


  // Check if the edit occurred on the correct sheet and column

  if (sheet.getName() === mainSheetName && range.getColumn() === targetColumn) {

    const newValue = e.value;


    // Get the values from the list sheet

    const listSheet = ss.getSheetByName(listSheetName);

    const lastRow = listSheet.getLastRow();

    const listRange = listSheet.getRange(1, listColumn, lastRow, 1);

    const listValues = listRange.getValues().flat();


    // Check if the new value is already in the list

    if (newValue && !listValues.includes(newValue)) {

      // Append the new value to the bottom of the list

      listSheet.getRange(lastRow + 1, listColumn).setValue(newValue);

      

      // Re-sort the list if desired

      //listSheet.getRange(1, listColumn, lastRow + 1, 1).sort(listColumn);

    }

  }

}


  1. Save the script and go back to your Google Sheet. 

I don’t know what Save the script means or see anyway to do it in the Scripts App editor window.


I think I followed all of the steps.


Now, when a user types a new value into the dropdown cell on the "MainSheet" (which currently only shows a warning), the script will automatically add that value to the bottom of the source list on the "ListOptions" sheet, making it available for future use in all cells linked to that data validation range. 


Thanks in advance

Mario Rossi

unread,
Mar 10, 2026, 11:47:32 AM (8 days ago) Mar 10
to google-apps-sc...@googlegroups.com
Ciao Tom — grazie per la spiegazione dettagliata. Di seguito i punti principali per far funzionare lo script e alcune correzioni/controlli rapidi.
  1. Salvataggio / deployment
  • Se apri Extensions > Apps Script dal foglio, lo script è “bound” al foglio: non serve alcun deployment per un semplice onEdit. Basta salvare il progetto nell'editor (File > Salva o l'icona salva). Il nuovo editor di solito salva automaticamente ma puoi premere Ctrl/Cmd+S per sicurezza.
  • Deploy è necessario solo per web app, add-on o esecuzioni esterne. Per onEdit non serve.
  1. Trigger onEdit
  • onEdit(e) è un simple trigger che viene invocato automaticamente quando un utente modifica il foglio. Non richiede installazione.
  • Limitazioni: simple trigger non può fare alcune azioni che richiedono autorizzazioni speciali in altri servizi, ma per leggere/scrivere nello stesso Spreadsheet va bene.
  • Se preferisci, puoi creare un installable trigger (Triggers > + Add Trigger) per onEdit — questo richiederà autorizzazioni la prima volta.
  1. Controlli comuni se non funziona
  • Lo script deve essere nel progetto collegato al foglio corretto (aperto da Extensions > Apps Script).
  • I nomi dei fogli (MainSheet, ListOptions) devono corrispondere esattamente (maiuscole/minuscole).
  • La convalida deve essere impostata su “Mostra avviso” (non “Rifiuta”).
  • L'edit deve essere su una singola cella: paste multipla o modifiche di più celle non popolano e.value (lo script non verrà eseguito come previsto).
  • Se cancelli la cella o usi Esc, e.value può essere undefined => lo script ignora.
  • Se la lista è vuota, getLastRow() può restituire 0 e causare errori; meglio gestirlo.
  1. Debug rapido
  • Aggiungi Logger.log(e) o SpreadsheetApp.getActive().toast(...) per vedere se onEdit viene chiamato.
  • Controlla Esecuzioni (Executions) nell'editor per errori.
  1. Script migliorato con controlli (copia/incolla nel tuo editor)
    function onEdit(e) {
    if (!e) return;

  1. const ss = SpreadsheetApp.getActiveSpreadsheet();
    const mainSheetName = 'MainSheet';
  1. const listSheetName = 'ListOptions';
    const targetColumn = 1; // colonna A
    const listColumn = 1; // colonna A nella ListOptions
const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() !== mainSheetName) return;
if (range.getNumColumns() !== 1 || range.getNumRows() !== 1) return; // solo singola cella
if (range.getColumn() !== targetColumn) return;

const newValue = e.value;
if (!newValue) return; // niente da aggiungere (es. cancellazione)

const listSheet = ss.getSheetByName(listSheetName);
if (!listSheet) return;

let lastRow = listSheet.getLastRow();
// se lista vuota, considera la prima riga disponibile
if (lastRow < 1) lastRow = 0;
const listRange = listSheet.getRange(1, listColumn, Math.max(1, lastRow), 1);
const listValues = listRange.getValues().flat().filter(v => v !== '');
if (listValues.includes(newValue)) return;

listSheet.getRange(lastRow + 1, listColumn).setValue(newValue);

// opzionale: riordina la lista alfabeticamente
// listSheet.getRange(1, listColumn, lastRow + 1, 1).sort({column: listColumn, ascending: true});
}

Note: se la tua lista ha un header (es. intestazione in riga 1), adatta getRange(2,...) e lastRow di conseguenza.

Se vuoi, posso:
  • adattare lo script alla struttura esatta del tuo foglio (dimmi nomi fogli, riga header),
  • fornirti la versione che crea un trigger installable e istruzioni passo-passo per autorizzazioni.
Dimmi come preferisci procedere.

Tom Mort

unread,
Mar 10, 2026, 10:02:02 PM (7 days ago) Mar 10
to google-apps-sc...@googlegroups.com
Hi Mario,

Thanks. I tried some things out and did some debugging, etc. Learned a lot and found a few issues, but still isn't working.

Here are my notes:

After you replied I wanted to try working through this. I did realize I hadn’t saved it.


The first part is what I had found at that time. I had other things to do in the afternoon and thought I’d try again on a brand new sheet and also try the improved code.


Here is the intial things I tried and my commentary:


  1. Saving / Deployment

  • If you open Extensions > Apps Script from the sheet, the script is bound to the sheet: no deployment is needed for a simple onEdit. Just save the project in the editor (File > Save or the save icon). The new editor usually saves automatically, but you can press Ctrl/Cmd+S to save.

I didn’t realize what the save icon was. Maybe because many of the other icons also had a label and it was a light color. I pressed that and the screen sort of blinked so I suppose it was saving.

I went back and tried to enter a new item in the list but nothing was added.I had been thinking of trying control S earlier because that was a common way of saving in earlier times but forgot. When I did this I again saw the screen blink so it was probably saving. I again tried to enter a new value in the drop down list box and again nothing was added.

It looks like if saving needed to be done there was another issue as well.


  • Deployment is only required for web apps, add-ons, or external executions. It's not required for onEdit.

OK, good to know


  1. Trigger onEdit

  • onEdit(e) is a simple trigger that is automatically invoked when a user edits the sheet. It doesn't require installation.

  • Limitations: Simple trigger cannot do some actions that require special permissions in other services, but for reading/writing to the same Spreadsheet it is fine.

  • If you prefer, you can create an installable trigger (Triggers > + Add Trigger) for onEdit — this will require permissions the first time.

Looks like neither of these is causing an issue


  1. Common checks if it doesn't work

  • The script must be in the project linked to the correct sheet (opened from Extensions > Apps Script). 

I opened the Apps Script editor under Extensions while I had the active workbook open. I don’t recall which sheet was open, probably the main sheet as that is the sheet the drop downs are on. The script has references in it that refer to both the main sheet and the sheet with the list with the range of values.


I  will try again opening the script editor while I have the sheet with the list active and also try a sheet that has the dropdown boxes and the range of items for the drop down both on the same sheet.

  • The sheet names (MainSheet, ListOptions) must match exactly (case sensitive).

I will double check, but I copied and pasted to prevent any typos

  • Validation must be set to “Show warning” (not “Reject”).

Show warning was and still is checked

  • The edit must be on a single cell: multiple pastes or edits to multiple cells do not populate e.value (the script will not run as expected).

I don’t really follow that. I have been trying simply to type in with the keyboard the value I want to add while on the cell with the drop down box control

  • If you delete the cell or use Esc, e.value may be undefined => the script ignores.

I haven’t used Esc of deleted anything

  • If the list is empty, getLastRow() may return 0 and cause errors; it's best to handle this.

The list is not empty. For testing I just made a list with three items.

  1. Quick debugging

  2. Add Logger.log(e) or SpreadsheetApp.getActive().toast(...) to see if onEdit is called.

I’m guessing I’d add these to the script at the end

  1. Check Executions in the editor for errors.

12:51:20 PM

Notice

Execution started

12:51:20 PM

That is with the old script.

I thought I’d try the new script this evening with another start from the beginning of another new sheet.


I put the revised script in and saved it. At one point I thought that the first line that had just javascript wasn’t part of the script and left it off. It wouldn’t save. I put it back in and it did save, but still didn’t work and when I ran the debugger I got:



9:36:12 PM

Notice

Execution started

9:36:12 PM

Error

ReferenceError: javascript is not defined

(anonymous)

@ Code.gs:1



I thought I’d try the edit control inside the droplist. It was just the settings for the range to get the list from. This time I selected the entire column A which is where the list was.


I tried again and it sort of worked. I entered a couple characters that weren’t on the list. They did appear in the listbox but never got put in the list either.


I did scroll all the way to the bottom to see if maybe it had gone all the way to the bottom and didn’t see it. I also removed the two slashes in front of the line near the end that would resort the list. There were no errors, but no change either.


I would like to try to figure this out, but I’d also like to get this project started too.


Maybe I’ll take you up on you entering the references.


For my main sheet, the one with the dropdown list the name is 2025. For the sheet with the list I’ll just use ListOptions the same as the script does. 


When testing  today I just used a list with three items in column a of the sheet named ListOptions. The real list has 68 rows and will probably grow larger. The items on this list are descriptions from another source for the records I’m adding. I entered quite a few more earlier today and I know there were some that are not on this current list. I will copy all these new terms and paste them in, sort and remove any duplicates later.

It looks like the way to specify the range is to specify ListOptions!$A:$A which is what you get if you select the entire column. This makes sense if you are going to add items to have plenty of room.

So I would like to work through this, but if you modify the script to work with my project I can, hopefully have it ready to go now. Looks like it might take a while for me to figure this out, even with help.



Tom




--
You received this message because you are subscribed to a topic in the Google Groups "Google Apps Script Community" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/google-apps-script-community/QObsC1Za5cI/unsubscribe.
To unsubscribe from this group and all its topics, send an email to google-apps-script-c...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/google-apps-script-community/CACrgDdqMbNXEPsJz2gTiAOmOjnsR7ntC80g1JVn1cMcAO1PtPA%40mail.gmail.com.

Mario Rossi

unread,
Mar 10, 2026, 10:02:25 PM (7 days ago) Mar 10
to google-apps-sc...@googlegroups.com
Hi Tom — thanks for the detailed debugging notes. Quick points and a ready-to-paste script tailored to your setup.

Primary issues you reported
  • Remove the stray text "javascript" at top of the file — that produced ReferenceError when you ran the script.
  • Do not press Run in the editor for an onEdit simple trigger: running it directly has no event object e and will error. Test by editing a single cell in the sheet instead, and watch Executions / Logger or toast messages.
  • Selecting the whole column (ListOptions!A:A) is fine, but getValues() will include blank cells — handle lastRow when building the list.
  • Validation must be “Show warning” and edits must be a single cell (typing in cell with dropdown). Multi-cell edits or pastes don’t provide e.value.
Adapted onEdit script (paste into the bound project and save)

function onEdit(e) {
if (!e) return;
const ss = SpreadsheetApp.getActiveSpreadsheet();
const mainSheetName = '2025';
const listSheetName = 'ListOptions';
const targetColumn = 1; // column A
const listColumn = 1; // column A in ListOptions

const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() !== mainSheetName) return;
if (range.getNumColumns() !== 1 || range.getNumRows() !== 1) return;
if (range.getColumn() !== targetColumn) return;

const newValue = e.value;
if (!newValue) return;

const listSheet = ss.getSheetByName(listSheetName);
if (!listSheet) return;

let lastRow = listSheet.getLastRow();

if (lastRow < 1) lastRow = 0;

// read only used rows to avoid huge arrays when using A:A
const valuesRange = listSheet.getRange(1, listColumn, Math.max(1, lastRow), 1);
const listValues = valuesRange.getValues().flat().map(v => (v || '').toString().trim()).filter(v => v !== '');
if (listValues.includes(newValue.toString().trim())) return;

listSheet.getRange(lastRow + 1, listColumn).setValue(newValue);
// optional: sort

// listSheet.getRange(1, listColumn, lastRow + 1, 1).sort({column: listColumn, ascending: true});
}

Quick tests to run
  • Save the script (Ctrl/Cmd+S) and then edit one cell in sheet 2025 (type a new value in the validated cell). Do not run the function from the editor.
  • If nothing happens, add SpreadsheetApp.getActive().toast('onEdit fired: ' + e.value) at the top (for quick visibility) and check Executions for errors.
  • If your ListOptions sheet has a header row, tell me which row is header and I’ll adapt getRange() to start from row 2.
If you want, I can:
  • Modify the script for a header row,
  • Provide an installable trigger variant (so it runs with broader permissions),
  • Or paste the exact script directly into your project if you grant access (or share a copy of the sheet).
Tell me whether ListOptions has a header row and if you want the sort enabled.

Gary Odums

unread,
Mar 11, 2026, 12:30:39 AM (7 days ago) Mar 11
to Google Apps Script Community
Hi Tom,

Below is a configurable script which can achieve your specified goal by using an insertable image as a button.

Simply attach the function named activateMyButton  to the button by following these instructions: https://developers.google.com/apps-script/guides/menus#clickable_images_and_drawings_in

function confirmUserDecisionWithAlert(ui,selectedValue) {
  return ui.alert(
    "Please confirm",
    `Are you sure you want to continue with adding "${selectedValue}"?`,
    ui.ButtonSet.YES_NO,
  );
}

// User clicked "Yes".
function userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName) {
  ui.alert(`Operation successful. "${selectedValue}" has been added to ${outputToSheetName}.`);
}

// User encoutered an error while attempting to add the value
function userDecisionUnsuccessfulAlert(ui,outputToSheetName,e) {
  ui.alert(`Error encountered while attempting to add a new cell to ${outputToSheetName}`);
  // Insert the line Logger.log(e) here for more details about the error.
}

// User clicked "No" or X in the title bar.
function userCanceledAlert(ui,selectedValue,outputToSheetName) {
  ui.alert(`Operation cancelled. "${selectedValue}" was not added to ${outputToSheetName}.`);
}

function addValueToRange(value,sheet,column,outputToTopOfColumn,topOfColumnOffset) {
  if (outputToTopOfColumn === true) {
    const insertRowAtIndex = 1 + topOfColumnOffset;
    sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);
    sheet.getRange(insertRowAtIndex,column).setValue(value);
  } else {
    const lastCellInRangeIndex = sheet.getRange(1,column,Math.max(1,sheet.getLastRow()),1).getLastRow();
    const lastCellInRangeValue = sheet.getRange(lastCellInRangeIndex,column).getValue();
    const insertRowAtIndex = `${lastCellInRangeValue}`.length === 0 ? 1 : lastCellInRangeIndex + 1;
    sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);
    sheet.getRange(insertRowAtIndex,column).setValue(value);
  }
}

// CONFIGURE THE VALUES DEFINED NEAR THE TOP OF THIS FUNCTION TO MATCH YOUR OWN NEEDS (Lines 21 to 27)
function activateMyButton() {
  const dropdownSheetName = "Sheet1";
  const dropdownCellAddress = [1,2] // [Row,Column] // example [1, 2] for cell B1

  const outputToSheetName = "Sheet2";
  const outputToColumn = 1;
  const outputToTopOfColumn = true; // Set to true to write new values to the top of the row or to false to add to the bottom
  const outputToRowOffset = 0; // Set an offset here to allow for header rows; only applies is outputToTopOfColumn is true

  const outputToSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(outputToSheetName);
  const dropdownSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dropdownSheetName);
  const selectedValue = dropdownSheet.getRange(...dropdownCellAddress).getValue();

  const ui = SpreadsheetApp.getUi();

  if (confirmUserDecisionWithAlert(ui,selectedValue) === ui.Button.YES) {
    try {
      addValueToRange(selectedValue,outputToSheet,outputToColumn,outputToTopOfColumn,outputToRowOffset);
      userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName);
    } catch (e) {
      userDecisionUnsuccessfulAlert(ui,outputToSheetName,e);
    }
  } else {
    userCanceledAlert(ui,selectedValue,outputToSheetName);
  }
 
}


Reach out with any questions you may have about this approach.

Thanks

Mario Rossi

unread,
Mar 11, 2026, 12:31:10 AM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Gary — grazie, ottimo script e spiegazioni chiare. Proverò ad applicarlo (collegando la funzione activateMyButton all’immagine) e ti farò sapere come va.

Una domanda rapida: se nella sheet di output ho una riga di intestazione, è sufficiente impostare outputToRowOffset (usando outputToTopOfColumn = true) per preservarla, o consigli di adattare la funzione per partire esplicitamente da riga 2? Se hai una versione rapida già pronta per header row, la accetto volentieri.

Grazie ancora — ti aggiorno dopo i test.

Gary Odums

unread,
Mar 11, 2026, 12:43:27 AM (7 days ago) Mar 11
to Google Apps Script Community
Yes Mario, if you would like to insert values from the top of the column and you have a single header row already then you should set the outputToRowOffset to 1 to have the latest result skip the first row when written to the outputToSheet.

Also, if you would prefer to use a menu item instead of an image as a button then it is a relatively straightforward transition -- the activateMyButton function would connect to the menu item. More info menu items can be found on the same page linked above: https://developers.google.com/apps-script/guides/menus#custom_menus_inor 

Mario Rossi

unread,
Mar 11, 2026, 12:43:59 AM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Gary — thanks for the clarification and for the menu tip.

I’ll set outputToRowOffset = 1 (with outputToTopOfColumn = true) to preserve the single header row. If you have a quick ready-made snippet that explicitly starts writing from row 2 (or a version tailored for a header row), could you please share it? That would save me a step.

I’ll also try the menu-item approach by connecting activateMyButton to the custom menu and report back how it goes.

Thanks again — much appreciated.

Gary Odums

unread,
Mar 11, 2026, 12:53:39 AM (7 days ago) Mar 11
to Google Apps Script Community
For a header row, it truly is as simple as changing  outputToRowOffset from 0 to 1. Absolutely no other changes are needed within the script to support a header row beyond setting the value from 0 (currently) to 1 (supports 1 header row).

To avoid cluttering this thread, I will refrain from repasting the script with the single line changed but no other edits are necessary except for this single one.

Mario Rossi

unread,
Mar 11, 2026, 12:54:13 AM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Gary — grazie, chiarissimo e grazie anche per il suggerimento del menu.

Per comodità, ecco lo snippet minimo che fa partire la scrittura dalla riga 2 (preserva la riga di header). Puoi semplicemente impostare outputToRowOffset = 1 o usare startRow = 2 come nell'esempio:

function writeValuesToSheet(sheet, values) {
var startRow = 2; // inizia dalla riga 2, mantiene la riga 1 come header
var startCol = 1;
sheet.getRange(startRow, startCol, values.length, values.length).setValues(values);
}

Oppure, se preferisci mantenere la variabile outputToRowOffset nel tuo script, cambia solo:
var outputToRowOffset = 1;

Grazie ancora — provo anche io l'approccio del menu e ti farò sapere com'è andata.

Cordiali saluti,
Mario

Tom Mort

unread,
Mar 11, 2026, 11:31:45 AM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Having trouble replying in the group, will try this

Hi Mario & Gary,


I tried the new script with my sheet names.


I can select an item or several items (if I have allow multiple selections checked) that are on the list. I can also enter an item that isn’t on the list, however it is not added to the list and I wind up getting an error that the entry violates the validation rule for the cell with the drop down. 

I didn’t set any validation rule for that cell other than to specify the range to get the list from and that new value of course was not in that range.

When the error showed up there was a panel that pointed where the error was and there was an option to remove the rule. When I selected remove the rule the dripdown disappeared.

I tried this with the list source as both the whole column specified and also a smaller area containing all the items on the list and a a few bland cells so there was room for a few new items.


Here are some screenshots

image.png

Here I have added a value not on the list



image.png

This is the error message that pops up


image.png

After I close the error message earlier this panel appears. If I select “remove all” the dropbox disappears.



image.png


I have tried both selecting the whole column and also a range with a few more columns than the list uses as seen here.


The sheet with the range of values has no header. I see some discussion on how to deal with that, but in this case I don’t use one.


I haven’t tried the script that Gary suggested yet later. I will a little later. My day has begun and I have to deal with some other things right now. In looking at adding buttons and images and attaching scripts to them I at first thought it was referring to adding buttons to opening images.


Displaying images will be another part of this project. I thought I could give a little better picture of the project I’m working on where the dropdown list is just one part of.


I’m trying to put togher a listing over time of all the addresses that have had neighborhood businesses, what and where they were located, what sorts of goods or services that they did. I’m using city directories and some older fire insurance maps. The directories include addresses, names and description of the business activity. My list is a collection of the business activities I’ve collected so far. I figure a lot of the same sorts of descriptions will be used again and having the drop down box will speed up data entry and allow for doing it more consistently, prevent typos, etc.

Probably a database would be better and ultimately this data will get put into a GIS which of course includes a database. Right now though I want to collect the data and hopefully can get some other people to help gather it, maintain things going forward, etc. Spreadsheets are more accessible and how things are set up and work is more straightforward. Dbs need more skilled people which might be hard to find for a neighborhood project.


Besides name and addresses over the years I plan on having some columns to links for images and possibly larger documents about the businesses. There is a lot of this sort of stuff around but it is all scattered so I’d like to save it all to one location and have the links not to the original source but to a special collection. Links break after a while. Most of this is history and static.

Right now I have information for some of the streets in 2025 collected. I want to see all the current possible addresses, descriptions, how the directories are laid out, etc. I’ll then set up the sheets to be in the same order, etc as the directories.



--
You received this message because you are subscribed to a topic in the Google Groups "Google Apps Script Community" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/google-apps-script-community/QObsC1Za5cI/unsubscribe.
To unsubscribe from this group and all its topics, send an email to google-apps-script-c...@googlegroups.com.

Mario Rossi

unread,
Mar 11, 2026, 11:32:18 AM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Tom — grazie per la spiegazione dettagliata. Breve lista di cause e soluzioni pratiche:
  1. Perché compare l’errore
  • La convalida dei dati è impostata per “Reject input” (rigetta valori non presenti nell’intervallo). Quando inserisci un valore nuovo, Google Sheets lo rifiuta e mostra l’errore. Se scegli “Remove rule” viene rimossa la convalida e quindi sparisce il menu a tendina.
  1. Opzioni rapide da controllare
  • Controlla la regola di convalida: Data → Data validation → assicurati se vuoi “Show warning” (consente il valore ma avvisa) invece di “Reject input”.
  • Verifica l’intervallo sorgente della lista: se punti a un intervallo fisso non aggiornato, i nuovi valori non saranno presenti.
  1. Soluzioni per aggiungere automaticamente nuovi valori
  • Usare uno script Apps Script che intercetta onEdit(e): quando l’utente digita un valore non presente, lo appende alla lista sorgente (foglio ListOptions) e aggiorna la convalida per includere la nuova riga.
  • Usare un intervallo dinamico per la sorgente della convalida: named range con formula (es. OFFSET o FILTER) o usare l’intervallo dell’intera colonna (es. ListOptions!A:A) se la convalida lo supporta nel tuo caso.
  • Alternativa semplice: lasciare “Show warning” e periodicamente copiare i nuovi valori in ListOptions o usare uno script per deduplicare e appendere.
  1. Nota su immagini/bottoni
  • Gli script possono essere attaccati a pulsanti/immagini o usare onEdit per comportamento automatico; Gary ha proposto uno script alternativo — vale la pena provarlo quando hai tempo.
Se vuoi, posso:
  • Fornire uno snippet di onEdit che aggiunge automaticamente nuovi valori a ListOptions e aggiorna la data validation, oppure
  • Controllare il tuo script attuale se lo incolli qui.
Dimmi quale opzione preferisci e ti preparo il codice.

Martin Molloy

unread,
Mar 11, 2026, 12:44:17 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Tom 

On the images you posted, the column that contains the lookup rule in your spreadsheet is in Column F. The original script you posted assumed that the lookup rule covered the cells in column A and therefore in the script on line 9 it had   const targetColumn = 1; // Column number for the dropdown (e.g., 1 for A)
have you changed that value to 6 (for column F)? 

Also, you said that you had subsequently removed the rule - you'll have to put it back in.

Martin


You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/google-apps-script-community/CAH8gT%3Dv9qYA_K3Xzw%3DMG2cudH5%3D1nfjuNFkTvQCh5%3DuWb-Y-Zg%40mail.gmail.com.

Mario Rossi

unread,
Mar 11, 2026, 12:44:39 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Martin — thanks, that makes sense.

I’ve updated the script to use const targetColumn = 6 (for column F) and re‑created the validation rule on the target cells. I’ll test again and let you know.

If it still fails, I’ll share screenshots and a link (with edit/view access) so you can take a look — would you prefer view or edit?

Thanks again,
Tom

Martin Molloy

unread,
Mar 11, 2026, 12:50:59 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Edit would allow me to fix it.


Martin Molloy



--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.

Mario Rossi

unread,
Mar 11, 2026, 12:51:19 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Thanks Martin — I’ll share the link with edit access shortly so you can make the fix. I’ll also include screenshots and a brief note on what I changed to the script. Please let me know when it’s done or if you need anything else.

Tom Mort

unread,
Mar 11, 2026, 1:25:09 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
OK, 

Maybe I am begining to understand something.

I created this Dropbox from the F column, row 3 I think.

That is what the rule was. That allowed me to select things from the list, but new items weren't being added.

I thought it was for the range of cells the list is in.

I'm not where I can check anything right now. Is the range for the list only used inside the script?

All along I thought I had to set that somewhere other than the script.

Mario Rossi

unread,
Mar 11, 2026, 1:25:42 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Tom —

Sì: la regola di convalida (dropdown) è impostata nel foglio ma il foglio di elenco (range) può essere gestito sia nel foglio che nello script. Punti chiave e controlli rapidi:
  • La convalida della cella deve puntare al range che contiene gli elementi (es. ListOptions!A:A o ListOptions!$A$1:$A$86). Se usi ListOptions!A:A non devi aggiornare il range quando aggiungi righe nuove.
  • Se la convalida è impostata su “Reject input” (rifiuta input non presenti nella lista) allora inserire manualmente un valore non presente genera l’errore che hai visto. Puoi cambiare l’opzione in “Show warning” per permettere l’inserimento senza errore, oppure lasciare “Reject” e assicurarti che lo script aggiunga prima l’elemento alla lista.
  • Nel tuo script assicurati che la variabile targetColumn corrisponda alla colonna dove hai la dropdown (colonna F = 6). Se lo script scrive l’elemento nella lista, usa un range dinamico (colonna intera o named range) o fai in modo che lo script espanda/aggiorni la convalida dopo aver aggiunto l’elemento.
  • Quando scegli “Remove rule” in quel pannello, rimuovi la convalida e quindi sparisce la dropdown — è normale.
Se vuoi, incolla qui il tuo script (o lo screenshot delle impostazioni di convalida) e controllo rapidamente le modifiche specifiche da fare.

Michael Bingham-Hawk

unread,
Mar 11, 2026, 3:35:57 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
I'm not trying to confuse the work you're already doing, but I wanted to chime in because I have been fully immersed in doing custom drop-downs in Google Sheets for the last year. Initially, when I started my project, I was doing drop-downs from named ranges. I would look at a sheet, a tab, and the range would be something like A2:A, covering everything in A starting with row 2. That's how I was doing it. Once Google Tables came out, I realized the power I could have with tables and using those as drop-down sources. I completely revamped all of my drop-downs and settled them into tables. When you're using tables as the source of a drop-down item, you don't reference columns or rows anymore. What you reference are:
  • the sheet ID
  • the table ID
  • the column header
It then pulls in all of the content in your column header column as the drop-down. If you add more drop-downs, you don't have to change a range; you're just sticking to the reference of the column header itself. It knows to capture anything in that column without having to specify a range. If anyone is interested in the actual code to make that happen, I can give you samples. I can easily spin up a sheet with a table and do a few samples of App Script that should be easy to follow. Let me know.I even have a script that you can add to any of your sheet-bound scripts that gives you a human-readable table of assets in your sheet. It pulls your entire sheet and gives you:
  • the table ID
  • the table name
  • all of the column headers of that table
in an easy copy-paste format. It also outputs it into a CSV format. I find this really helpful, especially if I'm using coding agents like Codex, ChatGPT, Claude Code, or any of the IDEs. I can just run that script, and it prints out the table properties. I can then copy and paste it into my coding agent, and it will use those as sources to help you write the script. I use this all the time. In fact, every single sheet that I have that has a table in it, I add this code block to a file in the script project, and then I just run it. It prints out all of the info about your tables, which comes in super handy because the table ID is not easy to find.

 one other note of importance is that, in order for any of this to work, you need to add the Google Sheets API under your service section. It's really easy:
  1. Click on Service.
  2. Add one.
  3. Find Google Sheets API.
That is the Google Sheets Advanced API v4. It's that API only that gives you the ability to use tables in this way. I have found it to be a very powerful tool, and it's why I spent all this time redeveloping the way I'm doing drop-downs in my Google Sheets infrastructure.

--------------------Script to Output Table Properties---------------------
function listTablesWithCSV() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssId = ss.getId();
// Fetch raw metadata (removing 'fields' to ensure we get table data)
var metadata = Sheets.Spreadsheets.get(ssId);
var humanReadableOutput = [];
var csvOutput = [];

// Add the specific header row you asked for to the CSV list
csvOutput.push("Table ID,Table Name,Column1,Column2,Column3,Column4,Column5,Column6,Column7,Column8,Column9,Column10");

if (metadata.sheets) {
metadata.sheets.forEach(function(s) {
if (s.tables && s.tables.length > 0) {
var sheetName = s.properties.title;
var sheetObj = ss.getSheetByName(sheetName);
s.tables.forEach(function(t) {
var tableName = t.name || "Unnamed Table";
var tableId = t.tableId;
// --- 1. CALCULATE COORDINATES ---
var range = t.range;
var startRow = (range.startRowIndex || 0) + 1;
var startCol = (range.startColumnIndex || 0) + 1;
var numCols = (range.endColumnIndex || 1) - (range.startColumnIndex || 0);

// --- 2. FETCH HEADERS ---
var headers = [];
try {
if (numCols > 0) {
headers = sheetObj.getRange(startRow, startCol, 1, numCols).getValues()[0];
}
} catch (e) {
headers = ["Error reading headers"];
}

// --- 3. FORMAT: HUMAN READABLE ---
humanReadableOutput.push("Table Name: " + tableName);
humanReadableOutput.push("Table ID: " + tableId);
humanReadableOutput.push("Headers: " + headers.join(", "));
humanReadableOutput.push("--------------------------------------------------");

// --- 4. FORMAT: CSV STYLE ---
// Create an array starting with ID and Name, then add all headers
// We wrap items in quotes if they contain commas to keep the CSV clean
var csvRow = [tableId, tableName].concat(headers).map(function(item) {
return (item.toString().indexOf(',') > -1) ? '"' + item + '"' : item;
});
csvOutput.push(csvRow.join(","));
});
}
});
}

// --- LOG THE OUTPUTS ---
console.log("=== HUMAN READABLE LIST ===");
if (humanReadableOutput.length > 0) {
console.log(humanReadableOutput.join("\n"));
} else {
console.log("No tables found.");
}

console.log("\n\n=== CSV FORMAT (COPY THIS) ===");
if (csvOutput.length > 1) { // >1 because we added the header row manually
console.log(csvOutput.join("\n"));
} else {
console.log("No tables found.");
}
}
----------------------------------End of Script for Table Properties----------------------------------------------- 

Michael Bingham-Hawk
HIGH ALTITUDE SPECIAL EVENTS MANAGEMENT

Event Management | Race Timing | Equipment Rental
Media & Marketing | Athlete Photography | Large Event Internet Services
mic...@highaltitudeevents.com
720-608-4725 office
303-656-0732 mobile

www.HighAltitudeEvents.com



--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.

Mario Rossi

unread,
Mar 11, 2026, 3:36:22 PM (7 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Ciao Michael — grazie mille per il contributo molto dettagliato e per lo script: è esattamente il tipo di soluzione che cercavo.

Mi interesserebbero entrambe le cose che proponi:
  • lo script che popola i drop-down dalle colonne di una table (esempi di utilizzo e gestione degli ID table/colonne);
  • lo script che stampa le proprietà delle tabelle in formato umano e CSV.
Puoi incollare qui i sample di script (o condividere un foglio di esempio con accesso commento/modifica)? Se preferisci, basta anche un file .gs con due funzioni: una per elencare le tabelle e una per creare/aggiornare i drop-down. Confermo inoltre che abiliterò l’Advanced Sheets API come indicato.

Grazie ancora — aspetto i sample per provarli e darti un feedback rapido.

Mario

Gary Odums

unread,
Mar 11, 2026, 5:39:54 PM (6 days ago) Mar 11
to Google Apps Script Community
Hi Tom,

Thanks for sharing more details about your project goal. If your city directory/fire insurance map documents can be digitized or already are then you may be able to use a tool like the Notebooklm app provided by Google to automatically extract data out of your various document data sources. Google Forms may also help ease the data entry process if you must do manual entries.

Mario Rossi

unread,
Mar 11, 2026, 5:40:34 PM (6 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Gary,

grazie per il suggerimento su NotebookLM e Google Forms — utili spunti.

Ti scrivo perché, come indicato nel mio messaggio citato qui sotto, mi interesserebbero i sample di script per:
  • elencare le tabelle e le loro proprietà (formato umano/CSV),
  • popolare/aggiornare i drop‑down da colonne di una table (con esempi di gestione ID table/colonne).
Potresti incollare qui i due file .gs o condividere un foglio di esempio con accesso commento/modifica? In alternativa va bene anche un repository o un link Drive. Confermo che abiliterò l’Advanced Sheets API come consigliato.

Appena ricevo i sample li provo e ti do un feedback rapido. Grazie ancora per l’aiuto.

Cordiali saluti,
Mario Rossi

Tom Mort

unread,
Mar 11, 2026, 9:13:54 PM (6 days ago) Mar 11
to google-apps-sc...@googlegroups.com
I will look into the notebooklm app. I came across one of the old fire insurance maps online that had been digitized and was available in pdf and text form. I did look a little at the pdf version in a browser. It did have some graphics for some advertising within it. I think that the pdf has a text layer as well based on other old documents in digital form from that source. I haven't had a chance to look and see if there are more in digital form for the area I'm interested in.

I know locally the old ones are on microfilm and the readers do allow you to turn them into images, but they wouldn't have been run through OCR and cleaned up. I would have to do this page by page I'm pretty sure, but I woudn't need all that many pages to get the addresses I need for this project.

The City Directories are Polk Directories. I know as of about 20 or thirty years ago you could get a digital subscription and you could download the pages of interest in digital form. I don't if they have gone backwards and the old ones are available in that form though. The newer info starts out in digital form so that would be simple.

However simpler it is you do need a subscription which I don't have and it doesn't appear the library has, plus I'd be mainly interested in the older data. To complicate matters they have changed their name to something like Data Axle or something like that. I do have access to the book forms going back many years. I'm just beginning this project and have been just taking pictures of the relevant pages and then entering the info at home when I have some time. Right now more to get a feel for the order the info is in, etc to get use the spreadhseets as a data entry form and get it all sorted in the same order as the books are in or at least the books from the last twenty years or so. Older than that they are in two volumes rather than four and divided up differently. So the entry form may have to be adjusted for those and the still older ones are on microfilm.

I've just taken the pictures with a smartphone. Sometimes though I miss a bit of a page or the fold by the binding makes it illegible. I suppose those are in digital format and can be scanned by OCR. The bent areas from the folds may cause issues. It's been years since I've done any attempts at OCR. I suppose with all the money and effort going to get everything out there to train LLMs there are many books being scanned so maybe this app has those capabilities built in. I suppose maybe in the fine print anything processed with it is used for AI training. I suppose all of this is pretty much public info that it is fair game or use.

Those things do sound like they could be helpful to I'll be sure to look into that app and try running some images for docs though it representing a range of forms I may get the data in.

--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.

Mario Rossi

unread,
Mar 11, 2026, 9:14:55 PM (6 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Tom — thanks for the detailed overview.

That all sounds sensible. A few practical suggestions that might help:
  • If possible scan pages flat (library scanner or a flatbed) to avoid fold distortion; smartphone photos can work if taken carefully and cropped/deskewed.
  • For OCR: try Google Drive OCR or Adobe Scan for quick results; ABBYY/FineReader is better for batch cleanup. Post-process with a simple script or Google Sheets to normalize addresses.
  • For Polk/Data Axle content, check library subscriptions and terms of use — if you can’t access a digital subscription, photographing book pages and OCRing them is a reasonable fallback.
  • NotebookLM and similar LLM-based tools can help extract structured data, but check privacy/training usage in the terms before uploading proprietary scans.
  • For manual entry, a Google Form tied to a normalized Sheet often speeds data capture and keeps formats consistent; you can adapt the form for two-volume vs four-volume layouts.
  • If you want, share a few sample images or a spreadsheet template and I can suggest an OCR + extraction workflow or a small Apps Script to normalize the data and populate drop-downs.
Let me know which option you prefer and I’ll help set it up.

Joseph Vaughan

unread,
Mar 11, 2026, 9:45:10 PM (6 days ago) Mar 11
to google-apps-sc...@googlegroups.com
@Michael Bingham-Hawk,

Thanks.  That's a clever piece of scripting. 

Can you expand a little on exactly how you reference a table using the sheet ID, the table ID and the column header?   Does that information go into a formula? 

Thanks,

Joseph
---------------
Joseph Vaughan
CIO/VP for Computing and Information Services
Harvey Mudd College


Mario Rossi

unread,
Mar 11, 2026, 9:45:41 PM (6 days ago) Mar 11
to google-apps-sc...@googlegroups.com
Hi Joseph — thanks, glad it was useful.

You do NOT put sheetId/tableId/column header into a spreadsheet formula. Those identifiers are used from Apps Script (via the Advanced Sheets API) to locate the table metadata; then your script reads the table’s header and the column values and applies them to the cell validation (or returns them for other use).

Minimal workflow and sample:
  1. Enable Advanced Google Sheets API in the script project (Services → Add → Google Sheets API).
  2. Use Sheets.Spreadsheets.get(ssId) to retrieve sheet metadata, find the sheet and the table with the tableId, read t.range to compute startRow/startCol/numCols.
  3. Read the header row from the sheet (sheet.getRange(startRow, startCol, 1, numCols).getValues()). Find the header index for the desired column header.
  4. Read that column’s values (sheet.getRange(startRow+1, startCol+headerIndex, numRows, 1).getValues()) and apply them as a dropdown via SpreadsheetApp.newDataValidation().requireValueInList(values, true).build() on the target range.
Example (condensed):

function applyTableColumnAsDropdown(tableId, headerName, targetRangeA1) {
const ss = SpreadsheetApp.getActive();
const ssId = ss.getId();
const meta = Sheets.Spreadsheets.get(ssId); // Advanced Sheets API
for (const s of meta.sheets || []) {
if (!s.tables) continue;
for (const t of s.tables) {
if (t.tableId == tableId) {
const sheet = ss.getSheetByName(s.properties.title);
const r = t.range;
const startRow = (r.startRowIndex || 0) + 1;
const startCol = (r.startColumnIndex || 0) + 1;
const numCols = (r.endColumnIndex || startCol) - (r.startColumnIndex || 0);
const headers = sheet.getRange(startRow, startCol, 1, numCols).getValues();
const idx = headers.indexOf(headerName);
if (idx === -1) throw new Error('Header not found');
const data = sheet.getRange(startRow+1, startCol+idx, sheet.getLastRow()-startRow, 1)
.getValues().flat().filter(v => v !== '');
const rule = SpreadsheetApp.newDataValidation().requireValueInList(data, true).build();
ss.getRange(targetRangeA1).setDataValidation(rule);
return;
}
}
}
throw new Error('Table ID not found');
}

If you want, I can: a) provide a ready-to-run sample sheet + script, or b) show how to return a human-readable list of tableId/tableName/headers (the script Michael posted already does this). Which do you prefer?

Laurie Nason

unread,
Mar 12, 2026, 2:56:27 AM (6 days ago) Mar 12
to google-apps-sc...@googlegroups.com
Hi Tom,
One trick I have done over the years is to do the following:
  1. Have your dropdown based on a range (sounds like you already have this)
  2. Allow the dropdown to show a warning if data is invalid (advanced Options)
  3. Display style to Arrow (advanced Options) - the default chip dropdown can be deleted v. easily by users
  4. The range you are using to build the dropdown - use the formula SORT(UNIQUE(range of dropdowns cells e.g. A2:A)
  5. This way you can add new values into the dropdown by typing in the cells - this then gets added to the list that the dropdown is based on automatically for future use. 
  6. If you need some static values in your dropdown at the top of the list, then put the formula in 4 above below the list you want to use as the values of the dropdown.
  7. It's open to users entering incorrect or different capitalisation into the list so potentially the formula in 4 could include PROPER() function too depending on your data. That would force future values to be consistent.
Your project sounds cool - If I was doing it - I would look at appsheet to do the data collection - it can handle a lot more data entries and provides various functionality like geo-location at time of data capture along with storing photos, urls, etc.

Hope this makes sense! Let me know if you need any extra info or an example
Laurie 

You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.

Mario Rossi

unread,
Mar 12, 2026, 2:56:44 AM (6 days ago) Mar 12
to google-apps-sc...@googlegroups.com
Hi Laurie,

Thanks — this is very helpful. I’ll try using SORT(UNIQUE(range)) (and wrap PROPER() where needed), set the validation to “show warning” and change the display to Arrow so users can’t easily delete the dropdown. Your tip about putting static values above the formula is also useful.

AppSheet sounds like a good fit for richer data capture (photos, geolocation); I’ll evaluate it for the next phase.

If you have a short example sheet or formula snippet you’ve used for the “static values above formula” pattern, I’d appreciate seeing it.

Thanks again,
Tom

Laurie Nason

unread,
Mar 12, 2026, 3:05:50 AM (6 days ago) Mar 12
to google-apps-sc...@googlegroups.com
You are welcome. 

Here's the sheet - this link will make a copy you can take a look at
https://docs.google.com/spreadsheets/d/1NodXA6izqBVYQJfBWtioxhTrbfnwvE0D7PzJAee69ko/copy

Any questions please let me know.
Laurie

--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.

Mario Rossi

unread,
Mar 12, 2026, 3:06:10 AM (6 days ago) Mar 12
to google-apps-sc...@googlegroups.com
Hi Laurie,

Thanks — I received the copy link and will review the sheet. I appreciate you sharing it.

If there’s any specific tab, range, or formula in the example you’d recommend I look at first, please let me know. I’ll get back to you with any questions.

Best regards,
Tom

Tom Mort

unread,
Mar 12, 2026, 8:52:45 AM (6 days ago) Mar 12
to google-apps-sc...@googlegroups.com

Yesterday was a hectic day and I didn’t get a chance to look into things until late. I wanted to go further but figured it would be best to wait for the morning when I was more rested. So here is a listing of what I did and the results:



I tried putting the validation rule back in for F3 did not enter or get put into the list either.


Here are a few screenshots of how things are setup


The two screenshots below show the validations and other settings for the drop down list


image.png


image.png


Below is the section of code I changed. I first changed both 1s to 6, but realized one was for the target and the other was fro the list.


image.png


It’s late and I’m tired at this point and I will have to look through a lot of messages about debugging and the right thing to test or log to chack, but it will be later.


I will also try the other suggestions about adding a button and adding a script to the button and also the other suggestion about using tables. I keep seeing these popups showing them.



I thought I’d try the other method that was suggested of assigning a script to an image. I was able to successfully open the text box with the script in the example.


I then replaced the example function with the full script provided, I changed the OutputToSheet  to outputToListOptions on lines 24 & 25.


When I clicked on the button I got this this message:


image.png


I did a search of the script and it activateMyButton is in the script but down near the bottom.


I also saw there were two more instances of OutputToSheet on lines 17 & 18. This section seems more about responding to the user input though and I haven’t got thar far so haven’t changed that.


Here is the script with my configurations as I entered into the App Script editor:


function confirmUserDecisionWithAlert(ui,selectedValue) {

 return ui.alert(

   "Please confirm",

   `Are you sure you want to continue with adding "${selectedValue}"?`,

   ui.ButtonSet.YES_NO,

 );

}



// User clicked "Yes".

function userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName) {

 ui.alert(`Operation successful. "${selectedValue}" has been added to ${outputToSheetName}.`);

}



// User encoutered an error while attempting to add the value

function userDecisionUnsuccessfulAlert(ui,outputToSheetName,e) {

 ui.alert(`Error encountered while attempting to add a new cell to ${outputToSheetName}`);

 // Insert the line Logger.log(e) here for more details about the error.

}



// User clicked "No" or X in the title bar.

function userCanceledAlert(ui,selectedValue,outputToListOptions) {

 ui.alert(`Operation cancelled. "${selectedValue}" was not added to ${outputToListOptions}.`);

}



function addValueToRange(value,sheet,column,outputToTopOfColumn,topOfColumnOffset) {

 if (outputToTopOfColumn === true) {

   const insertRowAtIndex = 1 + topOfColumnOffset;

   sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);

   sheet.getRange(insertRowAtIndex,column).setValue(value);

 } else {

   const lastCellInRangeIndex = sheet.getRange(1,column,Math.max(1,sheet.getLastRow()),1).getLastRow();

   const lastCellInRangeValue = sheet.getRange(lastCellInRangeIndex,column).getValue();

   const insertRowAtIndex = `${lastCellInRangeValue}`.length === 0 ? 1 : lastCellInRangeIndex + 1;

   sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);

   sheet.getRange(insertRowAtIndex,column).setValue(value);

 }

}



// CONFIGURE THE VALUES DEFINED NEAR THE TOP OF THIS FUNCTION TO MATCH YOUR OWN NEEDS (Lines 21 to 27)

function activateMyButton() {

 const dropdownSheetName = "Sheet1";

 const dropdownCellAddress = [1,2] // [Row,Column] // example [1, 2] for cell B1



 const outputToSheetName = "Sheet2";

 const outputToColumn = 1;

 const outputToTopOfColumn = true; // Set to true to write new values to the top of the row or to false to add to the bottom

 const outputToRowOffset = 0; // Set an offset here to allow for header rows; only applies is outputToTopOfColumn is true



 const outputToSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(outputToSheetName);

 const dropdownSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dropdownSheetName);

 const selectedValue = dropdownSheet.getRange(...dropdownCellAddress).getValue();



 const ui = SpreadsheetApp.getUi();



 if (confirmUserDecisionWithAlert(ui,selectedValue) === ui.Button.YES) {

   try {

     addValueToRange(selectedValue,outputToSheet,outputToColumn,outputToTopOfColumn,outputToRowOffset);

     userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName);

   } catch (e) {

     userDecisionUnsuccessfulAlert(ui,outputToSheetName,e);

   }

 } else {

   userCanceledAlert(ui,selectedValue,outputToSheetName);

 }

}


I also checked to make sure I had copied the full script suggested by Gary and it was what was sent.


I thought I’d go back and try the first improved suggested script again. I did it last night but it was late and thought I’d try again.  I did it with the existing sheet and a brand new one. Didn’t get very far at all.


I would get an error when I tried to save the script. At first I used the sheet I had already used after clearing out the script for using the image button. I then set up a brand new set of sheets. One was empty and the other contained the list. I tried it as the script was originally written and I made the changes for column 6/F. I got the same error each time:


Syntax error: SyntaxError: Missing initializer in const declaration line: 27 file: Code.gs

 

My day is starting to begin and I’ll be busy with other things for a while. I’m just curling and getting frustrated. Doing something else for a while will be good. I figure I’ll post this in the meantime.


I want to also look into using tables or forms as suggested. I wonder if either might have the ability to do this sort of thing without using a script?



On Wed, Mar 11, 2026 at 12:30 AM Gary Odums <garyo...@gmail.com> wrote:
Hi Tom,

Below is a configurable script which can achieve your specified goal by using an insertable image as a button.

Simply attach the function named activateMyButton  to the button by following these instructions: https://developers.google.com/apps-script/guides/menus#clickable_images_and_drawings_in

function confirmUserDecisionWithAlert(ui,selectedValue) {
  return ui.alert(
    "Please confirm",
    `Are you sure you want to continue with adding "${selectedValue}"?`,
    ui.ButtonSet.YES_NO,
  );
}

// User clicked "Yes".
function userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName) {
  ui.alert(`Operation successful. "${selectedValue}" has been added to ${outputToSheetName}.`);
}

// User encoutered an error while attempting to add the value
function userDecisionUnsuccessfulAlert(ui,outputToSheetName,e) {
  ui.alert(`Error encountered while attempting to add a new cell to ${outputToSheetName}`);
  // Insert the line Logger.log(e) here for more details about the error.
}

// User clicked "No" or X in the title bar.
function userCanceledAlert(ui,selectedValue,outputToSheetName) {
  ui.alert(`Operation cancelled. "${selectedValue}" was not added to ${outputToSheetName}.`);
}

function addValueToRange(value,sheet,column,outputToTopOfColumn,topOfColumnOffset) {
  if (outputToTopOfColumn === true) {
    const insertRowAtIndex = 1 + topOfColumnOffset;
    sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);
    sheet.getRange(insertRowAtIndex,column).setValue(value);
  } else {
    const lastCellInRangeIndex = sheet.getRange(1,column,Math.max(1,sheet.getLastRow()),1).getLastRow();
    const lastCellInRangeValue = sheet.getRange(lastCellInRangeIndex,column).getValue();
    const insertRowAtIndex = `${lastCellInRangeValue}`.length === 0 ? 1 : lastCellInRangeIndex + 1;
    sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);
    sheet.getRange(insertRowAtIndex,column).setValue(value);
  }
}

// CONFIGURE THE VALUES DEFINED NEAR THE TOP OF THIS FUNCTION TO MATCH YOUR OWN NEEDS (Lines 21 to 27)
function activateMyButton() {
  const dropdownSheetName = "Sheet1";
  const dropdownCellAddress = [1,2] // [Row,Column] // example [1, 2] for cell B1

  const outputToSheetName = "Sheet2";
  const outputToColumn = 1;
  const outputToTopOfColumn = true; // Set to true to write new values to the top of the row or to false to add to the bottom
  const outputToRowOffset = 0; // Set an offset here to allow for header rows; only applies is outputToTopOfColumn is true

  const outputToSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(outputToSheetName);
  const dropdownSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dropdownSheetName);
  const selectedValue = dropdownSheet.getRange(...dropdownCellAddress).getValue();

  const ui = SpreadsheetApp.getUi();

  if (confirmUserDecisionWithAlert(ui,selectedValue) === ui.Button.YES) {
    try {
      addValueToRange(selectedValue,outputToSheet,outputToColumn,outputToTopOfColumn,outputToRowOffset);
      userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName);
    } catch (e) {
      userDecisionUnsuccessfulAlert(ui,outputToSheetName,e);
    }
  } else {
    userCanceledAlert(ui,selectedValue,outputToSheetName);
  }
 
}


Reach out with any questions you may have about this approach.

Thanks


On Tuesday, March 10, 2026 at 10:02:25 PM UTC-4 Mario Rossi wrote:
Hi Tom — thanks for the detailed debugging notes. Quick points and a ready-to-paste script tailored to your setup.

Primary issues you reported
  • Remove the stray text "javascript" at top of the file — that produced ReferenceError when you ran the script.
  • Do not press Run in the editor for an onEdit simple trigger: running it directly has no event object e and will error. Test by editing a single cell in the sheet instead, and watch Executions / Logger or toast messages.
  • Selecting the whole column (ListOptions!A:A) is fine, but getValues() will include blank cells — handle lastRow when building the list.
  • Validation must be “Show warning” and edits must be a single cell (typing in cell with dropdown). Multi-cell edits or pastes don’t provide e.value.
Adapted onEdit script (paste into the bound project and save)

function onEdit(e) {
if (!e) return;
const ss = SpreadsheetApp.getActiveSpreadsheet();
const mainSheetName = '2025';
const listSheetName = 'ListOptions';
const targetColumn = 1; // column A
const listColumn = 1; // column A in ListOptions

const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() !== mainSheetName) return;
if (range.getNumColumns() !== 1 || range.getNumRows() !== 1) return;
if (range.getColumn() !== targetColumn) return;

const newValue = e.value;
if (!newValue) return;

const listSheet = ss.getSheetByName(listSheetName);
if (!listSheet) return;

let lastRow = listSheet.getLastRow();

if (lastRow < 1) lastRow = 0;

// read only used rows to avoid huge arrays when using A:A
const valuesRange = listSheet.getRange(1, listColumn, Math.max(1, lastRow), 1);
const listValues = valuesRange.getValues().flat().map(v => (v || '').toString().trim()).filter(v => v !== '');
if (listValues.includes(newValue.toString().trim())) return;

listSheet.getRange(lastRow + 1, listColumn).setValue(newValue);
// optional: sort

// listSheet.getRange(1, listColumn, lastRow + 1, 1).sort({column: listColumn, ascending: true});
}

Quick tests to run
  • Save the script (Ctrl/Cmd+S) and then edit one cell in sheet 2025 (type a new value in the validated cell). Do not run the function from the editor.
  • If nothing happens, add SpreadsheetApp.getActive().toast('onEdit fired: ' + e.value) at the top (for quick visibility) and check Executions for errors.
  • If your ListOptions sheet has a header row, tell me which row is header and I’ll adapt getRange() to start from row 2.
If you want, I can:
  • Modify the script for a header row,
  • Provide an installable trigger variant (so it runs with broader permissions),
  • Or paste the exact script directly into your project if you grant access (or share a copy of the sheet).
Tell me whether ListOptions has a header row and if you want the sort enabled.

--
You received this message because you are subscribed to a topic in the Google Groups "Google Apps Script Community" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/google-apps-script-community/QObsC1Za5cI/unsubscribe.
To unsubscribe from this group and all its topics, send an email to google-apps-script-c...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/google-apps-script-community/17c1382f-9cfa-4d8c-b85b-f05cef7a7201n%40googlegroups.com.

Mario Rossi

unread,
Mar 12, 2026, 8:53:26 AM (6 days ago) Mar 12
to google-apps-sc...@googlegroups.com
Ciao Tom — grazie per il resoconto dettagliato. Di seguito trovi suggerimenti pratici e una versione pulita dello script pronta da incollare e testare. Mantieni la calma: gli errori che vedi sono quasi sempre piccoli refusi o nomi duplicati.

Controlli rapidi (prima di tutto)
  • Rimuovi ogni testo estraneo in cima al file (es. la parola "javascript").
  • Evita di eseguire onEdit(e) dal editor: onEdit semplice richiede l'evento e; testa modificando la cella nella sheet.
  • Verifica che non ci siano dichiarazioni const duplicate con lo stesso nome in altri punti del file.
  • Apri View > Executions e Logger per vedere errori dettagliati. Gli errori "Missing initializer in const declaration" sono spesso dovuti a una riga troncata, a una dichiarazione duplicata o a una parentesi/acapo mancante.
Suggerimenti relativi al tuo uso:
  • Per il bottone assegnato all'immagine, assegna la funzione activateMyButton esattamente con quel nome.
  • Se usi cella F3 come dropdown, configura dropdownCellAddress = [3, 6] (riga 3 colonna 6).
  • Quando salvi e provi, esegui activateMyButton manualmente (Run) per vedere errori; la funzione usa SpreadsheetApp e può essere eseguita dal progetto per debug (ti chiederà autorizzazioni).
Script pulito (incolla nella IDE e salva). Adatta solo i tre valori in cima (sheet e cella):

function confirmUserDecisionWithAlert(ui, selectedValue) {

return ui.alert(
"Please confirm",
Are you sure you want to continue with adding "${selectedValue}"?,
ui.ButtonSet.YES_NO
);
}

function userDecisionSuccessfulAlert(ui, selectedValue, outputToSheetName) {
ui.alert(Operation successful. "${selectedValue}" has been added to ${outputToSheetName}.);
}

function userDecisionUnsuccessfulAlert(ui, outputToSheetName, e) {
ui.alert(Error encountered while attempting to add a new cell to ${outputToSheetName});
Logger.log(e);
}

function userCanceledAlert(ui, selectedValue, outputToSheetName) {
ui.alert(Operation cancelled. "${selectedValue}" was not added to ${outputToSheetName}.);
}

function addValueToRange(value, sheet, column, outputToTopOfColumn, topOfColumnOffset) {

if (outputToTopOfColumn === true) {
const insertRowAtIndex = 1 + topOfColumnOffset;
sheet.getRange(insertRowAtIndex, column).insertCells(SpreadsheetApp.Dimension.ROWS);
sheet.getRange(insertRowAtIndex, column).setValue(value);
} else {
const lastCellInRangeIndex = sheet.getRange(1, column, Math.max(1, sheet.getLastRow()), 1).getLastRow();
const lastCellInRangeValue = sheet.getRange(lastCellInRangeIndex, column).getValue();
const insertRowAtIndex = ${lastCellInRangeValue}.length === 0 ? 1 : lastCellInRangeIndex + 1;
sheet.getRange(insertRowAtIndex, column).insertCells(SpreadsheetApp.Dimension.ROWS);
sheet.getRange(insertRowAtIndex, column).setValue(value);
}
}

// CONFIG: change only these values to match your spreadsheet
function activateMyButton() {
const dropdownSheetName = "2025"; // sheet that has the dropdown
const dropdownCellAddress = [3, 6]; // [row, column] — e.g. F3 is [3,6]

const outputToSheetName = "ListOptions"; // sheet to append the value
const outputToColumn = 1; // column A = 1
const outputToTopOfColumn = false; // true = insert at top, false = append at bottom
const outputToRowOffset = 0;

const ss = SpreadsheetApp.getActiveSpreadsheet();
const outputToSheet = ss.getSheetByName(outputToSheetName);
const dropdownSheet = ss.getSheetByName(dropdownSheetName);
if (!outputToSheet || !dropdownSheet) {
SpreadsheetApp.getUi().alert("Sheet not found: check names.");
return;
}

const selectedValue = dropdownSheet.getRange(...dropdownCellAddress).getValue();
const ui = SpreadsheetApp.getUi();

if (confirmUserDecisionWithAlert(ui, selectedValue) === ui.Button.YES) {
try {
addValueToRange(selectedValue, outputToSheet, outputToColumn, outputToTopOfColumn, outputToRowOffset);
userDecisionSuccessfulAlert(ui, selectedValue, outputToSheetName);
} catch (e) {
userDecisionUnsuccessfulAlert(ui, outputToSheetName, e);
}
} else {
userCanceledAlert(ui, selectedValue, outputToSheetName);
}
}

Se continui a ricevere "Missing initializer in const declaration" salva il file e guarda la riga indicata: copia qui (o incolla le righe 20–40 del tuo file) così controllo il contesto perché spesso il problema è in una riga subito sopra o una parentesi non chiusa.

Vuoi che ti aiuti a convertire questo in un onEdit installabile o a correggere la versione onEdit che usavi? Dimmi quale comportamento preferisci (bottone vs onEdit) e se ListOptions ha intestazione (header).

Tom Mort

unread,
Mar 12, 2026, 10:35:08 PM (5 days ago) Mar 12
to google-apps-sc...@googlegroups.com
Hi Gary, I'm trying to use this button script now I wasn't having much luck with the other one. I can get it to do some things, but it isn't really working the way I would want it to, although maybe I just don't have it configured right.

I see where to set the start of the list but not how to specify the whole range of the entries in the source list or the entire column so new values can be added to the selection list.

What i had in mind was a drop down or button on each line to select the value to add to each row from the values in the list and also be able to add new values to the list if needed. 

As it is now if I press the button with the cell empty it asks if I want to enter the first value on the list, but the whole list is not displayed. I also notice that if I do this again it places the new entry down one line on the main sheet and if I do it again below the bottom one. Likewise if a type in a word in the cell it will put that word in the list but not in the cell or the top of bottom but somewhere in the middle of the list I am generating.

I suppose if the button and entry cell was but in a header I might be able to use it to fill in all the rows from top to bottom, but there are to be addresses and some addresses won't necessarily have any entry as some structures get reconfigured from time to time from having just one address to several.and back again.

But these added values are being added to the main spreadsheet not the range where the list of choices is. So I probably have things switched around somehow.

Where the values come from and are placed is also in the script. If I was to put a button or dropdown in each row the script each puts the value into would need to be different. A formula in spreadsheet cell changes when it is copied to different cells. It doesn't seem like this would work with scrips. Maybe there are ways to deal with that or something.

I think it was you that suggested NotebookLM. I did play with it a little with some pictures I took of some pages a few days ago and uploaded them into it and selected table. What I got looked good. There was word spelled incorrectly, but that may have been that way in the picture as well. I still have to compare the original pictures to what it produced.  So if that works out I  should be able to avoid a lot of the manual entry.

I took a little over twenty pictures. A few were probably the same page photographed more than once. I tried adding them all at once and the process seemed to stall or maybe it sensed the total size of the files and the bandwith it uses and things got throttled down or something. Maybe it was just an  temporary thing.

I have done some coding in the past, not javascript or a google product. So I'm starting to get a little feel for this. but think it is time for a break right now. I have some other things to do and I want to go back and try the original drop down list script again too.














On Wed, Mar 11, 2026 at 12:30 AM Gary Odums <garyo...@gmail.com> wrote:
Hi Tom,

Below is a configurable script which can achieve your specified goal by using an insertable image as a button.

Simply attach the function named activateMyButton  to the button by following these instructions: https://developers.google.com/apps-script/guides/menus#clickable_images_and_drawings_in

function confirmUserDecisionWithAlert(ui,selectedValue) {
  return ui.alert(
    "Please confirm",
    `Are you sure you want to continue with adding "${selectedValue}"?`,
    ui.ButtonSet.YES_NO,
  );
}

// User clicked "Yes".
function userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName) {
  ui.alert(`Operation successful. "${selectedValue}" has been added to ${outputToSheetName}.`);
}

// User encoutered an error while attempting to add the value
function userDecisionUnsuccessfulAlert(ui,outputToSheetName,e) {
  ui.alert(`Error encountered while attempting to add a new cell to ${outputToSheetName}`);
  // Insert the line Logger.log(e) here for more details about the error.
}

// User clicked "No" or X in the title bar.
function userCanceledAlert(ui,selectedValue,outputToSheetName) {
  ui.alert(`Operation cancelled. "${selectedValue}" was not added to ${outputToSheetName}.`);
}

function addValueToRange(value,sheet,column,outputToTopOfColumn,topOfColumnOffset) {
  if (outputToTopOfColumn === true) {
    const insertRowAtIndex = 1 + topOfColumnOffset;
    sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);
    sheet.getRange(insertRowAtIndex,column).setValue(value);
  } else {
    const lastCellInRangeIndex = sheet.getRange(1,column,Math.max(1,sheet.getLastRow()),1).getLastRow();
    const lastCellInRangeValue = sheet.getRange(lastCellInRangeIndex,column).getValue();
    const insertRowAtIndex = `${lastCellInRangeValue}`.length === 0 ? 1 : lastCellInRangeIndex + 1;
    sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);
    sheet.getRange(insertRowAtIndex,column).setValue(value);
  }
}

// CONFIGURE THE VALUES DEFINED NEAR THE TOP OF THIS FUNCTION TO MATCH YOUR OWN NEEDS (Lines 21 to 27)
function activateMyButton() {
  const dropdownSheetName = "Sheet1";
  const dropdownCellAddress = [1,2] // [Row,Column] // example [1, 2] for cell B1

  const outputToSheetName = "Sheet2";
  const outputToColumn = 1;
  const outputToTopOfColumn = true; // Set to true to write new values to the top of the row or to false to add to the bottom
  const outputToRowOffset = 0; // Set an offset here to allow for header rows; only applies is outputToTopOfColumn is true

  const outputToSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(outputToSheetName);
  const dropdownSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dropdownSheetName);
  const selectedValue = dropdownSheet.getRange(...dropdownCellAddress).getValue();

  const ui = SpreadsheetApp.getUi();

  if (confirmUserDecisionWithAlert(ui,selectedValue) === ui.Button.YES) {
    try {
      addValueToRange(selectedValue,outputToSheet,outputToColumn,outputToTopOfColumn,outputToRowOffset);
      userDecisionSuccessfulAlert(ui,selectedValue,outputToSheetName);
    } catch (e) {
      userDecisionUnsuccessfulAlert(ui,outputToSheetName,e);
    }
  } else {
    userCanceledAlert(ui,selectedValue,outputToSheetName);
  }
 
}


Reach out with any questions you may have about this approach.

Thanks


On Tuesday, March 10, 2026 at 10:02:25 PM UTC-4 Mario Rossi wrote:
Hi Tom — thanks for the detailed debugging notes. Quick points and a ready-to-paste script tailored to your setup.

Primary issues you reported
  • Remove the stray text "javascript" at top of the file — that produced ReferenceError when you ran the script.
  • Do not press Run in the editor for an onEdit simple trigger: running it directly has no event object e and will error. Test by editing a single cell in the sheet instead, and watch Executions / Logger or toast messages.
  • Selecting the whole column (ListOptions!A:A) is fine, but getValues() will include blank cells — handle lastRow when building the list.
  • Validation must be “Show warning” and edits must be a single cell (typing in cell with dropdown). Multi-cell edits or pastes don’t provide e.value.
Adapted onEdit script (paste into the bound project and save)

function onEdit(e) {
if (!e) return;
const ss = SpreadsheetApp.getActiveSpreadsheet();

Mario Rossi

unread,
Mar 12, 2026, 10:35:37 PM (5 days ago) Mar 12
to google-apps-sc...@googlegroups.com
Hi Tom — thanks for the clear description. A few concise points and practical steps to get this working the way you want.
  1. Why the whole list doesn’t show
  • A script that inserts a single value into the list sheet won’t automatically open a UI dropdown showing all choices. The spreadsheet’s dropdown (data validation) is what displays the list; the script only modifies the list source. Make sure your dropdown’s validation points to the full list range (or a named/dynamic range), not a single cell.
  1. Use a dynamic range for the source list
  • On the sheet that holds choices (e.g. "ListOptions") use a named range or a range formula that only includes non-empty cells. Example approaches:
    • Create a named range manually and update its range when you add items.
    • Use a helper column with FILTER or use Apps Script to compute the last row and then setDataValidation to that exact range.
      This lets new entries appear in every dropdown automatically.
  1. Adding new items to the list (script behavior)
  • If new values are being added to the wrong sheet/column, adjust the script so it writes to the ListOptions sheet and to the intended column. In short: getSheetByName('ListOptions') and setValue(row, col) there.
  • Your onEdit script example (shared earlier in thread) is the right idea: it checks the edited cell, compares with existing list items and appends to the list sheet if missing. Use getLastRow() on ListOptions to append.
  1. Dropdown or button per row
  • Preferred: use data validation on the entire target column (so every row has the dropdown). That is simpler and more native than a button per row.
  • If you need a per-row button, use an image/drawing assigned to a function. The script can detect the active cell (SpreadsheetApp.getActiveRange()) or prompt for the target row. You can also have one button in a header that applies the currently selected value to the active row.
  • Scripts do not auto-change when copied like formulas — but they can read the active cell or take a row argument so a single script works for any row.
  1. Handling empty cells, insertion position, and ordering
  • Decide whether you want new list items at top or bottom. The provided addValueToRange function in Gary’s message already supports adding to top or bottom; adapt the target sheet/column and header offset if you have header rows.
  1. Practical next steps for you
    • If you want, I can:
      • Adapt the onEdit script to your exact sheet names and whether ListOptions has a header row.
      • Provide a small script that updates the data validation range dynamically after adding an item.
      • Provide a version that uses an installable trigger if you need broader permissions or dialogs.
    Tell me:
    • Names of the two sheets (main sheet and list sheet)
    • Which column holds the dropdown on the main sheet (A, B, etc.)
    • Whether the list sheet has a header row
      I’ll paste a ready-to-use script tuned to those details.

    Tom Mort

    unread,
    Mar 14, 2026, 10:15:06 PM (3 days ago) Mar 14
    to google-apps-sc...@googlegroups.com

    I’ve been pretty busy with other things and there has been some weather. It’s taken me some time to find the time to look at this and into it. I really appreciate and want to thank you for all of the assistance and your gracious offers.


    I’m both using this to reply and also gather together the various replies and scripts and suggestions I’ve received. I seem to have gotten on an email list but not actually on the board. I get a lot of messages, but many of them aren’t for me. Finding the ones I need takes some digging.


    I’m switching back to dropdown lists and the script for them. I have inserted them at the top of a column so they and the validation rules, ets are for all of the columns. I have set the list range for the list to the entire column, etc. I have made adjustments to the script for some of this. I find that I can’t save it though. I get this error: Syntax error: SyntaxError: Missing initializer in const declaration line: 27 file: Code.gs


    I am also getting an error on this line if I step through with the debugger. I remember you saying not to use the debugger for this script though. I checked this line of the script against the one you posted and they are the same.


    1. Why the whole list doesn’t show

    • A script that inserts a single value into the list sheet won’t automatically open a UI dropdown showing all choices. The spreadsheet’s dropdown (data validation) is what displays the list; the script only modifies the list source. Make sure your dropdown’s validation points to the full list range (or a named/dynamic range), not a single cell.

    OK, I see.  I have changed the range to =ListOptions!$A:$A 

    Use a dynamic range for the source list

    • On the sheet that holds choices (e.g. "ListOptions") use a named range or a range formula that only includes non-empty cells. Example approaches:

      • Create a named range manually and update its range when you add items.


    • Use a helper column with FILTER or 

    Looks like helper is a function I’m not familiar with, but I guess it would be looking for any cells in the whole range that contain “”.  I have used if A1<>””,01 and copied it down a column to distinguish between blank cells and those with values. Will have to think about how to then find the last row number, etc.

    • use Apps Script to compute the last row and then setDataValidation to that exact range.

    I’m not familiar with javascript at all, but thought I’d see if I can find something and found this. I think in my case the rows would be sorted and there wouldn’t be areas at least in some columns where there might be gaps, but his bit of code would get the correct last row number regardless


    const ss = SpreadsheetApp.getActive(); const sh = ss.getSheetByName('Sheet1') const lrow = sh.getLastRow(); const Avals = sh.getRange("A1:A"+lrow).getValues(); const Alast = lrow - Avals.reverse().findIndex(c=>c[0]!='');


    This, if I can figure out how to use it, seems simpler. I supposed I would just need to insert it before the insert of the new item entry occurs.



    This lets new entries appear in every dropdown automatically.


    1. Adding new items to the list (script behavior)

    • If new values are being added to the wrong sheet/column, adjust the script so it writes to the ListOptions sheet and to the intended column. In short: getSheetByName('ListOptions') and setValue(row, col) there.

    I did some of that and maybe will have to do it again. Right now I can’t save the script.

    • Your onEdit script example (shared earlier in thread) is the right idea: it checks the edited cell, compares with existing list items and appends to the list sheet if missing. Use getLastRow() on ListOptions to append.

    Looks like getLastRow would be used in conjunction with with script I mentioned above along with a little more code.

    1. Dropdown or button per row

    • Preferred: use data validation on the entire target column (so every row has the dropdown). That is simpler and more native than a button per row.

    That is how I have it at the moment. I have a two row header on the sheet and it is set to stay visible as you scroll through the sheet. I don’t need any dropdowns there. Maybe there is  away for change how these cells look through formatting. The upper cell as nothing in it and the lower cell does display the header text for that column. So it can function that way, but the drop downs being there could cause some confusion. Maybe I could get an image of the headers and insert them above that row if nothing else.

    • If you need a per-row button, use an image/drawing assigned to a function. The script can detect the active cell (SpreadsheetApp.getActiveRange()) or prompt for the target row. You can also have one button in a header that applies the currently selected value to the active row.

    I think the drop down will work better in this case

    • Scripts do not auto-change when copied like formulas — but they can read the active cell or take a row argument so a single script works for any row.

    OK, that addresses my concern about that and is good to hear.

    1. Handling empty cells, insertion position, and ordering

    • Decide whether you want new list items at top or bottom. The provided addValueToRange function in Gary’s message already supports adding to top or bottom; adapt the target sheet/column and header offset if you have header rows.

    I’d like the list to be in alphabetical order and intend to use the optional sort in the script you provided. It doesn’t seem like in that case where the entries are put initially will matter. Please let me know if this is not the case.

    1. Practical next steps for you

    • If you want, I can:

      • Adapt the onEdit script to your exact sheet names and whether ListOptions has a header row.

    I think this might address the questions I had about header rows.

    • Provide a small script that updates the data validation range dynamically after adding an item.

    This sounds like you are addressing where I mention the script for finding the last row, etc.

    • Provide a version that uses an installable trigger if you need broader permissions or dialogs.


    I’m not sure if this would be needed or not, but sounds like it could be of use

    Tell me:

    • Names of the two sheets (main sheet and list sheet)

    Right now the main sheet is called 2025 and the list sheet is ListOptions. There will be a main sheet for each year (or at least which year there is a source of information) going back in time from 2025 and possibly going forward to keep a running record.

    • Which column holds the dropdown on the main sheet (A, B, etc.)

    The column that has the dropdown on the mainsheet is F. The first row that will hold drop entries in is 3. The first two rows are part of the header.

    • Whether the list sheet has a header row

    No, the list sheet has no header row. There is only the one row and the sheet name explains what is on the list


    I’ll paste a ready-to-use script tuned to those details.





    Add Value to Range Script


    function addValueToRange(value,sheet,column,outputToTopOfColumn,topOfColumnOffset) {

      if (outputToTopOfColumn === true) {

        const insertRowAtIndex = 1 + topOfColumnOffset;

        sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);

        sheet.getRange(insertRowAtIndex,column).setValue(value);

      } else {

        const lastCellInRangeIndex = sheet.getRange(1,column,Math.max(1,sheet.getLastRow()),1).getLastRow();

        const lastCellInRangeValue = sheet.getRange(lastCellInRangeIndex,column).getValue();

        const insertRowAtIndex = `${lastCellInRangeValue}`.length === 0 ? 1 : lastCellInRangeIndex + 1;

        sheet.getRange(insertRowAtIndex,column).insertCells(SpreadsheetApp.Dimension.ROWS);

        sheet.getRange(insertRowAtIndex,column).setValue(value);

      }

    }














    NOTES ABOUT NOTEBOOKLM OCR ETC


    I did get a chance to try notebooklm a little. I did have some pictures of some pages from a directory that I had taken recently. The were just taken with a cellphone while standing in front of the open book on the table, one page at a time (just of the few pages of interest, not the whole book).  I took a little over twenty pictures, but a few of them were of the same page.


    I did just a single page at first and it uploaded pretty quickly and I selected table and it was created pretty quickly and it looked pretty good, but I had to wait a while to compare the original image to the file that was created. What was there was pretty good, but the list of locations on the NotebookLM table had way fewer entries than what was on the image.


    The book was just open on the table, not pressed against a flat plane scanner. The books are at a library and they do have a scanner or at least a copier that can also save as pdf, maybe in other format or maybe a dedicated scanner. I’m not sure. 


    I first upload just a page or two. Things went pretty quickly. I then figured why not upload all twenty or so. Things just bogged down at that point. Maybe when the screen went to sleep every process did too.


    Later I experienced the same thing when only doing one or two pages. Maybe the network traffic is high, maybe some other glitch.





















    • If possible scan pages flat (library scanner or a flatbed) to avoid fold distortion; 

    I will stop by and see what they have and try to get some better scans. I suppose taking the book apart and using a drum scanner would be better yet, but I don’t think that is an option in this case.

    • smartphone photos can work if taken carefully and cropped/deskewed.

    I tried to be careful in taking them, mainly to be sure to include the whole page or at least to make sure so include all the parts of the page with information I’m interested in. I know there were sections of some of the images I took near the curve binding that I couldn't read myself.  I also see that cropping the image to only include the information of interest might improve things at least in terms of less image to process. Probably a “real” camera and a tripod might help too. I do have a good tripod and a more standard camera that might work well for this

    • For OCR: try Google Drive OCR or Adobe Scan for quick results; ABBYY/FineReader is better for batch cleanup. Post-process with a simple script or Google Sheets to normalize addresses.

    I was thinking of trying some other OCR method. Thanks for listing these.  


    • For Polk/Data Axle content, check library subscriptions and terms of use — if you can’t access a digital subscription, photographing book pages and OCRing them is a reasonable fallback.

    I will ask at the library about if they have a subscription to the Data Axle material and if so I can access it and how far back it may go. I’m not affiliated with or a student at any college or university in the area, but they might have a digital subscription. I’ll look into that as well and if so see if I might be able to interest a professor or someone in this project. It might even get some helpers, knowledge and insight.

    • NotebookLM and similar LLM-based tools can help extract structured data, but check privacy/training usage in the terms before uploading proprietary scans.

    I’m not sure what you mean by a proprietary scan. Maybe the Data Axle stuff? Maybe the Polk Directory? But, i’ll look into that as well. I’m just an individual doing this as a community project for whatever that is worth. I’m not doing this to create a product or service to sell, etc.

    • For manual entry, a Google Form tied to a normalized Sheet often speeds data capture and keeps formats consistent; you can adapt the form for two-volume vs four-volume layouts.

    I’ve been meaning to look into that a little. The spreadsheet is pretty straightforward as it is and I am trying to have it in the same order as the directories have things listed in. I when I get to the older books, and the older things on microfilm the format may change a few times. I did find a very old Polk directory that had been digitized. It really had little more than a few street addresses with no other information. 


    • If you want, share a few sample images or a spreadsheet template and I can suggest an OCR + extraction workflow or a small Apps Script to normalize the data and populate drop-downs.


    At the moment this is sort of in progress and I’ve got one with all the information as I received it and also added some cells with formulas to parse out some things to sort and standardize things. That includes some others to just work through how to do some other things. Those columns are all there, but off screen to the right and I’ve put the relevant things in the first few columns on screen. 

    Right now I have collected a list of all the street address points of the area and have added some that were not in the current list from the GIS. I have found in the directories and other listings of things some additional addresses. Often a business in a storefront or strip mall will expand and take up a larger space and there will be fewer addresses and at other times there are more businesses in the structure and there will be more addresses.


    Often there are listings for more than business that have been at the same address in the Polk Directory for a single year. Sometimes one business closes and another takes its place. Sometimes  I suspect a business is long gone, but Polk isn’t sure and retains it and sometimes there might be multiple businesses with different names that actually use the same street number address and just use their name or suite number to distinguish each other.

    In those cases there  is a list of them for that address. Ultimately I’d like to get this data into a GIS. I need a separate record for each entity even if they have the same address. The format of the street addresses from the county GIS had the street number, street name and street direction in all caps concatenated as a string. I don’t care for that and separated that out as street number then the rest of the string, but I’m thinking of adding a column number for each entity in the same year and then concatenate the earlier string with that number. That way each entity will have its own row and a unique identifier and the same address.

    I will put together a copy of a sheet as described above.



    Here is a link to the spreadsheet. I made a copy and it said it would include any scripts as an attachment. However, as I mentioned I wasn't able to save the script that I configured.  https://docs.google.com/spreadsheets/d/17BDiyoeA5UYVTod85KqYk9orBjIe9EIFe3ttgN65XTg/edit?usp=sharing

    Below is the script that wasn't saved:

    function onEdit(e) {
    if (!e) return;
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const mainSheetName = '2025';
    const listSheetName = 'ListOptions';
    const targetColumn = 1; // column A
    const listColumn = 1; // column A in the ListOptions
    const range = e.range;
    const sheet = range.getSheet();
    if (sheet.getName() !== mainSheetName) return;
    if (range.getNumColumns() !== 1 || range.getNumRows() !== 1) return; // single cell only
    if (range.getColumn() !== targetColumn) return;


    const newValue = e.value;
    if (!newValue) return; // nothing to add (e.g., deletion)


    const listSheet = ss.getSheetByName(listSheetName);
    if (!listSheet) return;


    let lastRow = listSheet.getLastRow();
    // if the list is empty, consider the first available row
    if (lastRow < 1) lastRow = 0;
    const listRange = listSheet.getRange(1, listColumn, Math.max(1, lastRow), 1);
    const listValues = listRange.getValues().flat().filter(v => v !== '');
    if (listValues.includes(newValue)) return;


    listSheet.getRange(lastRow + 1, listColumn).setValue(newValue);


    // optional: reorder the list alphabetically
    listSheet.getRange(1, listColumn, lastRow + 1, 1).sort({column: listColumn, ascending: true});



    --
    You received this message because you are subscribed to a topic in the Google Groups "Google Apps Script Community" group.
    To unsubscribe from this topic, visit https://groups.google.com/d/topic/google-apps-script-community/QObsC1Za5cI/unsubscribe.
    To unsubscribe from this group and all its topics, send an email to google-apps-script-c...@googlegroups.com.

    Tom Mort

    unread,
    Mar 16, 2026, 3:07:32 PM (2 days ago) Mar 16
    to google-apps-sc...@googlegroups.com





























    Hi Mario,


    I had intended to send some of those page images I used, but forgot. I also thought I’d play around trying some other OCR software that you suggested and some others. I have heard of ABBYY/FineReader. I see not only is it Windows only it is also a subscription service. I use Linux, but do have Windows11 on a VM. The current state of windows is sort of torture and having to pay for experience that torture just compounds things. I see there might be a way to run it on Linux, but I’m not sure if it is current.s 


    Looks like the Adobe app is also not free and I suppose also a subscription and of course it is Adobe, different but similar to above.


    I was able to at least run the demo for the Google Cloud OCR. It worked reasonably well for about half a the page I tried it on.  The half of the page that was the best imae was not included. Maybe that was a limitation of the demo. It did read things pretty weil, but it didn’t categorize things and put them into a table like the NotebookLM does. I do see there are all sorts of APIs and ways to configure it to do different things. Looks like that could be a bigger project than just manual entry. I’ll be going  back quite a few years, but the actual number or addresses of interest will not be all that large. I suspect this too is fee based.


    I played around a little with a linux app, ImageReader which uses Tesseract.  ImageReader got some words/strings right while it looks like many others got translated to some other language entirely.  I’ve downloaded and attached my demo session with the Google Cloud OCR. I’m attaching it and some of the images I had of pages that I tried. I’ll also attach the text files I generated with the other apps.


    I see there are many OCR apps and services for pdf. It would be easy enough to convert an image file to a pdf, but it would only have an image layer. Maybe they are to scan a pdf that has only an image and it does OCR and adds a text layer. 


    I don’t think I will have a chance to get to the library and use any sort of flatbed scanner today, but will go there when I get a chance. I think I’ll do a few scans of the newer four volume books, a few of the older two volume books and then the older ones on Microfilm. I can get an idea what the layouts are of the different ones and for the microfilm ones how readable the text is.



    On Thu, Mar 12, 2026 at 10:35 PM Mario Rossi <projectlaba...@gmail.com> wrote:
    --
    You received this message because you are subscribed to a topic in the Google Groups "Google Apps Script Community" group.
    To unsubscribe from this topic, visit https://groups.google.com/d/topic/google-apps-script-community/QObsC1Za5cI/unsubscribe.
    To unsubscribe from this group and all its topics, send an email to google-apps-script-c...@googlegroups.com.
    ImageReaderleonardpp337octrtxt.txt
    Google OCR pasted into Docs & downloaded as pdf.pdf
    Reply all
    Reply to author
    Forward
    0 new messages