Search code examples
angularangular7angular-reactive-forms

How to submit reactive form with input type as a File with data for that file?


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

Solution

  • 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);