Angular doesn't have a special file-upload directive — a file <input> is just a native DOM element, and getting the selected file out of it is a plain DOM event, not a reactive-forms binding.
1. Reading the selected file
<input type="file" (change)="onFileSelected($event)">
selectedFile: File | null = null;
onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
this.selectedFile = input.files?.[0] ?? null;
}
formControlName doesn't work on <input type="file"> — browsers don't allow JavaScript to programmatically set a file input's value for security reasons, so two-way binding through Angular's forms API isn't possible here; the (change) event is the only reliable way to read it.
2. Building the FormData payload
upload(): void {
if (!this.selectedFile) return;
const formData = new FormData();
formData.append('file', this.selectedFile, this.selectedFile.name);
formData.append('description', this.description); // any extra fields go alongside it
this.http.post('/api/uploads', formData).subscribe({
next: () => console.log('Uploaded'),
error: (err) => console.error('Upload failed', err),
});
}
FormData is a browser API, not an Angular one — HttpClient recognizes it automatically and sets the request's Content-Type to multipart/form-data with the correct boundary string on its own. Setting that header manually is a common mistake: it breaks the request, because the boundary Angular would have generated no longer matches what's actually in the body.
3. Showing upload progress
For large files, a progress bar needs the request's raw events rather than just the final response — pass reportProgress: true and observe: 'events':
import { HttpEventType } from '@angular/common/http';
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);
}
if (event.type === HttpEventType.Response) {
console.log('Upload complete', event.body);
}
});
4. Validating before upload
File type and size limits should be checked client-side for a fast, obvious error message — but the server must repeat the same checks, since a client-side check is trivial to bypass and never a substitute for real validation:
onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const maxSizeBytes = 5 * 1024 * 1024;
if (file.size > maxSizeBytes) {
this.error = 'File must be under 5MB';
return;
}
if (!['image/png', 'image/jpeg'].includes(file.type)) {
this.error = 'Only PNG and JPEG files are allowed';
return;
}
this.error = null;
this.selectedFile = file;
}