Salta al contenuto
Note per Studenti Esercizio - Pagina AJAX per creare e cancellare prenotazioni (applicazione web)

Esercizio - Pagina AJAX per creare e cancellare prenotazioni (applicazione web)

Esercizio originale, nello stile della prova di programmazione (JavaScript che usa l'API REST). Teoria: AJAX e fetch - applicazioni Web 2.0 e MVC rivistoAJAX (Asynchronous JavaScript and XML, oggi quasi sempre con JSON) permette a JavaScript di chiedere dati al server in background e aggiornare solo una parte del DOM, senza ricaricare la pagina. L'oggetto storico è XMLHttpRequest (open, send, eventi onload e onerror); l'API moderna è fetch, che restituisce una Promise di Response (ok, status, json()). Una Promise rappresenta un risultato futuro (in attesa, mantenuta, rifiutata) e si concatena con then, catch e finally; async/await è la sintassi che la fa leggere come codice sequenziale, e dopo ogni await il resto della funzione riparte quando la Promise si risolve (il codice sincrono finisce prima, poi vengono le promise, poi i timer). Attenzione: fetch rifiuta la Promise solo per errori di rete; un 404 o un 500 sono risposte e si controllano con response.ok. Il client delle aule ha api.js (funzioni che chiamano l'API REST e lanciano ErroreApi se il codice non è 2xx) e pagina.js (legge il form, chiama api.js, aggiorna la tabella con textContent, un solo gestore di click per i bottoni di cancellazione). Il paradigma MVC viene rivisto: il server resta con model e controller REST ma restituisce solo dati, la view è nel browser (DOM costruito da JavaScript) e il controller della pagina sono i gestori di eventi; lo stato dell'interfaccia sta nel client e il server è senza stato. La same-origin policy (origine = schema, host, porta) impedisce a uno script di leggere risposte di altre origini; CORS permette al server di autorizzarle con Access-Control-Allow-Origin, e le richieste non semplici (JSON, DELETE) sono precedute da una richiesta OPTIONS di preflight.AJAX e fetch - applicazioni Web 2.0 e MVC rivisto →, 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 →, 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 →. L'API è quella di Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →. Il codice è stato provato con Node.js: api.js e giorno.js contro un server simulato che rispetta il contratto dell'API (stessi metodi, URI, codici), pagina.js con un DOM minimo simulato; i risultati riportati sono le uscite reali di quelle prove. Non è stato aperto in un browser.

Testo

L'API restituisce JSON con i nomi delle colonne (id, aula_id, giorno, ora_inizio, ora_fine, studente; per le aule id, nome, posti). Si scrive la pagina prenota.html:

  1. un form con aula (menu a tendina riempito dall'API), giorno, ora d'inizio, ora di fine e studente, e un pulsante "Prenota";
  2. sotto, una tabella delle prenotazioni dell'aula e del giorno scelti, aggiornata al cambio di aula o giorno;
  3. la prenotazione si crea con POST /api/prenotazioni, la tabella si aggiorna senza ricaricare la pagina; se l'orario è occupato (409) si mostra "Orario già occupato.";
  4. ogni riga ha un pulsante "Cancella" (DELETE /api/prenotazioni/{id});
  5. il pulsante "Prenota" è disabilitato mentre la richiesta è in corso;
  6. una funzione prenotazioniDelGiorno(giorno) che restituisce per ogni aula le prenotazioni di quel giorno, facendo le richieste per le aule in parallelo.

Domande: (a) separare il codice in api.js e pagina.js e spiegare perché; (b) scrivere la traccia delle richieste HTTP e delle modifiche alla pagina per la sequenza "apro la pagina, prenoto A1 dalle 11:00 alle 12:30 come Neri, riprovo 10:00-12:00, cancello la prima, cancello di nuovo la stessa"; (c) che cosa succede con un fetch verso un server spento, e con un 404? (d) perché si usa textContent?

Soluzione

(a) Due file con due compiti

File Ruolo Conosce
api.js accesso ai dati remoti (il "model" del client) URL e formato dell'API, fetch, ErroreApi; non la pagina
pagina.js lettura del form e aggiornamento del DOM (view e controller della pagina) gli id degli elementi e api.js; non gli URL

Se l'API cambia (un URL, un campo) si modifica un file; se cambia la grafica, l'altro. api.js si prova da solo con un fetch simulato (è stato fatto), senza browser.

javascript
// api.js - il "model" del client: parla con l'API REST, non conosce la pagina HTML.
const BASE = "/api";                      // l'API sta sotto /api, alla radice del sito

export class ErroreApi extends Error {
  constructor(stato, messaggio) {
    super(messaggio);
    this.stato = stato;                   // codice di stato HTTP della risposta
  }
}

async function richiesta(metodo, percorso, corpo) {
  const opzioni = { method: metodo, headers: { Accept: "application/json" } };
  if (corpo !== undefined) {
    opzioni.headers["Content-Type"] = "application/json";
    opzioni.body = JSON.stringify(corpo);
  }
  const risposta = await fetch(`${BASE}${percorso}`, opzioni);   // rifiuta solo se la rete fallisce
  if (risposta.status === 204) {
    return null;                          // successo senza corpo (DELETE)
  }
  const dati = await risposta.json().catch(() => null);          // il corpo potrebbe non essere JSON
  if (!risposta.ok) {                     // 4xx e 5xx NON fanno rifiutare fetch: si controllano a mano
    throw new ErroreApi(risposta.status, dati?.errore ?? risposta.statusText);
  }
  return dati;
}

export const elencaAule = () => richiesta("GET", "/aule");

export function elencaPrenotazioni(aulaId, giorno) {
  const parametri = new URLSearchParams({ giorno });
  if (aulaId) {
    parametri.set("aula", aulaId);
  }
  return richiesta("GET", `/prenotazioni?${parametri}`);
}

export const creaPrenotazione = (prenotazione) => richiesta("POST", "/prenotazioni", prenotazione);

export const cancellaPrenotazione = (id) => richiesta("DELETE", `/prenotazioni/${id}`);

La funzione richiesta è l'unico punto che chiama fetch: aggiunge Content-Type e corpo solo se c'è un corpo, restituisce null per 204, e per ogni risposta non ok lancia ErroreApi(stato, messaggio), con il messaggio preso da {"errore": ...}. Così il codice chiamante usa try/catch e non ripete if (!risposta.ok).

Pagina e script

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>
  <style>
    .ok { color: #166534; }
    .errore { color: #b91c1c; }
    td, th { padding: .25rem .75rem; text-align: left; }
  </style>
</head>
<body>
  <h1>Prenotazione aule</h1>

  <form id="modulo">
    <label>Aula <select id="aula_id"></select></label>
    <label>Giorno <input type="date" id="giorno" required></label>
    <label>Dalle <input type="time" id="ora_inizio" required></label>
    <label>Alle <input type="time" id="ora_fine" required></label>
    <label>Studente <input type="text" id="studente" maxlength="40" required></label>
    <button>Prenota</button>
  </form>

  <p id="messaggio" role="status"></p>

  <table>
    <thead><tr><th>Dalle</th><th>Alle</th><th>Studente</th><th></th></tr></thead>
    <tbody id="righe"></tbody>
  </table>

  <script type="module" src="js/pagina.js"></script>
</body>
</html>
javascript
// pagina.js - la "view" e il "controller" del client: legge il form, chiama api.js, aggiorna la tabella.
import { ErroreApi, elencaAule, elencaPrenotazioni, creaPrenotazione, cancellaPrenotazione } from "./api.js";

const $ = (id) => document.getElementById(id);

function messaggio(testo, errore = false) {
  const p = $("messaggio");
  p.textContent = testo;                  // textContent: il testo non viene mai interpretato come HTML
  p.className = errore ? "errore" : "ok";
}

function riga(prenotazione) {
  const tr = document.createElement("tr");
  for (const testo of [prenotazione.ora_inizio, prenotazione.ora_fine, prenotazione.studente]) {
    const td = document.createElement("td");
    td.textContent = testo;
    tr.append(td);
  }
  const bottone = document.createElement("button");
  bottone.textContent = "Cancella";
  bottone.dataset.id = prenotazione.id;
  const tdAzione = document.createElement("td");
  tdAzione.append(bottone);
  tr.append(tdAzione);
  return tr;
}

async function aggiorna() {
  try {
    const lista = await elencaPrenotazioni($("aula_id").value, $("giorno").value);
    $("righe").replaceChildren(...lista.map(riga));
  } catch (e) {
    messaggio(`Impossibile leggere le prenotazioni: ${e.message}`, true);
  }
}

$("modulo").addEventListener("submit", async (evento) => {
  evento.preventDefault();                // niente invio classico del form: la pagina non si ricarica
  const prenotazione = {
    aula_id: Number($("aula_id").value),
    giorno: $("giorno").value,
    ora_inizio: $("ora_inizio").value,
    ora_fine: $("ora_fine").value,
    studente: $("studente").value,
  };
  const bottone = evento.submitter;       // il bottone "Prenota" che ha inviato il form
  bottone.disabled = true;                // un secondo clic durante la richiesta non invia una seconda POST
  try {
    await creaPrenotazione(prenotazione);
    messaggio("Prenotazione registrata.");
    await aggiorna();
  } catch (e) {
    const testo = e instanceof ErroreApi && e.stato === 409 ? "Orario già occupato." : e.message;
    messaggio(testo, true);
  } finally {
    bottone.disabled = false;             // in ogni caso, riuscita o no, il bottone torna attivo
  }
});

$("righe").addEventListener("click", async (evento) => {   // un solo ascoltatore per tutti i bottoni
  const id = evento.target.dataset.id;
  if (!id) {
    return;
  }
  try {
    await cancellaPrenotazione(id);
    messaggio("Prenotazione cancellata.");
    await aggiorna();
  } catch (e) {
    messaggio(e.message, true);
  }
});

$("aula_id").addEventListener("change", aggiorna);
$("giorno").addEventListener("change", aggiorna);

const aule = await elencaAule();          // await al livello superiore: consentito nei moduli
$("aula_id").replaceChildren(...aule.map((a) => new Option(`${a.nome} (${a.posti} posti)`, a.id)));
$("giorno").value = new Date().toISOString().slice(0, 10);
await aggiorna();
  • L'ultimo blocco del file (avvio) usa await al livello superiore, permesso nei moduli (type="module"). Riempie il menu con new Option(testo, valore), imposta il giorno di oggi e chiama aggiorna().
  • Il form ha preventDefault() perché l'invio classico ricaricherebbe la pagina: tutto passa dall'API.
  • Il pulsante "Prenota" si disabilita (evento.submitter) prima della richiesta e si riabilita nel finally, sia che la richiesta riesca, sia che fallisca: provato, la cronologia di disabled è true →\to false anche nel caso del 409. Senza finally un errore lascerebbe il pulsante bloccato.
  • Le righe e i pulsanti "Cancella" sono creati con createElement e textContent; un solo click sul tbody (delegazione) gestisce tutti i pulsanti, leggendo data-id.
  • Dopo ogni modifica si richiama aggiorna(), che richiede l'elenco al server: la tabella mostra sempre lo stato reale, anche se nel frattempo un'altra persona ha prenotato.

Il punto 6: richieste in parallelo

javascript
// giorno.js - la vista "tutte le aule di un giorno": due API, richieste in parallelo con Promise.all.
import { elencaAule, elencaPrenotazioni } from "./api.js";

export async function prenotazioniDelGiorno(giorno) {
  const aule = await elencaAule();                                                 // 1 richiesta
  const liste = await Promise.all(aule.map((a) => elencaPrenotazioni(a.id, giorno)));   // N richieste insieme
  return aule.map((aula, i) => ({ aula, prenotazioni: liste[i] }));
}

// una riga di testo per aula: "Aula A1 (80 posti): 09:00-11:00 Rossi, 14:00-16:00 Bianchi"
export function riepilogo(gruppi) {
  return gruppi.map(({ aula, prenotazioni }) => {
    const elenco = prenotazioni.length === 0
      ? "libera"
      : prenotazioni.map((p) => `${p.ora_inizio}-${p.ora_fine} ${p.studente}`).join(", ");
    return `${aula.nome} (${aula.posti} posti): ${elenco}`;
  });
}
  • elencaAule() serve prima, per sapere quali aule esistono; poi per ogni aula parte una richiesta, tutte insieme: aule.map(...) costruisce un array di Promise (le richieste sono già partite), await Promise.all(...) aspetta che siano tutte finite e restituisce i risultati nello stesso ordine delle aule. Con tre aule, il tempo totale è quello della più lenta e non la somma; un await dentro un ciclo for le farebbe invece una dopo l'altra.
  • Se una richiesta fallisce, Promise.all rifiuta subito e prenotazioniDelGiorno lancia l'errore: chi la chiama gestisce con try/catch. Con Promise.allSettled si avrebbero tutti gli esiti, riusciti o no.
  • Risultato per il 19 ottobre (uscita della prova):
Aula A1 (80 posti): 09:00-11:00 Rossi, 14:00-16:00 Bianchi
Aula B2 (45 posti): libera
Lab Info (30 posti): libera

e le richieste partite (GET unico per le aule, poi tre GET filtrati): GET /api/aule, GET /api/prenotazioni?giorno=2026-10-19&aula=1, ...&aula=2, ...&aula=3.

(b) Traccia della sequenza

Aula A1 e giorno 19 ottobre già scelti (dati dell'esempio). Le richieste sono quelle emesse dal codice nella prova con server e DOM simulati (per l'apertura, quelle del codice di avvio, con il primo elemento del menu già selezionato):

Azione dell'utente Richieste HTTP Effetto sulla pagina
apre la pagina GET /api/aule, poi GET /api/prenotazioni?giorno=2026-10-19&aula=1 il menu ha 3 aule; la tabella ha 2 righe (Rossi, Bianchi)
prenota 11:00-12:30 come Neri POST /api/prenotazioni →\to 201; poi GET /api/prenotazioni?giorno=2026-10-19&aula=1 messaggio "Prenotazione registrata."; tabella a 3 righe
riprova 10:00-12:00 POST /api/prenotazioni →\to 409 messaggio "Orario già occupato." (classe errore); nessun GET; tabella ancora a 3 righe
cancella la prima (id 5) DELETE /api/prenotazioni/5 →\to 204; poi GET dell'elenco messaggio "Prenotazione cancellata."; 2 righe
cancella di nuovo la stessa DELETE /api/prenotazioni/5 →\to 404 messaggio "prenotazione inesistente" (dal corpo {"errore": ...}); tabella invariata

Nota: nel secondo tentativo (10:00-12:00) l'intervallo si sovrappone a Rossi (09:00-11:00) e a Neri (11:00-12:30). Nella tabella le righe le ordina il server, la pagina non riordina.

(c) Errori di rete e 404

  • Server spento o rete assente: fetch stesso rifiuta la Promise (TypeError: Failed to fetch). Dentro richiesta l'eccezione risale senza modifiche fino al catch della pagina, che mostra e.message. Nessun ErroreApi, perché non c'è un codice di stato.
  • 404 (o 500): fetch non rifiuta: la Promise è mantenuta e risposta.ok è false. Se richiesta non controllasse ok, il 404 verrebbe trattato come un successo e si proverebbe a mostrare un oggetto di errore come se fosse una prenotazione. Il controllo di ok e il throw new ErroreApi(...) rendono i due casi uguali per il chiamante: entrambi finiscono in un catch.

(d) textContent e non innerHTML

Il nome dello studente viene scritto da un utente e arriva alla pagina attraverso il server: assegnarlo a innerHTML vorrebbe dire interpretare come HTML anche <img src=x onerror="...">, cioè eseguire codice di un altro utente nella pagina di chi guarda la tabella (XSS). textContent imposta solo testo: i caratteri < e > restano caratteri. Allo stesso modo il messaggio d'errore del server si mostra con textContent. Il controllo lato server non basta a prevenire questo problema: la protezione va messa dove i dati entrano nel DOM.

Versione ripasso

Richiesta. prenota.html: form (aula da /api/aule, giorno, ora d'inizio, ora di fine, studente, "Prenota"), tabella delle prenotazioni di aula e giorno, POST /api/prenotazioni senza ricaricare la pagina (409 →\to "Orario già occupato."), pulsante "Cancella" per riga (DELETE /api/prenotazioni/{id}), "Prenota" disabilitato durante la richiesta, e prenotazioniDelGiorno(giorno) con le richieste delle aule in parallelo.

Due file. api.js = accesso ai dati remoti (URL, formato, fetch, ErroreApi), non conosce la pagina; pagina.js = form e DOM, non conosce gli URL. Se cambia l'API si tocca un file, se cambia la grafica l'altro; api.js si prova da solo con un fetch simulato.

javascript
async function richiesta(metodo, percorso, corpo) {
  const opzioni = { method: metodo, headers: { Accept: "application/json" } };
  if (corpo !== undefined) {
    opzioni.headers["Content-Type"] = "application/json";
    opzioni.body = JSON.stringify(corpo);
  }
  const risposta = await fetch(`${BASE}${percorso}`, opzioni);
  if (risposta.status === 204) return null;
  const dati = await risposta.json().catch(() => null);
  if (!risposta.ok) throw new ErroreApi(risposta.status, dati?.errore ?? risposta.statusText);
  return dati;
}

Pagina. Script type="module" (await al livello superiore): avvio = await elencaAule() riempie il menu con new Option(testo, valore), giorno di oggi, aggiorna(). aggiorna() chiede l'elenco e sostituisce le righe con replaceChildren.

javascript
$("modulo").addEventListener("submit", async (evento) => {
  evento.preventDefault();                  // niente invio classico: la pagina non si ricarica
  const prenotazione = { aula_id: Number($("aula_id").value), giorno: $("giorno").value,
                         ora_inizio: $("ora_inizio").value, ora_fine: $("ora_fine").value, studente: $("studente").value };
  const bottone = evento.submitter;
  bottone.disabled = true;                  // niente doppia POST
  try {
    await creaPrenotazione(prenotazione);
    messaggio("Prenotazione registrata.");
    await aggiorna();
  } catch (e) {
    messaggio(e instanceof ErroreApi && e.stato === 409 ? "Orario già occupato." : e.message, true);
  } finally {
    bottone.disabled = false;               // anche dopo un errore
  }
});
  • Un solo click sul tbody (delegazione degli eventi); evento.target.dataset.id legge data-id. Dopo ogni modifica aggiorna(): la tabella mostra lo stato reale del server. Provato: cronologia di disabled = true →\to false anche con il 409.

Parallelo.

javascript
export async function prenotazioniDelGiorno(giorno) {
  const aule = await elencaAule();
  const liste = await Promise.all(aule.map((a) => elencaPrenotazioni(a.id, giorno)));   // richieste insieme
  return aule.map((aula, i) => ({ aula, prenotazioni: liste[i] }));
}
  • Promise.all aspetta tutte e restituisce i risultati nell'ordine delle aule; il tempo è quello della richiesta più lenta (con await in un for sarebbe la somma). Se una fallisce, rifiuta subito (Promise.allSettled dà tutti gli esiti).
  • Il 19 ottobre: Aula A1 (80 posti): 09:00-11:00 Rossi, 14:00-16:00 Bianchi; Aula B2 (45 posti): libera; Lab Info (30 posti): libera.

Traccia (A1, 19 ottobre).

Azione Richieste Pagina
apre GET /api/aule, GET /api/prenotazioni?giorno=2026-10-19&aula=1 3 aule; 2 righe
prenota 11:00-12:30 (Neri) POST →\to 201, poi GET dell'elenco "Prenotazione registrata."; 3 righe
riprova 10:00-12:00 POST →\to 409 "Orario già occupato."; nessun GET
cancella la 5 DELETE /api/prenotazioni/5 →\to 204, poi GET "Prenotazione cancellata."; 2 righe
cancella di nuovo la 5 DELETE →\to 404 "prenotazione inesistente"; tabella invariata

Errori. Server spento: fetch rifiuta (TypeError: Failed to fetch), non c'è un codice di stato. 404/500: la Promise è mantenuta, ok falso; senza il controllo di ok il 404 passerebbe per un successo. Con ErroreApi i due casi finiscono entrambi in un catch.

textContent e non innerHTML: il nome dello studente viene da un altro utente; innerHTML interpreterebbe <img src=x onerror="..."> ed eseguirebbe codice (XSS), textContent assegna solo testo. Vale anche per i messaggi d'errore del server.

Errori tipici

  • Niente finally: il pulsante resta disabilitato dopo un errore.
  • await dentro un for per richieste indipendenti (sequenziali, lente).
  • fetch senza controllo di ok; preventDefault() dimenticato; un gestore per ogni pulsante dinamico.
  • Costruire righe con innerHTML e dati del server.

Teoria: AJAX e fetch - applicazioni Web 2.0 e MVC rivistoAJAX (Asynchronous JavaScript and XML, oggi quasi sempre con JSON) permette a JavaScript di chiedere dati al server in background e aggiornare solo una parte del DOM, senza ricaricare la pagina. L'oggetto storico è XMLHttpRequest (open, send, eventi onload e onerror); l'API moderna è fetch, che restituisce una Promise di Response (ok, status, json()). Una Promise rappresenta un risultato futuro (in attesa, mantenuta, rifiutata) e si concatena con then, catch e finally; async/await è la sintassi che la fa leggere come codice sequenziale, e dopo ogni await il resto della funzione riparte quando la Promise si risolve (il codice sincrono finisce prima, poi vengono le promise, poi i timer). Attenzione: fetch rifiuta la Promise solo per errori di rete; un 404 o un 500 sono risposte e si controllano con response.ok. Il client delle aule ha api.js (funzioni che chiamano l'API REST e lanciano ErroreApi se il codice non è 2xx) e pagina.js (legge il form, chiama api.js, aggiorna la tabella con textContent, un solo gestore di click per i bottoni di cancellazione). Il paradigma MVC viene rivisto: il server resta con model e controller REST ma restituisce solo dati, la view è nel browser (DOM costruito da JavaScript) e il controller della pagina sono i gestori di eventi; lo stato dell'interfaccia sta nel client e il server è senza stato. La same-origin policy (origine = schema, host, porta) impedisce a uno script di leggere risposte di altre origini; CORS permette al server di autorizzarle con Access-Control-Allow-Origin, e le richieste non semplici (JSON, DELETE) sono precedute da una richiesta OPTIONS di preflight.AJAX e fetch - applicazioni Web 2.0 e MVC rivisto →.

Teoria collegata