Salta al contenuto
Note per Studenti Esercizio - Form HTML di prenotazione con validazione nativa e in JavaScript (applicazione web)

Esercizio - Form HTML di prenotazione con validazione nativa e in JavaScript (applicazione web)

In questa pagina 4

Testo. Nell'applicazione di prenotazione delle aule (tabelle aula(id, nome, posti) e prenotazione(id, aula_id, giorno, ora_inizio, ora_fine, studente)) serve una pagina con il modulo per prenotare un'aula. Richieste:

  • campi: aula (scelta fra le tre aule), giorno, ora di inizio, ora di fine, cognome dello studente; i nomi dei campi sono quelli delle colonne;
  • validazione nativa HTML: tutti i campi obbligatori; il giorno non nel passato; le ore fra le 08:00 e le 20:00 a passi di mezz'ora; il cognome di 2-40 caratteri, solo lettere, apostrofo e spazio;
  • validazione in JavaScript per i controlli che HTML non sa fare: l'ora di fine deve seguire quella di inizio e l'aula non deve essere già prenotata nello stesso intervallo;
  • all'invio valido, la pagina non ricarica ma costruisce l'oggetto JSON da mandare al server.

Teoria: HTML5 - struttura del documento, elementi semantici e formUn documento HTML5 ha la forma doctype (), html con lang, head (meta charset UTF-8, viewport, title, link, script) e body. Il parser HTML5 non dà mai errore: corregge (aggiunge il tbody, chiude i p). Gli elementi semantici (header, nav, main, section, article, aside, footer, figure, time) descrivono il ruolo del contenuto e danno ai lettori di schermo i punti di riferimento; div e span sono neutri. Link: a href (assoluto, relativo, #frammento, mailto), rel e target; immagini con alt obbligatorio. Tabelle: table, caption, thead, tbody, th con scope, td, colspan e rowspan, solo per dati. Form: action, method (GET mette i campi nella query string, per letture idempotenti e ricercabili; POST li mette nel corpo, per modificare lo stato), enctype (application/x-www-form-urlencoded predefinito, multipart/form-data per i file); si invia solo ciò che ha name; label for collega etichetta e campo; tipi di input (text, email, number, date, time, radio, checkbox, hidden...); validazione nativa con required, minlength, maxlength, min, max, step, pattern e type, aggirabile e quindi da ripetere sul server; novalidate la spegne. Accessibilità: lang, alt, label, gerarchia dei titoli, navigazione da tastiera, ARIA solo quando l'HTML nativo non basta.HTML5 - struttura del documento, elementi semantici e form → (form, tipi di input, validazione nativa, GET e POST), JavaScript - linguaggio, DOM ed eventiJavaScript (standard ECMAScript, Ecma-262) è il linguaggio dei browser: tipizzato dinamicamente, con sette tipi primitivi (number, string, boolean, undefined, null, bigint, symbol) e gli oggetti (anche array e funzioni); typeof null vale "object" per un errore storico; == converte i tipi, === no (si usa sempre ===); valori falsy: false, 0, 0n, "", null, undefined, NaN. let e const hanno visibilità di blocco e una zona morta prima della dichiarazione, var visibilità di funzione e hoisting; const impedisce di riassegnare il nome, non di modificare l'oggetto. Funzioni: dichiarazione (hoisted), espressione, arrow function (this lessicale), parametri predefiniti, rest e spread, destructuring. Array: map, filter, reduce, find, some, every, sort (senza comparatore ordina come stringhe). Closure: una funzione ricorda le variabili dell'ambiente in cui è nata. Eventi: addEventListener; propagazione in tre fasi (cattura, target, bolla), stopPropagation, preventDefault, delegazione con event.target e closest. DOM: albero di nodi; querySelector, createElement, append, replaceChildren, textContent (sicuro) contro innerHTML (rischio di XSS), classList, dataset. Moduli ES (type="module", import e export) sono differiti e in modalità strict. Event loop: un solo thread, call stack, coda dei task (setTimeout, eventi) e coda dei microtask (promise, await) svuotata dopo ogni task: l'esecuzione asincrona (fetch, async e await, Promise.all) è alla base di AJAX e il codice lungo blocca la pagina.JavaScript - linguaggio, DOM ed eventi → (eventi, preventDefault, array). Si ricorda che i controlli nel browser sono solo una comodità: il server li deve rifare (Organizzazione fisica dei dati e accesso programmatico alle basi di datiUn DBMS è formato da un gestore delle connessioni, un parser, un ottimizzatore che sceglie il piano di esecuzione usando le statistiche del catalogo, un esecutore, i metodi di accesso (heap, ordinato, hash, B-tree, indici), il buffer manager e il gestore delle transazioni (lock e log). Il costo si misura in accessi a blocchi del disco. L'ottimizzatore riscrive l'espressione algebrica (selezioni e proiezioni il prima possibile, prodotto con selezione = join, ordine dei join) e sceglie gli algoritmi: scansione o indice per le selezioni, nested loop, block nested loop, index nested loop, sort-merge o hash join per i join (con 1000 e 100 blocchi, 10 pagine di buffer: 11 mila accessi con block nested loop, 3300 con hash join). La progettazione fisica sceglie organizzazione dei file, indici ed eventuale denormalizzazione in base al carico. Le applicazioni accedono ai dati con SQL incorporato, interfacce a livello di chiamata (ODBC, JDBC, DB-API di Python), ORM o routine memorizzate: connessione, cursore, execute con parametri, fetch, commit o rollback. Mai concatenare l'input dell'utente nell'SQL (SQL injection): query parametrizzate. Un'applicazione web è a tre livelli: browser, server applicativo (HTTP senza stato, sessioni e cookie) e DBMS.Organizzazione fisica dei dati e accesso programmatico alle basi di dati →). Il codice è stato provato in un browser (Edge) e la logica pura anche con node.

Parte 1 - Il form HTML con la validazione nativa

html
<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Prenota un'aula</title>
</head>
<body>
  <h1>Prenota un'aula</h1>

  <form id="prenota" action="/prenotazioni" method="post">
    <p>
      <label for="aula_id">Aula</label>
      <select id="aula_id" name="aula_id" required>
        <option value="">Scegli...</option>
        <option value="1">Aula A1 (80 posti)</option>
        <option value="2">Aula B2 (45 posti)</option>
        <option value="3">Lab Info (30 posti)</option>
      </select>
    </p>
    <p>
      <label for="giorno">Giorno</label>
      <input type="date" id="giorno" name="giorno" required>
    </p>
    <p>
      <label for="ora_inizio">Dalle</label>
      <input type="time" id="ora_inizio" name="ora_inizio" required
             min="08:00" max="19:00" step="1800">
      <label for="ora_fine">alle</label>
      <input type="time" id="ora_fine" name="ora_fine" required
             min="09:00" max="20:00" step="1800">
    </p>
    <p>
      <label for="studente">Cognome</label>
      <input type="text" id="studente" name="studente" required
             minlength="2" maxlength="40"
             pattern="[A-Za-zÀ-ÿ' ]+"
             title="Solo lettere, apostrofo e spazio">
    </p>
    <p><button type="submit">Prenota</button></p>
    <p id="esito" role="status"></p>
  </form>

  <script src="prenota.js"></script>
</body>
</html>

Spiegazione:

  • method="post": la prenotazione crea una riga, non è una lettura idempotente. action="/prenotazioni" è l'indirizzo che la servlet PrenotazioniServlet esporrà.
  • Ogni campo ha label for uguale all'id, e il name uguale alla colonna: senza name il campo non verrebbe inviato.
  • La select ha una prima option con value="" ("Scegli..."): con required il browser considera "vuoto" proprio quel valore, e costringe a scegliere un'aula vera. Le altre option hanno come value l'id dell'aula, che è ciò che viaggia.
  • type="date" e type="time" danno i selettori nativi e inviano AAAA-MM-GG e HH:MM, gli stessi formati delle colonne SQL. min, max e step="1800" (secondi: 30 minuti) limitano le ore: ora_inizio da 08:00 a 19:00, ora_fine da 09:00 a 20:00.
  • minlength="2" maxlength="40" per la lunghezza; pattern="[A-Za-zÀ-ÿ' ]+" deve coincidere con tutto il valore: lettere (anche accentate), apostrofo, spazio. title è il testo che il browser aggiunge al messaggio d'errore.
  • <p id="esito" role="status"> è la zona del messaggio di esito: role="status" fa leggere ai lettori di schermo il testo quando cambia.
  • Il giorno minimo non si può scrivere nell'HTML (cambia ogni giorno): lo imposta JavaScript (parte 3).

Che cosa controlla il browser. Verificato impostando valori diversi e leggendo validity:

valore campo stato
campo vuoto aula, giorno, cognome valueMissing
07:00 ora_inizio rangeUnderflow
09:10 ora_inizio stepMismatch (non è un multiplo di 30 minuti dall'inizio dell'intervallo)
09:30 ora_inizio valido
20:30 ora_fine rangeOverflow
R2d2 cognome patternMismatch
Dall'Olio Anna cognome valido
2020-01-01 giorno (con min impostato) rangeUnderflow

Il browser blocca l'invio e mostra un messaggio sul primo campo non valido. Due dettagli: minlength (e maxlength) sono controllati solo dopo una modifica fatta dall'utente, non su un valore impostato da codice (provato: "R" impostato con JavaScript risulta valido); inoltre la validazione nativa non confronta due campi fra loro.

Parte 2 - Che cosa viaggia

Compilando aula B2, giorno 2026-10-22, dalle 09:30 alle 10:30, cognome D'Angelo e premendo "Prenota" (senza JavaScript), il browser invia:

POST /prenotazioni HTTP/1.1
Content-Type: application/x-www-form-urlencoded

aula_id=2&giorno=2026-10-22&ora_inizio=09%3A30&ora_fine=10%3A30&studente=D%27Angelo

Parte 3 - Validazione in JavaScript

HTML non sa dire "la fine è dopo l'inizio" né "l'aula è libera": lo fa JavaScript, con l'API di validazione dei vincoli, in particolare setCustomValidity(messaggio): un messaggio non vuoto rende il campo non valido (customError) e il browser lo mostra come i suoi; con "" il campo torna valido.

javascript
// Prenotazioni già presenti (in produzione arrivano dal server).
const esistenti = [
  { id: 1, aula_id: 1, giorno: "2026-10-19", ora_inizio: "09:00", ora_fine: "11:00", studente: "Rossi" },
  { id: 2, aula_id: 1, giorno: "2026-10-19", ora_inizio: "14:00", ora_fine: "16:00", studente: "Bianchi" },
  { id: 3, aula_id: 2, giorno: "2026-10-20", ora_inizio: "10:00", ora_fine: "12:00", studente: "Verdi" },
  { id: 4, aula_id: 3, giorno: "2026-10-21", ora_inizio: "09:00", ora_fine: "13:00", studente: "Rossi" }
];

// Due intervalli [i1,f1) e [i2,f2) si sovrappongono se ciascuno inizia prima che l'altro finisca.
// Le ore "HH:MM" si confrontano come stringhe: l'ordine alfabetico coincide con quello temporale.
function siSovrappone(a, b) {
  return a.ora_inizio < b.ora_fine && b.ora_inizio < a.ora_fine;
}

// Restituisce il messaggio d'errore, oppure "" se la richiesta è accettabile.
function controlla(nuova, esistenti) {
  if (nuova.ora_fine <= nuova.ora_inizio) {
    return "L'ora di fine deve essere dopo l'ora di inizio.";
  }
  const conflitto = esistenti.find(p =>
    p.aula_id === nuova.aula_id && p.giorno === nuova.giorno && siSovrappone(p, nuova));
  if (conflitto) {
    return "Aula già prenotata dalle " + conflitto.ora_inizio + " alle " + conflitto.ora_fine + ".";
  }
  return "";
}

const form = document.getElementById("prenota");
const campi = form.elements;                       // accesso per name
const esito = document.getElementById("esito");

// Giorno minimo = oggi (data locale in formato AAAA-MM-GG).
const oggi = new Date();
campi.giorno.min = oggi.getFullYear() + "-" +
  String(oggi.getMonth() + 1).padStart(2, "0") + "-" +
  String(oggi.getDate()).padStart(2, "0");

function leggi() {
  return {
    aula_id: Number(campi.aula_id.value),
    giorno: campi.giorno.value,
    ora_inizio: campi.ora_inizio.value,
    ora_fine: campi.ora_fine.value,
    studente: campi.studente.value.trim()
  };
}

function validaIntervallo() {
  campi.ora_fine.setCustomValidity("");            // azzera: altrimenti il campo resta invalido
  if (campi.ora_inizio.value && campi.ora_fine.value) {
    campi.ora_fine.setCustomValidity(controlla(leggi(), esistenti));
  }
}
for (const nome of ["aula_id", "giorno", "ora_inizio", "ora_fine"]) {
  campi[nome].addEventListener("input", validaIntervallo);
}

form.addEventListener("submit", e => {
  e.preventDefault();                              // niente invio classico: si userà fetch
  validaIntervallo();
  if (!form.reportValidity()) return;              // mostra i messaggi del browser e si ferma
  esito.textContent = "Invio: " + JSON.stringify(leggi());
});

Spiegazione:

Prove di controlla con le prenotazioni esistenti (node):

aula giorno dalle alle risultato
1 2026-10-19 08:00 09:00 accettata (finisce quando comincia la prenotazione 1)
1 2026-10-19 10:00 12:00 "Aula già prenotata dalle 09:00 alle 11:00."
2 2026-10-19 10:00 12:00 accettata (altra aula)
1 2026-10-19 11:00 12:00 accettata (comincia quando finisce la prenotazione 1)
1 2026-10-19 09:30 09:30 "L'ora di fine deve essere dopo l'ora di inizio."
1 2026-10-19 15:00 17:00 "Aula già prenotata dalle 14:00 alle 16:00."
1 2026-10-19 12:00 14:00 accettata (tra le due)
1 2026-10-19 08:00 18:00 "Aula già prenotata dalle 09:00 alle 11:00." (contiene la prenotazione 1)

Prova nel browser. Con il form compilato aula 1, 2027-12-01, 09:30-10:30, D'Angelo, requestSubmit() scrive nella zona dell'esito Invio: {"aula_id":1,"giorno":"2027-12-01","ora_inizio":"09:30","ora_fine":"10:30","studente":"D'Angelo"}. Con aula 1, 2026-10-19, 10:00-12:00 il campo ora_fine risulta in errore con il messaggio sull'aula già prenotata, e con fine prima dell'inizio, quello sull'ora di fine.

Parte 4 - E il server?

La pagina dà un'esperienza comoda, ma non protegge i dati: chiunque può mandare un POST con curl, senza form e senza JavaScript. Il server deve quindi rifare tutti i controlli:

In caso di errore risponde con uno stato 4xx e un messaggio (per esempio 400 per un dato non valido, 409 per un conflitto con un'altra prenotazione), che la pagina mostra nella zona esito.

Errori tipici

  • Campo senza name: non viene inviato.
  • select con required ma senza option value="" iniziale: parte già con un'aula scelta.
  • label senza for o non collegata all'id.
  • Confrontare le ore "9:00" e "14:00" come stringhe: l'ordine alfabetico sbaglia; usare sempre due cifre.
  • Non azzerare setCustomValidity(""): il campo resta invalido anche dopo la correzione.
  • Dimenticare preventDefault() nel gestore di submit.
  • Confrontare aula_id (numero) con il valore del campo (stringa) senza Number.
  • Dichiarare "tutto validato" senza rifare i controlli sul server.

Versione ripasso

Form. <form id="prenota" action="/prenotazioni" method="post"> con campi:

  • aula_id (select con prima option value="", required);
  • giorno (type="date", required);
  • ora_inizio (type="time", min="08:00" max="19:00" step="1800");
  • ora_fine (min="09:00" max="20:00" step="1800");
  • studente (required minlength="2" maxlength="40" pattern="[A-Za-zÀ-ÿ' ]+").

label for = id; name = colonna (senza name non si invia) (HTML5 - struttura del documento, elementi semantici e formUn documento HTML5 ha la forma doctype (), html con lang, head (meta charset UTF-8, viewport, title, link, script) e body. Il parser HTML5 non dà mai errore: corregge (aggiunge il tbody, chiude i p). Gli elementi semantici (header, nav, main, section, article, aside, footer, figure, time) descrivono il ruolo del contenuto e danno ai lettori di schermo i punti di riferimento; div e span sono neutri. Link: a href (assoluto, relativo, #frammento, mailto), rel e target; immagini con alt obbligatorio. Tabelle: table, caption, thead, tbody, th con scope, td, colspan e rowspan, solo per dati. Form: action, method (GET mette i campi nella query string, per letture idempotenti e ricercabili; POST li mette nel corpo, per modificare lo stato), enctype (application/x-www-form-urlencoded predefinito, multipart/form-data per i file); si invia solo ciò che ha name; label for collega etichetta e campo; tipi di input (text, email, number, date, time, radio, checkbox, hidden...); validazione nativa con required, minlength, maxlength, min, max, step, pattern e type, aggirabile e quindi da ripetere sul server; novalidate la spegne. Accessibilità: lang, alt, label, gerarchia dei titoli, navigazione da tastiera, ARIA solo quando l'HTML nativo non basta.HTML5 - struttura del documento, elementi semantici e form →). role="status" per l'esito.

Stati nativi: vuoto valueMissing; 07:00 rangeUnderflow; 09:10 stepMismatch; 20:30 rangeOverflow; R2d2 patternMismatch. minlength conta solo dopo la modifica dell'utente; nessun controllo fra due campi.

Cosa viaggia. POST con application/x-www-form-urlencoded: aula_id=2&giorno=2026-10-22&ora_inizio=09%3A30&ora_fine=10%3A30&studente=D%27Angelo (: = %3A, ' = %27, spazio = +). GET metterebbe i campi nell'URI: no per modificare dati.

JavaScript (JavaScript - linguaggio, DOM ed eventiJavaScript (standard ECMAScript, Ecma-262) è il linguaggio dei browser: tipizzato dinamicamente, con sette tipi primitivi (number, string, boolean, undefined, null, bigint, symbol) e gli oggetti (anche array e funzioni); typeof null vale "object" per un errore storico; == converte i tipi, === no (si usa sempre ===); valori falsy: false, 0, 0n, "", null, undefined, NaN. let e const hanno visibilità di blocco e una zona morta prima della dichiarazione, var visibilità di funzione e hoisting; const impedisce di riassegnare il nome, non di modificare l'oggetto. Funzioni: dichiarazione (hoisted), espressione, arrow function (this lessicale), parametri predefiniti, rest e spread, destructuring. Array: map, filter, reduce, find, some, every, sort (senza comparatore ordina come stringhe). Closure: una funzione ricorda le variabili dell'ambiente in cui è nata. Eventi: addEventListener; propagazione in tre fasi (cattura, target, bolla), stopPropagation, preventDefault, delegazione con event.target e closest. DOM: albero di nodi; querySelector, createElement, append, replaceChildren, textContent (sicuro) contro innerHTML (rischio di XSS), classList, dataset. Moduli ES (type="module", import e export) sono differiti e in modalità strict. Event loop: un solo thread, call stack, coda dei task (setTimeout, eventi) e coda dei microtask (promise, await) svuotata dopo ogni task: l'esecuzione asincrona (fetch, async e await, Promise.all) è alla base di AJAX e il codice lungo blocca la pagina.JavaScript - linguaggio, DOM ed eventi →):

  • siSovrappone(a, b) = a.ora_inizio < b.ora_fine && b.ora_inizio < a.ora_fine; intervalli adiacenti (fine = inizio) non si sovrappongono.
  • Le ore "HH:MM" si confrontano come stringhe (due cifre sempre).
  • controlla(nuova, esistenti) è pura: fine dopo inizio, poi nessuna sovrapposizione nella stessa aula e nello stesso giorno; restituisce il messaggio o "".
  • validaIntervallo: setCustomValidity("") poi il messaggio su ora_fine; richiamata a ogni input.
  • submit: preventDefault(), validaIntervallo(), form.reportValidity(); se valido JSON.stringify(leggi()) con aula_id convertito con Number.
  • Giorno minimo = oggi, impostato da JavaScript (getMonth() + 1, padStart).

Server. I controlli del browser sono solo comodità: il server rifà tutto (tipi, ora_fine > ora_inizio, sovrapposizioni al momento dell'inserimento, query parametrizzate) e risponde 400 o 409 (Organizzazione fisica dei dati e accesso programmatico alle basi di datiUn DBMS è formato da un gestore delle connessioni, un parser, un ottimizzatore che sceglie il piano di esecuzione usando le statistiche del catalogo, un esecutore, i metodi di accesso (heap, ordinato, hash, B-tree, indici), il buffer manager e il gestore delle transazioni (lock e log). Il costo si misura in accessi a blocchi del disco. L'ottimizzatore riscrive l'espressione algebrica (selezioni e proiezioni il prima possibile, prodotto con selezione = join, ordine dei join) e sceglie gli algoritmi: scansione o indice per le selezioni, nested loop, block nested loop, index nested loop, sort-merge o hash join per i join (con 1000 e 100 blocchi, 10 pagine di buffer: 11 mila accessi con block nested loop, 3300 con hash join). La progettazione fisica sceglie organizzazione dei file, indici ed eventuale denormalizzazione in base al carico. Le applicazioni accedono ai dati con SQL incorporato, interfacce a livello di chiamata (ODBC, JDBC, DB-API di Python), ORM o routine memorizzate: connessione, cursore, execute con parametri, fetch, commit o rollback. Mai concatenare l'input dell'utente nell'SQL (SQL injection): query parametrizzate. Un'applicazione web è a tre livelli: browser, server applicativo (HTTP senza stato, sessioni e cookie) e DBMS.Organizzazione fisica dei dati e accesso programmatico alle basi di dati →).

Errori tipici

  • Campo senza name; select senza option value="".
  • Non azzerare setCustomValidity.
  • Dimenticare preventDefault().
  • aula_id stringa contro numero.
  • Fidarsi solo della validazione nel browser. Controlli di controlla(nuova, esistenti) sulle prenotazioni esistenti dell'aula 1 (09-11 e 14-16 del 19 ottobre):
dalle alle esito
08:00 09:00 accettata (adiacente)
10:00 12:00 conflitto con 09:00-11:00
11:00 12:00 accettata (adiacente)
09:30 09:30 fine non dopo l'inizio
12:00 14:00 accettata
08:00 18:00 conflitto (contiene la prima)

L'aula 2 allo stesso orario è libera: il conflitto riguarda stessa aula e stesso giorno.

Messaggi nativi e personalizzati.

  • required dà valueMissing; min e max danno rangeUnderflow e rangeOverflow; step dà stepMismatch; pattern dà patternMismatch; setCustomValidity("...") dà customError.
  • form.reportValidity() esegue tutti i controlli, mostra il primo errore e restituisce false.
  • setCustomValidity("") va richiamata per togliere l'errore precedente.

Esito. Dopo preventDefault() e controlli superati la pagina mostra Invio: {"aula_id":1,"giorno":"2027-12-01","ora_inizio":"09:30","ora_fine":"10:30","studente":"D'Angelo"}; con fetch sarebbe il corpo di un POST application/json.

Server. Risponde 400 per dati non validi e 409 per conflitto; la sovrapposizione va controllata al momento dell'inserimento, perché un altro utente potrebbe aver prenotato nel frattempo.

Teoria collegata