Formulare ohne Neuladen abschicken – FormData und fetch
Ein Formular per JavaScript abzuschicken heißt nicht, jedes Feld einzeln auszulesen. FormData nimmt das ganze Formular, fetch schickt es weg – inklusive Dateien, und mit einer Fehlermeldung, die der Besucher versteht.
Handgeschriebenes Auslesen von Formularfeldern ist Fleißarbeit mit eingebauter Fehlerquelle: Ein neues Feld im HTML, und im JavaScript fehlt es. FormData liest das Formular so, wie der Browser es auch beim normalen Absenden täte – über die name-Attribute.
Das Grundgerüst
<form id="kontakt" method="post" action="/api/contact.php">
<input name="name" required>
<input name="email" type="email" required>
<textarea name="text" required></textarea>
<button>Absenden</button>
</form>const form = document.getElementById('kontakt');
form.addEventListener('submit', async e => {
e.preventDefault();
if (!form.reportValidity()) return; // nutzt die eingebaute HTML-Prüfung
const knopf = form.querySelector('button');
knopf.disabled = true;
try {
const antwort = await fetch(form.action, {
method: form.method,
body: new FormData(form), // Dateien inklusive
});
if (!antwort.ok) throw new Error('HTTP ' + antwort.status);
form.reset();
melde('Danke, die Nachricht ist da.');
} catch (fehler) {
melde('Das hat gerade nicht geklappt. Bitte später erneut versuchen.');
} finally {
knopf.disabled = false;
}
});Zwei Dinge, die hier wichtig sind: action und method kommen aus dem HTML – das Formular funktioniert damit auch ohne JavaScript. Und knopf.disabled verhindert den Doppelklick, der sonst zwei Nachrichten erzeugt.
JSON statt multipart
Erwartet dein Backend JSON, wandelst du die FormData um:
const daten = Object.fromEntries(new FormData(form));
await fetch('/api/contact.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(daten),
});Achtung bei mehrfach vergebenen Namen (Checkbox-Gruppen, name="tags[]"): Object.fromEntries behält nur den letzten Wert. Dafür gibt es getAll:
const fd = new FormData(form);
const daten = { ...Object.fromEntries(fd), tags: fd.getAll('tags') };Setz den Content-Type nicht selbst
// FALSCH – zerstört die Grenzmarkierung zwischen den Feldern:
fetch(url, { method: 'POST', headers: { 'Content-Type': 'multipart/form-data' }, body: fd });Bei einem FormData-Body setzt der Browser den Header selbst, samt boundary. Schreibst du ihn von Hand, fehlt die Grenzmarkierung und der Server liest ein leeres Formular.
Fallstrick
FormData sieht nur Felder mit einem name-Attribut, und nicht angehakte Checkboxen tauchen gar nicht auf (kein false, sondern nichts). Deaktivierte Felder (disabled) fehlen ebenfalls – wenn ein Wert mitsoll, nimm readonly statt disabled. Und ganz wichtig: Die Prüfung im Browser ist Bequemlichkeit, keine Sicherheit. Der Server muss jede Eingabe noch einmal selbst prüfen.
Kontaktformulare, die ankommen, und Backends, die sauber prüfen, baue ich regelmäßig – schreib mir über bymw.de.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
Promise.allSettled – wenn ein einziger Fehlschlag nicht alles kippen darf
Promise.all bricht beim ersten Fehler ab und wirft alle anderen Ergebnisse weg. Für ein Dashboard aus fünf Quellen ist das genau falsch: Vier Kacheln könnten Daten zeigen, aber die Seite bleibt leer.
Web Worker – rechnen, ohne die Oberfläche einzufrieren
JavaScript hat im Browser genau einen Faden für alles: Klicks, Animationen, Layout – und deine Rechenschleife. Dauert die zwei Sekunden, ist die Seite zwei Sekunden lang tot. Ein Worker nimmt die Arbeit auf einen eigenen Faden.
CustomEvent – Bausteine, die sich gegenseitig Bescheid sagen, ohne sich zu kennen
Sobald zwei Teile einer Seite aufeinander reagieren sollen, landet man schnell bei globalen Variablen oder Funktionsaufrufen quer durchs Projekt. Eigene Events entkoppeln das – mit denselben Mitteln, die der Browser selbst benutzt.