如何提交具有输入类型的响应式表单作为包含该文件数据的文件?

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

我需要将 CSV 文件的内容保存在我的响应式表单中作为表单控件的输入值。目前默认只选择文件名,我需要为该表单控件保存文件数据,而不仅仅是文件名。

我尝试了这里提到的一种方法:

它说用文件数据修补表单控件值。当我尝试采用这种方法时,出现以下错误:

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

此实现完全符合我使用 ControlValueAccessor 的需要。为此,您只需创建一个实现 ControlValueAccessor 接口的指令。

使用下面的代码:

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

现在将此指令包含在声明数组下的模块文件中:

// Your Directive location
import { FileInputValueAccessorDirective } from 'app/forms/accessors/file-input.accessor';

@NgModule({
  ...
  declarations: [
    ...
    FileInputValueAccessorDirective
  ]
})

最后在您的组件模板中使用:

<input observeFiles [(ngModel)]="fileContent" type="file" />

确保您的组件中有变量 fileContent 来保存数据。这就是所有需要的。数据将以 base 64 格式保存在变量 fileContent.

如果不需要 base 64 编码,您可以在指令中替换以下行:

this.onChangeCallback(files.length ? reader.result.toString().split(',')[1] : null);` inside `reader.onload

此行的方法:

this.onChangeCallback(files.length ? atob( reader.result.toString().split(',')[1] ) : null);