< Home
Stampa

Web applications

Sommario

Best practices e worst practices

Javascript è un linguaggio che è nato come come semplice linguaggio di scripting per dare qualche animazione alle pagine web, mentre il grosso dell’elaborazione era svolta lato server. All’inizio quindi il linguaggio aveva numerose limitazioni che ne impedivano la realizzazione di applicazioni vere e proprie, ed aveva come caratteristica principale una certa immediatezza anche per i non programmatori.

Solo dal 2010 si è evoluto in un linguaggio moderno, ed ha permesso di creare una comunità di sviluppatori ed esperti in grado di proporre metodologie e tecniche di programmazione efficienti e valide.

Per questa ragione quando uno studente o un programmatore cerca aiuto su web, molto spesso trova esempi di codice obsoleti o deprecati, e non è in grado di capire se oggi sono ancora validi. Lo stesso vale per le IA, che per pura forza statistica, propongono soluzioni spesso antiquate o errate.

In questa lezione e nella prossima lezione daremo quindi delle linee guida che aiutino a capire come fare una Web Application in Javascript, e non una semplice pagina html con del Javascript.

Essa si basa su questi principi fondamentali:

  • la Separation of Concerns: separazione tra html, CSS, JS
  • Evento, azione, reazione: il modello dell’interazione tra utente e software
  • separazione dell’applicazione in layers e componenti

Separation of concerns

La regola della “Separation of Concerns” SoC ci dic che deve esserci una netta separazione tra HTML, Javascript e CSS. L’HTML deve contenere la sola struttura del documento ed i contenuti. Il CSS deve dare le istruzioni per il layout, la tipografia, i colori e la grafica in generale. Il Javascript è il motore di funzionamento dell’intera WebApp.

Va osservato che tecnicamente è possibile inserire sia CSS che JS nell’HTML. Ma farlo implicherebbe mescolare documenti che hanno scopi diversi, rendendo difficoltoso gestire il progetto, perché bisognerebbe cercare le istruzioni Javascript anche nell’HTML e controllare se non siano in conflitto tra loro. Lo stesso ragionamento vale per il CSS. Inoltre la SoC permette di riusare html, css e JS in progetti successivi.

Sintetizzando le regole sono queste:

  • nell’HTML, mettere solo tag html. Per usare quindi un pulsante o una form è necessario crearne un riferimento nel DOM dello script e usarlo in questo modo.
  • nel JS si può generare HTML, tramite templates (oppure attraverso Virtual DOM, una tecnica che tiene in memoria sotto forma di oggetto JS dell’intero DOM, e sincronizza periodicamente col vero DOM).
  • non si applicano stili in linea in HTML ma si creano classi CSS.

Questo significa che molte worst practices sono da evitare:

  1. <button onclick="funzione()" ../> è una pratica scorretta, perché inseriamo codice JS nell’HTML
  2. div.appendChild(element) è un modo per inserire elementi DOM direttamente in pagina. E’ tecnicamente corretto ma è prestazionalmente inefficace. Il problema però è che la manipolazione diretta del DOM da parte di JS (ovvero in modo programmatico) è rischiosa perché non permtte un debugging efficace.
  3. button.addEventListener("click", function()) è un modo molto rischioso per aggiungere gestori evento, perché se è eseguita più volte, continua ad aggiungere gestori evento allo stesso elemento. Molto meglio usare button.onclick = function() che sostituisce ogni funzione precedente.

Evento, azione, reazione

Una pagina web segue un modello di comportamento interattivo, fatto di fasi di visualizzazione di contenuti ed azioni da parte dell’utente (click su pulsanti, compilazione di form, scroll, ecc.). In Javascript questo si traduce in un modello di programmazione basato su eventi:

  • l’interazione dell’utente: inserimento form, click su pulsanti e link, mouse scroll, ecc.
  • eventi di sistema: load della pagina, eventi legati al caricamenti di dati, ecc.
  • eventi generati dal programma stesso: setTimeout, setInterval, ecc.

Per questa ragione un modello per razionalizzare e semplificare lo sviluppo dell’applicazione consiste nel suddividere la gestione degli eventi in tre blocchi concettuali:

  • Event bind: istruzioni volte a gestire ed intercettare un determinato evento (click, scroll, key, ecc.) associandolo ad una funzione detta gestore evento (event handler) che lo gestirà;
  • azione: l’event handler svolge la cosiddetta “azione”: analizza l’evento, legge il DOM e il BOM, ie svolge una qualche azione sui dati e le variabili che gestisce l’applicazione. L’azione comporta una modifica dello stato interno dell’applicazione.
  • reazione: al termine dell’azione viene svolta una modifica della pagina web con una operazione chiamata render.

Facciamo un esempio. Ipotizziamo di voler scrivere una pagina web che contiene una form (input text e button) ed un ul. Ad ogni pressione del pulsante, JS legge il contenuto della form e lo aggiunge ad una lista di valori che stampa. Vediamo il codice:

<div >
    <h2>La mia Lista</h2>
    
    <form>
        <input type="text" id="item" placeholder="Scrivi qualcosa...">
        <button id="button" type="button">Aggiungi</button>
    </form>

    <ul id="itemList"></ul>
</div>

Come si vede, l’HTML non contiene JS.

Vediamo ora il JS:

    const elements = [];

    // binding del DOM
    const input = document.getElementById('item');
    const button = document.getElementById('button');
    const ul = document.getElementById('itemList');

    // EVENT bind
    button.onclick = eventHandler

    // AZIONE
    function eventHandler {
        const value = input.value.trim();
        if (value !== '') {
            elements.push(value);
            render(); // azione -> reazione
        }
    });

    // REAZIONE
    function render() {
        input.value = '';
        const template = '<li>%VALUE</li>';
        const rows = elements.map((element) => {
           return template.replace("%VALUE", element);
        }
        ul.innerHTML = rows.join("\n");
    }

Come si vede:

  • ogni evento è gestito separatamente dal suo gestore eventi;
  • il gestore eventi è l’azione, che svolge una operazione sui soli dati, e NON modifica l’HTML;
  • l’azione chiama la reazione, ad esempio la funzione render, che modifica l’HTML (e non fa niente altro)

Questo modello si applica sia quando c’è un solo evento, azione, reazione, sia quando ce ne sono molti eventi, che a loro volta provocano azioni e reazioni. L’obiettivo è scomporre un problema complesso come l’interattività in un insieme di operazioni più semplici e facili di realizzare.

E’ alla base di tutti i moderni framework Javascript, come Angular e soprattutto React (che prende proprio il nome da questo modello).

Generazione di HTML da Javascript

Un sistema veloce, semplice e soprattutto efficiente per creare html con Javascript consiste nel produrre una stringa di testo contenente HTML e “iniettarla” nella pagina, usando la proprietà element.innerHTML. In pratica il codice HTML viene inserito come se fosse testo, ed automaticamente il browser esegue il “rendering” in html e quindi modifica la pagina web, il DOM e l’oggetto document.

Ad esempio se abbiamo questo elemento:

<div id="container">

</div>

Ed eseguiamo questo codice[2]:

const container = document.getElementById("container");
container.innerHTML = `
 <div>Ciao</div>
`;

Otterremo questo html:

<div id="container">
    <div>
      Ciao
    </div>
  </div>

L’html una volta generato si comporta esattamente come l’html scritto in fase di coding (compile time), e quindi l’html generato diventa parte del DOM, e quindi è possibile gestirlo con document.

Questo sistema è utile soprattutto quando dobbiamo eseguire una trasformazione di dati in HTML, ad esempio per creare tabelle o liste, o pagine di schede.

Trasformazioni dai dati all’HTML

Quando dobbiamo scrivere html in modo ripetitivo, ovvero ad esempio righe di una tabella dove la struttura del codice è sempre la stessa, ma cambiano solo i dati, conviene usare un template, che andremo poi a personalizzare coi dati veri e propri.

E’ possibile usare map() e join() per trasformare velocemente una tabella con dei dati in html. Per generare un elenco di colonne di tabella HTML si può fare così:

const list = [1,2,3];
const template = "<td>%VALUE</td>";
const htmlElements = list.map(element => template.replace("%VALUE", element));
// Otterremo: ["<td>1</td>", "<td>2</td>", "<td>3</td>"]

Da qui è poi possibile usare join():

const htmlRow = htmlElements.join(""); 
// "<td>1</td><td>2</td><td>3</td>"
container.innerHTML = html;

La stessa tecnica si può replicare anche per gli UL

const list = [1,2,3];
const template = "<li>%VALUE</li>";
const htmlElements = list.map(element => template.replace("%VALUE", element));
const html = htmlElements.join("");
container.innerHTML = html;

E’ anche possibile creare tabelle intere come vedremo in questo esempio.

Un esempio di applicazione

Facciamo un esempio concreto.

Scrivere una webapp che valida un form. Il form contiene i seguenti campi:

  • nome: deve essere alfanumerico di almeno 3 lettere e cominciare con una lettera
  • cognome: deve essere alfanumerico di almeno 3 lettere e cominciare con una lettera
  • anno di nascita: deve essere tra 1900 e 2023
  • mese di nascita: deve essere tra 1 e 12
  • giorno di nascita: deve essere un giorno valido rispetto al mese
  • città: deve essere di almeno 2 lettere, cominciare con lettera
  • paese: deve essere almeno 4 lettere e cominciare con una lettera

E’ presente un pulsante “conferma”.

Una volta che il form viene compilato ed inviato, il programma JS deve:

  • validare il form secondo le regole: se non è valido deve indicare i campi di input non validi;
  • se il form è valido andrà ad inserire i valori in una tabella

Vediamo il codice.

Qui l’html:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <title>Form anagrafica</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" />
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
</head>

<body class="container">
  <div class="row">
    <div class="form">
    <label for="nome" class="form-label">Nome: </label>
    <input id="nome" type="text" class="form-control"/>
    <label for="cognome" class="form-label">Cognome: </label>
    <input id="cognome" type="text" class="form-control"/>
    <label for="anno" class="form-label">Anno: </label>
    <input id="anno" type="number" class="form-control"/>
    <label for="mese" class="form-label">Mese: </label>
    <input id="mese" type="number" class="form-control"/>
    <label for="giorno" class="form-label">Giorno: </label>
    <input id="giorno" type="number" class="form-control"/>
    <button id="submit" type="button" class="btn btn-success">Inserisci</button>
  </div>
  </div>
  <div class="row" id="content2">
    <table class="table">
      <thead>
        <tr><th>Nome</th><th>Cognome</th><th>Anno</th><th>Mese</th><th>Giorno</th></tr>
      </thead>
      <tbody id="tbody">
        
      </tbody>
    </table>

  </div>
  <script src="index.js"></script>
</body>

</html>

Come si vede dall’esempio la tabella è inizialmente vuota. 

Passiamo al Javascript. Prima di tutto eseguiamo il binding degli oggetti del DOM:

const inputNome = document.querySelector('#nome');
const inputCognome = document.querySelector('#cognome');
const inputAnno = document.querySelector('#anno');
const inputMese = document.querySelector('#mese');
const inputGiorno = document.querySelector('#giorno');
const buttonSubmit = document.querySelector('#submit');
const tbody = document.querySelector('#tbody');

Fatta l’inizializzazione veniamo alle funzionalità.

Alla pressione del pulsante (cioè l’evento), quello che ci aspettiamo che faccia l’applicazione è:

  1. associare l’evento button.onclick ad un event handler;
  2. carica i dati del form in un oggetto (operazione chiamata parse);
  3. esegue la validazione (funzione validator)
  4. Se non è valido modifica le classi degli input non validi (funzione renderCheckForm)
  5. Se è valido aggiunge i dati alla lista dell’anagrafica (funzione addElement), pulisce il form (renderClearForm) e crea la tabella html (renderList)

Vediamo quindi il codice che abbiamo appena descritto:

Evento (event handler):

buttonSubmit.onclick = () => {
    const data = parser();
    const result = validator(data);
    if (result.valid) {
        addElement(data);
        renderCleanForm();        
        renderList();
    } else {
        renderCheckForm(result);
    }    
}

Azioni:

const list = [];

const addElement = (data) => {
    list.push(data);
}
const isLetter = c => c.toLowerCase() !== c.toUpperCase();

const validator = (data) => {
    const result = {}
    result.nome = data.nome.length >= 3 && isLetter(data.nome[0]);
    result.cognome = data.cognome.length >= 3 && isLetter(data.cognome[0]);
    result.anno = data.anno >= 1900 && data.anno <= 2026;
    result.mese = data.mese >= 1 && data.mese <= 12;
    result.giorno = data.giorno >= 1 && data.giorno <= 31;
    result.valid = result.nome&&result.cognome&&result.anno&&result.mese&&result.giorno;
    return result;
}

const parser = () => {
    return {
        nome: inputNome.value,
        cognome: inputCognome.value,
        anno: inputAnno.value,
        mese: inputMese.value,
        giorno: inputGiorno.value
    }
}

Reazioni (render):

const renderCleanForm = (data) => {
    inputNome.value = "";
    inputCognome.value = "";
    inputAnno.value = "";
    inputMese.value = "";
    inputGiorno.value = "";
}

const renderCheckForm = (result, data) => {
    if (!result.nome) {
        inputNome.classList.add('is-invalid');
    } else {
        inputNome.classList.remove('is-invalid');
    }
    if (!result.cognome) {
        inputCognome.classList.add('is-invalid');
    } else {
        inputCognome.classList.remove('is-invalid');
    }
    if (!result.anno) {
        inputAnno.classList.add('is-invalid');
    } else {
        inputAnno.classList.remove('is-invalid');
    }
    if (!result.mese) {
        inputMese.classList.add('is-invalid');
    }else {
        inputMese.classList.remove('is-invalid');
    }
    if (!result.giorno) {
        inputGiorno.classList.add('is-invalid');
    }else {
        inputGiorno.classList.remove('is-invalid');
    }
}

const renderList = () => {
    const htmlRows = list.map((data, index) => {
        const template = `<tr>
           <td>%NOME</td><td>%COGNOME</td><td>%ANNO</td><td>%MESE</td><td>%GIORNO</td>        
        </tr>`;
        const htmlRow = template
            .replace('%NOME', data.nome)
            .replace('%COGNOME', data.cognome)
            .replace('%ANNO', data.anno)
            .replace('%MESE', data.mese)
            .replace('%GIORNO', data.giorno)
        return htmlRow;
    }); 
    tbody.innerHTML = htmlRows.join("\n");
    interactorList();
}

Le funzioni renderCleanForm e renderCheckForm sono molto chiare: manipolano DOM esistente. La funzione renderList invece è generativa, crea nuovo HTML (e quindi nuovo DOM).

Abbiamo bisogno di creare un template: tramite map e join poi andiamo a popolare la tabella coi dati della lista.

Tutto il lavoro consiste in manipolazione di stringhe: per ogni riga, si trasforma il template in una stringa contenente effettivo codice html coi dati.

E’ importante osservare che l’aggiunta di un nuovo elemento anagrafica non viene direttamente aggiunto alla tabella. La tabella viene ricreata da zero e viene sostituita quella precedente.

Con questo sistema  on si manipola quindi l’html precedente, ma lo si cancella e lo si ricrea. Questo sistema è più efficiente di altri sistemi, come per esempio quello di aggiungere solo l’html di una riga alla volta: in questo caso dovremmo analizzare l’html già generato analizzando quindi il DOM ed eseguendo il binding. Questa tecnica è ormai oggi considerata deprecata perché molto inefficiente nei browser moderni. E’ sempre più efficiente invece cancellare e rigenerare l’html, operazione che viene agevolata dai motori di visualizzazione dei moderni browsers.

Scrivere il codice in questo modo aiuta a tenere separati i due ambiti principali dell’applicazione:

  • i dati sono memorizzati in modo efficiente usando array, dizionari, ecc. e sono usati dal Javascript, in una parola, il modello dati;
  • l’interfaccia utente è invece in HTML e JS crea solo delle stringhe di testo HTML tramite una funzione render: JS non manipola MAI l’HTML, ma esegue il binding solo delle parti della pagina da leggere (es. form) o da cui intercettare eventi (es. pulsanti).

Interagire con HTML generato

Il DOM generato, in una applicazione reale, spesso è interattivo. Possiamo cioè generare HTML contenente ad esempio bottoni cliccabili, che a loro volta generano eventi, azioni, e reazioni.

Modifichiamo il nostro programma:

  • per ogni riga prevediamo un pulsante elimina ed un pulsante modifica;
  • alla pressione di elimina viene cancellata la riga corrispondente;
  • alla pressione del pulsante modifica la riga corrispondente viene inserita nel form per la modifica.

Modifichiamo renderList in questo modo:

const renderList = () => {
    const htmlRows = list.map((data, index) => {
        const template = `<tr>
        <td>%NOME</td><td>%COGNOME</td><td>%ANNO</td><td>%MESE</td><td>%GIORNO</td>
        <td>
            <button id='DEL%ID' class='btn btn-danger action-delete'>Elimina</button>
            <button id='EDIT%ID' class='btn btn-warning action-edit'>Modifica</button>
        </td>
        </tr>`;
        const htmlRow = template
            .replace('%NOME', data.nome)
            .replace('%COGNOME', data.cognome)
            .replace('%ANNO', data.anno)
            .replace('%MESE', data.mese)
            .replace('%GIORNO', data.giorno)
            .replaceAll('%ID', index);
        return htmlRow;
    }); 
    tbody.innerHTML = htmlRows.join("\n");
    interactorList();
}

In pratica generiamo due pulsanti in un nuovo TD, e diamo loro un id specifico:

  • <button id='DEL%ID' class='btn btn-danger action-delete'>Elimina</button>

Questo pulsante avrà ID pari alla posizione dell’elemento nella lista (quindi avremo "DEL0", "DEL1", "DEL2", ecc.

  • <button id='EDIT%ID' class='btn btn-warning action-edit'>Modifica</button>

Questo pulsante avrà ID pari alla posizione dell’elemento nella lista (quindi avremo "EDIT0", "EDIT1", "EDIT2", ecc.

In pratica avremo una coppia di pulsanti per ogni riga ognuno con il suo ID univoco.

A questo punto scriviamo la funzione interactorList, che inserisce gli event handler per i pulsanti creati:

const interactorList = () => {
    document.querySelectorAll('.action-delete').forEach((button) => {
        button.onclick = () => {
            const id = parseInt(button.id.replace('DEL', ''));
            deleteElement(id);
        }
    });
    document.querySelectorAll('.action-edit').forEach((button) => {
        button.onclick = () => {
            const id = parseInt(button.id.replace('EDIT', ''));
            modifyElement(id);
        }
    });
}

Come si vede utilizziamo la classe “action-delete” per intercettare tutti i pulsanti di eliminazione, e per ciascuno aggiungiamo un event handler, che recupera l’id della riga corrispondente ed esegue la funzione deleteElement(id). Lo stesso comportamento viene svolto per i pulsanti di editing.

Qui le funzioni deleteElement e modifyElement:

const deleteElement = (id) => {
    list.splice(id, 1);
    renderList();
}

const modifyElement = (id) => {
    const data = list[id];    
    list.splice(id, 1);
    renderForm(data);
    renderList();
}

Manca quindi la funzione renderForm:

const renderForm = (data) => {
    inputNome.value = data.nome;
    inputCognome.value = data.cognome;
    inputAnno.value = data.anno;
    inputMese.value = data.mese;
    inputGiorno.value = data.giorno;
}

che in modo lineare va a riempire gli input dei valori dell’elemento della riga che si vuole editare.

La nostra applicazione è quindi completamente dinamica ed interagisce sia con elementi scritti dal programmatore in html, sia con elementi generati via JS.

Layers e componenti

La costruzione di una Web Application non si limita solo a definire dei flussi Evento->azione->reazione: occorre comprendere che essa non è semplicemente un insieme di funzioni collegate tra di loro, ognuna responsabile di un pezzo dell’elaborazione, della presentazione o dell’interazione con l’utente.

Questo modello può andare bene per applicazioni molto semplici, con poche righe di codice, e con uno-due eventi semplici. Nel momento in cui aumenta la complessità, già anche solo nell’esempio precedente, si rischia di fare confusione nell’organizzazione logica del codice.

Per questo occorre pensare alle applicazioni Web (e non solo web) come a degli organismi, che hanno cioè apparati, composti da organi, che nel linguaggio del software chiameremo layers (livelli) e componenti. Un componente applicativo è una porzione di software (ad esempio una funzione) che svolge un preciso compito, ed è collocata in un layer, ovvero una porzione ben definita dell’applicazione, che svolge un ruolo specifico.

Nelle applicazioni web possiamo individuare fin da subito questi 3 layer:

Ogni layer ha un ruolo ben definito:

  • Model/Business logic contiene i dati interni dell’applicazione (es. la lista) e le funzioni per gestirli direttamente;
  • View contiene le funzioni che mostrano direttamente all’utente i contenuti dell’applicazione (nel nostro caso l’HTML);
  • Presentation/Interaction contiene le funzioni per fare interagire gli altri due layer: i gestori eventi e le render.

Le frecce indicano invece la direzione del flusso dati: dal model al presenter, dal presenter alla View, dalla View all’Interactor, dall’Interactor di nuovo al Model.

Con questo sistema abbiamo immediatamente un insieme di vantaggi nella progettazione e nello sviluppo applicativo:

  • non si fa confusione mettendo nella stessa funzione compiti differenti;
  • è possibile organizzare il codice in gruppi di funzioni che hanno responsabilità tra loro omogenee;
  • è possibile scrivere singolarmente ogni componente senza danneggiare gli altri.

Un esempio di applicazione multilayer

Torniamo alla nostra applicazione: l’utilizzo dei layer ci consente di definire in modo super chiaro chi fa cosa e di cosa è responsabile, possiamo quindi fin da subito riorganizzare le funzioni per layer:

const inputNome = document.querySelector('#nome');
const inputCognome = document.querySelector('#cognome');
const inputAnno = document.querySelector('#anno');
const inputMese = document.querySelector('#mese');
const inputGiorno = document.querySelector('#giorno');
const buttonSubmit = document.querySelector('#submit');
const tbody = document.querySelector('#tbody');

// #regionMODEL
const list = [];

const deleteElement = (id) => {
    list.splice(id, 1);
    renderList();
}

const modifyElement = (id) => {
    const data = list[id];    
    list.splice(id, 1);
    renderForm(data);
    renderList();
}

const addElement = (data) => {
    list.push(data);
}

const isLetter = c => c.toLowerCase() !== c.toUpperCase();

const validator = (data) => {
    const result = {}
    result.nome = data.nome.length >= 3 && isLetter(data.nome[0]);
    result.cognome = data.cognome.length >= 3 && isLetter(data.cognome[0]);
    result.anno = data.anno >= 1900 && data.anno <= 2026;
    result.mese = data.mese >= 1 && data.mese <= 12;
    result.giorno = data.giorno >= 1 && data.giorno <= 31;
    result.valid = result.nome&&result.cognome&&result.anno&&result.mese&&result.giorno;
    return result;
}

const mainLogic = () => {
   const data = parser();
    const result = validator(data);
    if (result.valid) {
        addElement(data);
        renderCleanForm();        
        renderList();
    } else {
        renderCheckForm(result);
    }
}

// #endregion

// #region INTERACTOR

const interactorList = () => {
    document.querySelectorAll('.action-delete').forEach((button) => {
        button.onclick = () => {
            const id = parseInt(button.id.replace('DEL', ''));
            deleteElement(id);
        }
    });
    document.querySelectorAll('.action-edit').forEach((button) => {
        button.onclick = () => {
            const id = parseInt(button.id.replace('EDIT', ''));
            modifyElement(id);
        }
    });
}

const parser = () => {
    return {
        nome: inputNome.value,
        cognome: inputCognome.value,
        anno: inputAnno.value,
        mese: inputMese.value,
        giorno: inputGiorno.value
    }
}

buttonSubmit.onclick = () => {
    mainLogic();    
}

// #endregion

// #region PRESENTER

const renderCleanForm = (data) => {
    inputNome.value = "";
    inputCognome.value = "";
    inputAnno.value = "";
    inputMese.value = "";
    inputGiorno.value = "";
}

const renderForm = (data) => {
    inputNome.value = data.nome;
    inputCognome.value = data.cognome;
    inputAnno.value = data.anno;
    inputMese.value = data.mese;
    inputGiorno.value = data.giorno;
}

const renderCheckForm = (result, data) => {
    if (!result.nome) {
        inputNome.classList.add('is-invalid');
    } else {
        inputNome.classList.remove('is-invalid');
    }
    if (!result.cognome) {
        inputCognome.classList.add('is-invalid');
    } else {
        inputCognome.classList.remove('is-invalid');
    }
    if (!result.anno) {
        inputAnno.classList.add('is-invalid');
    } else {
        inputAnno.classList.remove('is-invalid');
    }
    if (!result.mese) {
        inputMese.classList.add('is-invalid');
    }else {
        inputMese.classList.remove('is-invalid');
    }
    if (!result.giorno) {
        inputGiorno.classList.add('is-invalid');
    }else {
        inputGiorno.classList.remove('is-invalid');
    }
}

const renderList = () => {
    // PRESENTER
    const htmlRows = list.map((data, index) => {
        const template = `<tr>
        <td>%NOME</td><td>%COGNOME</td><td>%ANNO</td><td>%MESE</td><td>%GIORNO</td>
        <td>
            <button id='DEL%ID' class='btn btn-danger action-delete'>Elimina</button>
            <button id='EDIT%ID' class='btn btn-warning action-edit'>Modifica</button>
        </td>
        </tr>`;
        const htmlRow = template
            .replace('%NOME', data.nome)
            .replace('%COGNOME', data.cognome)
            .replace('%ANNO', data.anno)
            .replace('%MESE', data.mese)
            .replace('%GIORNO', data.giorno)
            .replaceAll('%ID', index);
        return htmlRow;
    }); 
    tbody.innerHTML = htmlRows.join("\n");
    //INTERACTOR
    interactorList();
    
}

// #endregion

Nelle prossime applicazioni, con l’utilizzo delle closures, andremo a definire in modo più efficace i componenti applicativi, non riducendoli a semplici funzioni ma a veri e propri oggetti applicativi dotati di proprietà e funzioni proprie, entrando nel vivo della progettazione applicativa per componenti.