Datei-Upload mit Vorschau und Fortschrittsbalken in Alpine.js
AI generated
x-data
Alpine
Alpine.js · Datei-Upload · Drag and Drop · Formulare
Datei-Upload mit Vorschau und Fortschrittsbalken
ohne externe Upload-Bibliothek, mit echtem Fortschritt

Ein Datei-Upload, der nur eine graue Datei-Auswahl-Schaltfläche zeigt, lässt Nutzer bei großen Bildern oder Dokumenten im Ungewissen. Mit FileReader, Drag and Drop und XMLHttpRequest baust du einen Datei-Upload mit sofortiger Bildvorschau, sauberer Validierung und einem Fortschrittsbalken, der echte Upload-Bytes anzeigt statt einer simulierten Animation.

17 Min. Lesezeit FileReader · XMLHttpRequest · Drag and Drop · Validierung Alpine.js 3.x

1. Warum ein einfaches File-Input nicht ausreicht

Ein natives <input type="file"> zeigt nach der Auswahl nur einen kryptischen Dateinamen an, ohne Vorschau und ohne Rückmeldung während des eigentlichen Hochladens. Bei einem Datei-Upload für Produktbilder, Profilfotos oder Dokumente erwarten Nutzer heute mindestens eine visuelle Bestätigung, was sie ausgewählt haben, und eine sichtbare Fortschrittsanzeige, solange die Datei noch überträgt.

Alpine.js eignet sich gut für einen eigenen Datei-Upload, weil die nötigen Bausteine, FileReader für die Vorschau, Drag-Events für die Ziehfunktion und XMLHttpRequest für echten Fortschritt, alle native Browser-APIs sind. Es braucht keine zusätzliche Upload-Bibliothek, um ein Ergebnis zu bauen, das sich mit kommerziellen Lösungen messen kann.

Der größte Denkfehler bei selbstgebauten Upload-Komponenten ist, den Fortschrittsbalken über setInterval zu simulieren, statt echte Bytes zu messen. Ein simulierter Balken, der bei neunzig Prozent stehen bleibt, während der Server noch verarbeitet, wirkt unfertig und untergräbt das Vertrauen in den Datei-Upload. Die folgenden Abschnitte zeigen, wie man stattdessen echten Fortschritt anzeigt.

2. Grundgerüst: Datei-Upload-Komponente mit x-data

Das Grundgerüst einer Alpine-Komponente für Datei-Upload braucht mindestens vier Zustände: die ausgewählte Datei, eine Vorschau-URL, den Fortschrittswert in Prozent und einen Status-String für lädt, fertig oder Fehler. Diese Trennung erlaubt es der Vorlage, jeden Zustand unabhängig darzustellen, ohne verschachtelte Bedingungen.

Wichtig ist, die Datei selbst nicht direkt in reaktiven Alpine-Daten zu speichern, sondern nur die für die Anzeige relevanten abgeleiteten Werte. Ein File-Objekt lässt sich zwar in x-data ablegen, sollte aber wie ein unveränderliches Referenzobjekt behandelt werden, das nur an FormData und XMLHttpRequest weitergereicht wird.


// Base skeleton for a file upload component with preview and progress
function fileUpload() {
  return {
    file: null,
    previewUrl: null,
    progress: 0,
    status: 'idle', // idle | reading | uploading | success | error
    errorMessage: '',

    onFileSelected(event) {
      const selected = event.target.files[0];
      if (!selected) return;
      this.setFile(selected);
    },

    setFile(selected) {
      this.file = selected;
      this.status = 'idle';
      this.progress = 0;
      this.errorMessage = '';
    },

    reset() {
      this.file = null;
      this.previewUrl = null;
      this.progress = 0;
      this.status = 'idle';
      this.errorMessage = '';
    },
  };
}

3. Bildvorschau mit FileReader ohne Server-Roundtrip

Die Bildvorschau eines Datei-Uploads muss nicht auf den Server warten. Die FileReader-API liest eine ausgewählte Datei direkt im Browser als Data-URL ein und liefert sie in Millisekunden zurück, lange bevor überhaupt ein Netzwerk-Request gestartet wurde. Das readAsDataURL()-Ergebnis lässt sich direkt als src-Attribut eines img-Elements binden.

Für sehr große Bilddateien lohnt sich eine Alternative: URL.createObjectURL(file) erzeugt eine Blob-URL, die schneller ist als das Einlesen der kompletten Datei als Base64-String, weil kein Encoding stattfindet. Der Nachteil ist, dass die Blob-URL mit URL.revokeObjectURL() explizit wieder freigegeben werden muss, sobald die Vorschau nicht mehr gebraucht wird, sonst hält der Browser den Speicher unnötig lange belegt.


function fileUpload() {
  return {
    file: null,
    previewUrl: null,
    status: 'idle',

    setFile(selected) {
      this.file = selected;
      this.status = 'reading';

      // Revoke the previous object URL before creating a new one
      if (this.previewUrl) URL.revokeObjectURL(this.previewUrl);

      if (selected.type.startsWith('image/')) {
        this.previewUrl = URL.createObjectURL(selected);
      } else {
        this.previewUrl = null; // No preview for non-image files
      }
      this.status = 'idle';
    },

    destroy() {
      // Cleanup when the component is removed from the DOM
      if (this.previewUrl) URL.revokeObjectURL(this.previewUrl);
    },
  };
}

4. Drag and Drop: Dateien per Ziehen statt nur per Klick

Drag and Drop verbessert die Bedienbarkeit eines Datei-Uploads erheblich, besonders auf Desktop-Geräten. Die Umsetzung braucht drei Ereignisse: dragover muss preventDefault() aufrufen, damit der Browser den Drop überhaupt erlaubt, dragleave setzt den visuellen Hover-Zustand zurück, und drop liest die Dateien aus event.dataTransfer.files aus, genau wie bei einer regulären Dateiauswahl.

Ein häufiger Fehler ist, den dragover-Handler nur auf dem äußersten Container zu registrieren, aber verschachtelte Kindelemente lösen bei jedem Übergang eigene dragenter/dragleave-Ereignisse aus, was zu flackerndem Hover-Zustand führt. Ein Zähler, der bei dragenter erhöht und bei dragleave verringert wird, löst dieses Problem zuverlässig.


function fileUpload() {
  return {
    file: null,
    isDragging: false,
    dragCounter: 0,

    onDragEnter() {
      this.dragCounter++;
      this.isDragging = true;
    },

    onDragLeave() {
      this.dragCounter--;
      if (this.dragCounter === 0) this.isDragging = false;
    },

    onDrop(event) {
      this.dragCounter = 0;
      this.isDragging = false;
      const dropped = event.dataTransfer.files[0];
      if (dropped) this.setFile(dropped);
    },
  };
}

5. Validierung von Dateigröße, Typ und Anzahl

Bevor ein Datei-Upload überhaupt gestartet wird, sollte er drei Dinge prüfen: die maximale Dateigröße, den erlaubten MIME-Typ und, bei Mehrfach-Uploads, die maximale Anzahl gleichzeitiger Dateien. Diese Prüfung gehört auf den Client, um Nutzern sofortiges Feedback zu geben, ersetzt aber nicht die serverseitige Validierung, weil file.type im Browser leicht manipulierbar ist und niemals als alleinige Sicherheitsgrenze gelten darf.

Für die Größenprüfung reicht ein Vergleich von file.size in Bytes gegen eine konfigurierte Obergrenze, üblicherweise fünf bis zehn Megabyte für Bilder. Für den Typ ist eine Allowlist aus erlaubten MIME-Typen robuster als eine Blocklist, weil neue, unerwartete Dateitypen sonst standardmäßig durchgelassen würden.


function fileUpload() {
  return {
    file: null,
    errorMessage: '',
    maxSizeBytes: 8 * 1024 * 1024, // 8 MB
    allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],

    setFile(selected) {
      const validationError = this.validate(selected);
      if (validationError) {
        this.errorMessage = validationError;
        this.file = null;
        return;
      }
      this.errorMessage = '';
      this.file = selected;
    },

    validate(candidate) {
      if (!this.allowedTypes.includes(candidate.type)) {
        return `Dateityp ${candidate.type} wird nicht unterstützt`;
      }
      if (candidate.size > this.maxSizeBytes) {
        const maxMb = (this.maxSizeBytes / 1024 / 1024).toFixed(0);
        return `Datei ist größer als ${maxMb} MB`;
      }
      return null;
    },
  };
}

6. Echter Fortschrittsbalken mit XMLHttpRequest

Die moderne fetch-API bietet keinen eingebauten Upload-Fortschritt, weil ihr Streaming-Modell dafür keinen Standard-Hook vorsieht. Für einen Datei-Upload mit echtem Fortschrittsbalken bleibt XMLHttpRequest deshalb weiterhin die richtige Wahl, konkret über das upload.onprogress-Ereignis, das loaded und total in Bytes liefert.

Der Fortschritt wird als Prozentsatz berechnet und direkt an eine reaktive Alpine-Eigenschaft gebunden, die die Breite eines Balkenelements per :style steuert. Da onprogress sehr häufig feuert, reicht die native Häufigkeit meist aus, ohne dass ein zusätzlicher Debounce nötig wäre, weil Alpines Reaktivitätssystem die DOM-Updates ohnehin bündelt.


function fileUpload() {
  return {
    file: null,
    progress: 0,
    status: 'idle',
    errorMessage: '',

    upload() {
      if (!this.file) return;
      this.status = 'uploading';
      this.progress = 0;

      const formData = new FormData();
      formData.append('file', this.file);

      const xhr = new XMLHttpRequest();
      xhr.open('POST', '/api/upload');

      // Real byte-level progress, not a simulated animation
      xhr.upload.addEventListener('progress', (event) => {
        if (event.lengthComputable) {
          this.progress = Math.round((event.loaded / event.total) * 100);
        }
      });

      xhr.addEventListener('load', () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          this.status = 'success';
          this.progress = 100;
        } else {
          this.status = 'error';
          this.errorMessage = 'Upload fehlgeschlagen';
        }
      });

      xhr.addEventListener('error', () => {
        this.status = 'error';
        this.errorMessage = 'Netzwerkfehler beim Upload';
      });

      xhr.send(formData);
    },
  };
}

7. Mehrere Dateien parallel hochladen mit Einzelstatus

Bei mehreren Dateien braucht jeder Datei-Upload-Eintrag seinen eigenen Fortschritt, Status und Fehlerzustand, unabhängig von den übrigen Dateien. Ein Array von Upload-Objekten, jedes mit eigener progress- und status-Eigenschaft, erfüllt diese Anforderung. Die Vorlage iteriert dann mit x-for über dieses Array und zeigt für jede Datei einen eigenen Fortschrittsbalken.

Ob Uploads parallel oder nacheinander gestartet werden, hängt von der Serverkapazität ab. Für die meisten Anwendungsfälle reicht eine Begrenzung auf drei bis vier gleichzeitige Uploads, mit einer Warteschlange für die restlichen Dateien, ähnlich dem Muster begrenzter Parallelität bei Hintergrundprozessen.

8. Fehlerbehandlung, Abbruch und erneuter Versuch

Ein robuster Datei-Upload muss drei Fehlerszenarien abdecken: der Nutzer bricht den Upload manuell ab, die Verbindung bricht ab, oder der Server lehnt die Datei aus fachlichen Gründen ab. Für den manuellen Abbruch reicht xhr.abort(), gebunden an einen Abbrechen-Button, der während des Uploads sichtbar wird.

Nach einem fehlgeschlagenen Upload sollte ein Wiederholen-Button erscheinen, der denselben Request erneut mit derselben Datei startet, ohne dass der Nutzer die Datei ein zweites Mal auswählen muss. Das setzt voraus, dass das File-Objekt auch nach einem Fehler in der Komponente erhalten bleibt und nicht vorzeitig verworfen wird.

9. Upload-Ansätze im Vergleich

Die folgende Tabelle vergleicht typische Entscheidungen beim Bau eines eigenen Datei-Uploads in Alpine.js.

Aspekt Unzureichend Empfohlener Datei-Upload Vorteil
Fortschritt setInterval-Simulation xhr.upload.onprogress echte Bytes, kein Stocken bei 90 Prozent
Vorschau Server-Roundtrip nötig FileReader oder Object URL sofortige Anzeige im Browser
Validierung nur serverseitig geprüft Client-Check plus Server-Check sofortiges Feedback, sichere Grenze
Drag-Hover flackert bei verschachtelten Kindern Zähler für dragenter/dragleave stabiler visueller Zustand
Abbruch kein Weg, Upload zu stoppen xhr.abort() an Abbrechen-Button Kontrolle für den Nutzer

Dieses Muster funktioniert unabhängig davon, ob der Datei-Upload für einzelne Profilbilder oder für Mehrfach-Uploads in einem Dokumentenverwaltungssystem eingesetzt wird. Die Kernbausteine, Vorschau, Validierung und echter Fortschritt, bleiben identisch.

Mironsoft

Alpine.js Upload-Komponenten und Frontend-Entwicklung

Datei-Upload, der wie ein Fremdkörper wirkt?

Wir bauen euch einen Datei-Upload mit Vorschau, Drag and Drop und echtem Fortschrittsbalken, der sich nahtlos in euer bestehendes Alpine.js-Frontend einfügt.

Upload-Komponente

Vorschau, Drag and Drop und Validierung nach Maß

Fortschrittsanzeige

Echter XMLHttpRequest-Fortschritt statt simulierter Balken

Server-Integration

Serverseitige Validierung für Magento und eigene APIs

10. Zusammenfassung

Ein durchdachter Datei-Upload in Alpine.js kombiniert vier Bausteine: eine sofortige Bildvorschau über FileReader oder Object URLs, Drag and Drop mit stabilem Hover-Zustand über einen Zähler, clientseitige Validierung von Größe und Typ als Ergänzung zur serverseitigen Prüfung, und einen echten Fortschrittsbalken über XMLHttpRequest statt einer simulierten Animation.

Diese Bausteine benötigen keine externe Upload-Bibliothek, weil alle nötigen Fähigkeiten native Browser-APIs sind. Bei mehreren gleichzeitigen Dateien braucht jeder Datei-Upload-Eintrag seinen eigenen Zustand, damit ein fehlgeschlagener Upload die übrigen Dateien nicht blockiert. Wer diese Struktur konsequent umsetzt, bekommt ein Upload-Erlebnis, das sich mit kommerziellen Lösungen messen kann.

Datei-Upload mit Vorschau und Fortschritt — Das Wichtigste auf einen Blick

Vorschau

FileReader oder URL.createObjectURL für sofortige Bildanzeige ohne Server-Roundtrip.

Fortschritt

xhr.upload.onprogress liefert echte Bytes, fetch bietet dafür keinen Standard-Hook.

Validierung

Größe und MIME-Typ clientseitig prüfen, serverseitig zwingend erneut absichern.

Drag and Drop

Zähler für dragenter/dragleave verhindert flackernden Hover-Zustand bei Kindelementen.

11. FAQ: Datei-Upload mit Vorschau und Fortschritt in Alpine.js

1Warum reicht ein einfaches file-Input nicht?
Es zeigt nur einen Dateinamen ohne Vorschau oder Fortschritt, was für moderne Uploads nicht mehr genügt.
2Wie erzeugt man eine Vorschau ohne Server?
FileReader.readAsDataURL() oder URL.createObjectURL() lesen die Datei direkt im Browser ein.
3Warum kein Fortschritt mit fetch?
fetch bietet keinen Standard-Hook für Upload-Fortschritt, XMLHttpRequest bleibt hierfür die zuverlässige Wahl.
4Nur clientseitig validieren?
Nein, file.type ist manipulierbar, serverseitige Validierung bleibt zwingend nötig.
5Flackernder Hover-Zustand vermeiden?
Ein Zähler für dragenter/dragleave setzt den Hover-Zustand erst bei null zurück.
6Mehrere Dateien gleichzeitig?
Ein Array von Upload-Objekten mit eigenem Status pro Datei und begrenzter Parallelität.
7Upload abbrechen?
xhr.abort() an einen sichtbaren Abbrechen-Button binden.
8Nach fehlgeschlagenem Upload?
Ein Wiederholen-Button startet denselben Request mit derselben Datei erneut.
9Blob-URL freigeben?
Ja, mit URL.revokeObjectURL(), sobald die Vorschau nicht mehr benötigt wird.
10Externe Bibliothek nötig?
Nein, FileReader, Drag-Events und XMLHttpRequest decken alles über native APIs ab.