I need to save a CSV files' content in my Reactive Form as an input value for the Form Control. Currently only file name is selected by default and I need to save File data for that Form Control instead of just the file name.
I tried one of the approach mentioned here: Angular 7 : How do I submit file/image along with my reactive form?
It says to patch the Form Control value with the file data. When I try following this approach, I get following error:
ERROR DOMException: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string. at EmulatedEncapsulationDomRenderer2.push../node_modules/@angular/platform-browser/fesm5/platform-browser.js.DefaultDomRenderer2.setProperty (http://localhost:4200/vendor.js:134583:18) at BaseAnimationRenderer.push../node_modules/@angular/platform-browser/fesm5/animations.js.BaseAnimationRenderer.setProperty (http://localhost:4200/vendor.js:133181:27) at DebugRenderer2.push../node_modules/@angular/core/fesm5/core.js.DebugRenderer2.setProperty (http://localhost:4200/vendor.js:85257:23) at DefaultValueAccessor.push../node_modules/@angular/forms/fesm5/forms.js.DefaultValueAccessor.writeValue (http://localhost:4200/vendor.js:86345:24) at http://localhost:4200/vendor.js:87606:27 at http://localhost:4200/vendor.js:88761:65 at Array.forEach () at FormControl.push../node_modules/@angular/forms/fesm5/forms.js.FormControl.setValue (http://localhost:4200/vendor.js:88761:28) at FormControl.push../node_modules/@angular/forms/fesm5/forms.js.FormControl.patchValue (http://localhost:4200/vendor.js:88776:14) at http://localhost:4200/vendor.js:89118:38
onFileChange(event, formCotrolKey: string) {
if (event.target.files && event.target.files.length) {
const [file] = event.target.files;
this.formGroup.patchValue({
[formCotrolKey]: file
});
// need to run CD since file load runs outside of zone
this.changeDetectorRef.markForCheck();
}
}
This implementation worked for me exactly as I needed using ControlValueAccessor
. For this you simply need to create a Directive that implements ControlValueAccessor
interface.
Use the following code below:
import { ControlValueAccessor } from '@angular/forms';
import { Directive } from '@angular/core';
import { ElementRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ChangeDetectorRef } from '@angular/core';
let noop = () => {
};
@Directive({
selector: 'input[type=file][observeFiles]',
host: {
'(blur)': 'onTouchedCallback()',
'(change)': 'handleChange( $event.target.files )'
},
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: FileInputValueAccessorDirective,
multi: true
}
]
})
export class FileInputValueAccessorDirective implements ControlValueAccessor {
private elementRef: ElementRef;
private onChangeCallback: Function;
private onTouchedCallback: Function;
// I initialize the file-input value accessor service.
constructor(elementRef: ElementRef,
private changeDetectorRef: ChangeDetectorRef) {
this.elementRef = elementRef;
this.onChangeCallback = noop;
this.onTouchedCallback = noop;
}
public handleChange(files: FileList): void {
if (this.elementRef.nativeElement.multiple) {
this.onChangeCallback(Array.from(files));
} else {
const reader = new FileReader();
reader.readAsDataURL(files[0]);
reader.onload = () => {
this.onChangeCallback(files.length ? reader.result.toString().split(',')[1] : null);
this.changeDetectorRef.markForCheck();
};
}
}
public registerOnChange(callback: Function): void {
this.onChangeCallback = callback;
}
public registerOnTouched(callback: Function): void {
this.onTouchedCallback = callback;
}
// I set the disabled property of the file input element.
public setDisabledState(isDisabled: boolean): void {
this.elementRef.nativeElement.disabled = isDisabled;
}
public writeValue(value: any): void {
if (value instanceof FileList) {
this.elementRef.nativeElement.files = value;
} else if (Array.isArray(value) && !value.length) {
this.elementRef.nativeElement.files = null;
} else if (value === null) {
this.elementRef.nativeElement.files = null;
} else {
if (console && console.warn && console.log) {
console.log('Ignoring attempt to assign non-FileList to input[type=file].');
console.log('Value:', value);
}
}
}
}
Now include this directive in your module file under declarations array:
// Your Directive location
import { FileInputValueAccessorDirective } from 'app/forms/accessors/file-input.accessor';
@NgModule({
...
declarations: [
...
FileInputValueAccessorDirective
]
})
Finally in your Component template use:
<input observeFiles [(ngModel)]="fileContent" type="file" />
Make sure you have the variable fileContent
in your component to save the data. That is all needed. Data will be saved in base 64 format in the variable fileContent
.
If do not need base 64 Encoding you can replace following line in your directive:
this.onChangeCallback(files.length ? reader.result.toString().split(',')[1] : null);` inside `reader.onload
method with this line:
this.onChangeCallback(files.length ? atob( reader.result.toString().split(',')[1] ) : null);