Reader Stacks

File Upload in Angular With FormData: Template-Driven and Reactive Forms

A file input's value can't be bound with ngModel — reading the selected file requires the (change) event's own file list instead, whether the surrounding form is template-driven or reactive.

A file input can't use [(ngModel)] or a reactive form's value binding the way a text input can — the browser doesn't allow JavaScript to set a file input's value programmatically for security reasons, so the selected file is always read through the (change) event instead.

Reading the selected file (works the same in either form style)

selectedFile: File | null = null;

onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.selectedFile = input.files?.[0] ?? null;
}

Uploading via a template-driven form

onUpload(): void {
    if (!this.selectedFile) return;

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

    this.http.post('/api/uploads', formData).subscribe(() => {
        console.log('Upload complete');
    });
}

Uploading via a reactive form

uploadForm = this.fb.group({
    description: ['', Validators.required],
});

selectedFile: File | null = null;

constructor(private fb: FormBuilder, private http: HttpClient) {}

onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.selectedFile = input.files?.[0] ?? null;
}

onSubmit(): void {
    if (this.uploadForm.invalid || !this.selectedFile) return;

    const formData = new FormData();
    formData.append('file', this.selectedFile);
    formData.append('description', this.uploadForm.value.description!);

    this.http.post('/api/uploads', formData).subscribe(() => {
        console.log('Upload complete');
    });
}

The file itself is handled identically in both form styles — it's tracked in a plain component property (selectedFile), not registered as a reactive form control, since FormControl is designed around values that can be validated and reset the normal way, which a File object doesn't cleanly support.

Why the Content-Type header should not be set manually

// WRONG — this breaks the multipart boundary the browser needs to set itself
this.http.post('/api/uploads', formData, {
    headers: { 'Content-Type': 'multipart/form-data' }
});

Angular's HttpClient automatically sets the correct Content-Type header, including the required multipart boundary string, when the request body is a FormData instance — manually overriding it (as shown above) strips out that boundary and breaks the upload, so the header should always be left for the browser to set.

Tracking upload progress

this.http.post('/api/uploads', formData, {
    reportProgress: true,
    observe: 'events',
}).subscribe(event => {
    if (event.type === HttpEventType.UploadProgress && event.total) {
        this.progress = Math.round((100 * event.loaded) / event.total);
    }
});

Validating file type and size before appending to FormData

onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    const file = input.files?.[0];

    if (file && !['image/jpeg', 'image/png'].includes(file.type)) {
        this.fileError = 'Only JPEG and PNG images are allowed.';
        return;
    }

    if (file && file.size > 5 * 1024 * 1024) {
        this.fileError = 'File must be under 5MB.';
        return;
    }

    this.fileError = '';
    this.selectedFile = file ?? null;
}