Creare un elemento select dinamico con JavaScript puro
Questo articolo mostra come generare dinamicamente un elemento HTML select a partire da un array di dati, inserire l'elemento nel DOM e reagire alle selezioni dell'utente mostrando il valore scelto. L'intero script viene eseguito in strict mode all'interno di una Immediately Invoked Function Expression (IIFE), mantenendo pulito lo scope globale.
La sorgente dati
Tutto parte da un array costante chiamato DATA. Ogni elemento è un oggetto semplice dotato di due proprietà: un'etichetta leggibile label e un valore numerico value. Questa struttura rispecchia la forma tipica delle opzioni che si forniscono a qualsiasi controllo di un modulo.
// Sorgente dati: ogni oggetto contiene un'etichetta e un valore numerico
const DATA = [
{ label: 'One', value: 1 },
{ label: 'Two', value: 2 },
{ label: 'Three', value: 3 }
];
Poiché DATA risiede all'interno della IIFE, non può essere sovrascritto accidentalmente da altri script presenti nella pagina. L'array è dichiarato con const, quindi anche la riassegnazione è impedita a livello di linguaggio.
Il wrapper IIFE
L'intero modulo è racchiuso in una Immediately Invoked Function Expression:
// Funzione auto-invocante che racchiude tutto il modulo
'use strict';
(function() {
// ...corpo del modulo...
})();
La direttiva 'use strict' posta in cima attiva la modalità rigorosa per l'intero script. All'interno della funzione anonima, ogni variabile, funzione e costante ha scope locale. Questo pattern era il modo standard per creare moduli privati prima della diffusione degli ES Modules e resta perfettamente valido negli script caricati tramite un semplice tag <script>.
Ascolto dei cambiamenti con la delega degli eventi
Invece di agganciare un listener direttamente all'elemento <select>, il codice utilizza la delega degli eventi. La funzione handleDynamicChangeEvent registra un unico listener change sull'oggetto document:
// Delega degli eventi: un unico listener sul documento gestisce tutti i select dinamici
function handleDynamicChangeEvent() {
document.addEventListener('change', evt => {
const element = evt.target;
if (element.classList.contains('dynamic-select')) {
element.nextElementSibling.innerText =
element.options[element.selectedIndex].value;
}
}, false);
}
Quando un qualsiasi evento change risale tramite bubbling fino al documento, il gestore verifica se l'elemento che lo ha generato possiede la classe CSS dynamic-select. In caso affermativo, legge il valore dell'opzione attualmente selezionata e lo scrive nell'elemento immediatamente successivo al <select> nel DOM, ovvero il suo nextElementSibling.
La delega degli eventi offre due vantaggi pratici in questo contesto. In primo luogo, funziona anche se il <select> non esiste ancora al momento della registrazione del listener. In secondo luogo, se in futuro venissero creati più select dinamici, un unico listener li gestirebbe tutti senza alcuna configurazione aggiuntiva.
Il terzo argomento passato ad addEventListener è false, il che significa che il gestore si attiva durante la fase di bubbling e non durante quella di capturing. Si tratta del comportamento predefinito, ma l'autore lo rende esplicito per maggiore chiarezza.
Costruzione dell'elemento select
La funzione createDynamicSelect si occupa di costruire il nodo <select> e di iniettarlo nella pagina:
// Crea un elemento select dinamico e lo inserisce nel DOM prima dell'elemento target
function createDynamicSelect(target = null, items = []) {
if (!target || items.length === 0) {
return;
}
const select = document.createElement('select');
select.className = 'dynamic-select';
// Costruisce le opzioni come stringhe HTML
let html = ['<option value="">Select an option</option>'];
for (const item of items) {
const option = `<option value="${item.value}">${item.label}</option>`;
html.push(option);
}
select.innerHTML = html.join('');
// Inserisce l'HTML del select subito prima dell'elemento target
target.insertAdjacentHTML('beforeBegin', select.outerHTML);
}
La funzione accetta due parametri con valori predefiniti: target (il nodo DOM prima del quale verrà posizionato il select) e items (l'array di dati). Una clausola di guardia all'inizio esce immediatamente se uno dei due argomenti è assente o vuoto, evitando manipolazioni inutili del DOM.
Un elemento <select> viene creato programmaticamente con document.createElement e gli viene assegnata la classe dynamic-select. Le opzioni vengono costruite come un array di stringhe HTML. Un'opzione segnaposto con valore vuoto compare per prima, seguita da un'opzione per ogni elemento dell'array. I template literal rendono l'interpolazione pulita e leggibile.
L'array viene unito in un'unica stringa e assegnato a select.innerHTML. Infine, insertAdjacentHTML con la posizione 'beforeBegin' colloca l'HTML serializzato dell'elemento select immediatamente prima del nodo target nell'albero del DOM. Si tratta di un dettaglio degno di nota: il codice non inserisce l'oggetto elemento in sé, bensì la sua rappresentazione sotto forma di stringa outerHTML. La conseguenza è che il nodo risultante nel DOM è un elemento nuovo, diverso da quello memorizzato nella variabile select.
Inizializzazione al DOMContentLoaded
L'ultimo blocco collega tutti i pezzi:
// Inizializzazione: attende che il DOM sia pronto, poi configura il select dinamico
document.addEventListener('DOMContentLoaded', () => {
const output = document.querySelector('form .output');
handleDynamicChangeEvent();
createDynamicSelect(output, DATA);
});
Il codice attende l'evento DOMContentLoaded, che si attiva non appena l'HTML è stato completamente analizzato dal parser, senza attendere fogli di stile, immagini o subframe. All'interno della callback, individua l'elemento corrispondente al selettore form .output e ne conserva un riferimento nella variabile output. Questo elemento svolge un doppio ruolo: è l'ancora di inserimento per il select (che comparirà subito prima di esso) e, grazie a nextElementSibling nel gestore del cambiamento, è anche il contenitore in cui verrà visualizzato il valore selezionato.
L'ordine delle due chiamate a funzione è rilevante. handleDynamicChangeEvent viene invocata per prima, cosicché il listener delegato sia già attivo quando createDynamicSelect aggiunge il select alla pagina. In pratica, dato che il listener risiede sul documento e reagisce all'interazione dell'utente, l'ordine funzionerebbe in entrambi i sensi; tuttavia, registrare il listener per primo è un'abitudine più sicura che evita condizioni di gara in scenari più complessi.
Presupposti sulla struttura HTML
Lo script presuppone che la pagina ospite contenga un <form> con un elemento figlio dotato della classe output. Un markup minimo compatibile si presenta così:
<!-- Markup minimo compatibile con lo script -->
<form>
<span class="output"></span>
</form>
Dopo l'esecuzione dello script, l'elemento <select> generato compare immediatamente prima dello span .output all'interno del form. Quando l'utente sceglie un'opzione, il valore numerico viene scritto nello span come contenuto testuale.
Possibili miglioramenti
Sebbene il codice sia funzionale e ben strutturato, alcuni accorgimenti potrebbero renderlo più robusto per un utilizzo in produzione.
La funzione createDynamicSelect costruisce il markup delle opzioni tramite concatenazione di stringhe. Se le proprietà label o value contenessero caratteri come virgolette doppie o parentesi angolari, l'HTML risultante si romperebbe. Sanificare gli input oppure usare document.createElement('option') per ogni elemento eliminerebbe il rischio di injection.
L'uso di insertAdjacentHTML con outerHTML è una tecnica creativa, ma implica che il nodo select in memoria non viene mai effettivamente inserito: viene inserita solo la sua copia serializzata. Se la funzione dovesse restituire un riferimento all'elemento attivo nel DOM, una strategia di inserimento diversa come parentNode.insertBefore sarebbe più appropriata.
Infine, il gestore del cambiamento scrive su innerText, mostrando solo il valore grezzo. In un'interfaccia più ricca potrebbe essere preferibile mostrare l'etichetta al posto del valore, oppure emettere un evento personalizzato che altre parti dell'applicazione possano ascoltare.
Codice sorgente completo
// Codice sorgente completo del modulo
'use strict';
(function() {
// Array di dati utilizzato per popolare il select
const DATA = [
{ label: 'One', value: 1 },
{ label: 'Two', value: 2 },
{ label: 'Three', value: 3 }
];
// Registra un listener delegato sul documento per gestire i cambiamenti dei select dinamici
function handleDynamicChangeEvent() {
document.addEventListener('change', evt => {
const element = evt.target;
if (element.classList.contains('dynamic-select')) {
element.nextElementSibling.innerText =
element.options[element.selectedIndex].value;
}
}, false);
}
// Crea un elemento select con le opzioni fornite e lo inserisce prima del target
function createDynamicSelect(target = null, items = []) {
if (!target || items.length === 0) {
return;
}
const select = document.createElement('select');
select.className = 'dynamic-select';
let html = ['<option value="">Select an option</option>'];
for (const item of items) {
const option = `<option value="${item.value}">${item.label}</option>`;
html.push(option);
}
select.innerHTML = html.join('');
target.insertAdjacentHTML('beforeBegin', select.outerHTML);
}
// Punto di ingresso: attende il caricamento del DOM, poi avvia la configurazione
document.addEventListener('DOMContentLoaded', () => {
const output = document.querySelector('form .output');
handleDynamicChangeEvent();
createDynamicSelect(output, DATA);
});
})();