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;
}