20

Angular 2 seems to have troubles with running validation when a file input changes.

I made a plunk to illustrate this problem:

I make a formGroup like

this.frm = new FormGroup({
    file: new FormControl("", this.validateFile)
});

And in the validateFile function I throw an alert and log to the console:

public validateFile(formControl: FormControl): {[key: string]: any; } {
   alert('Validation ran');
   console.log('Validation ran');
}

Plunkr to illustrate the issue: https://plnkr.co/edit/Pgcg4IkejgaH5YgbY3Ar?p=preview

The validation will run when initializing the page but won't run each time you change the file to be uploaded.

Is there any solution to this problem?

S. Robijns
  • 1,529
  • 3
  • 14
  • 17

3 Answers3

47

I fixed it using kemsky answer and Sebastien's comment. I made a ngValueAccessor which registers itself on every input with type file.

Plunkr can be found here.

Most relevant code + explanation beneath:

This adds a ControlValueAccessor for file inputs which might be part of the angular framework itself someday(#7341). A file input works different than other controls. This piece of code makes sure the selected files are read as the value:

import {Directive} from "@angular/core";
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from "@angular/forms";

@Directive({
    selector: "input[type=file]",
    host : {
        "(change)" : "onChange($event.target.files)",
        "(blur)": "onTouched()"
    },
    providers: [
        { provide: NG_VALUE_ACCESSOR, useExisting: FileValueAccessor, multi: true }
    ]
})
export class FileValueAccessor implements ControlValueAccessor {
    value: any;
    onChange = (_) => {};
    onTouched = () => {};

    writeValue(value) {}
    registerOnChange(fn: any) { this.onChange = fn; }
    registerOnTouched(fn: any) { this.onTouched = fn; }
}

And for the 'required' validation I made a validator which I use by adding the static validate method to the file FormControl for ReactiveForms. (or as a directive for template driven forms).

import {Directive} from "@angular/core";
import {NG_VALIDATORS, Validator, FormControl} from "@angular/forms";

@Directive({
    selector: "[requiredFile]",
    providers: [
        { provide: NG_VALIDATORS, useExisting: FileValidator, multi: true },
    ]
})
export class FileValidator implements Validator {
    static validate(c: FormControl): {[key: string]: any} {
        return c.value == null || c.value.length == 0 ? { "required" : true} : null;
    }

    validate(c: FormControl): {[key: string]: any} {
        return FileValidator.validate(c);
    }
}

Building my form looks like this:

private buildForm() {
    this.frm = new FormGroup({
        file: new FormControl("",    [FileValidator.validate])
    });
}

And for the html:

<input type="file" formControlName="file"/>
S. Robijns
  • 1,529
  • 3
  • 14
  • 17
  • 1
    Hi,thanks for your help . I try to run this on angular 4.3 but it's doesn't work. First, it's still invalid after i upload file and second my 'file' field after submit it's just empty string.. I need to change something ? Thank you again, – OriEng Oct 17 '17 at 08:48
  • @S.Robijns I don't have plunker to show , just nothing happen. is you success to get file object on your onSubmit() function ? Thx . – OriEng Oct 19 '17 at 12:16
  • @S.Robijns this is not working in Angular 5+ here is example [angular-input-flie-validation-reactive-form](https://stackblitz.com/edit/angular-input-flie-validation-reactive-form) – Sanoj_V Sep 10 '19 at 10:43
  • 1
    No need for custom `required` validation. You can just use Angular's `Validators.required` validator which uses this function under the hood: https://github.com/angular/angular/blob/539d720fcde2ba5094224abc6e7380f9f6d4828f/packages/forms/src/validators.ts#L16. – Lukasz Prus Nov 16 '21 at 12:16
10

Angular 4+ has changed there hostbindings.

import { Directive, HostListener } from "@angular/core";
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from "@angular/forms";

@Directive({
    selector: "input[type=file]",
    providers: [
        {provide: NG_VALUE_ACCESSOR, useExisting: FileValueAccessorDirective, multi: true}
    ]
})
export class FileValueAccessorDirective implements ControlValueAccessor {
    @HostListener('change', ['$event.target.files']) onChange = (_) => {};
    @HostListener('blur') onTouched = () => {};

    writeValue(value) {}
    registerOnChange(fn: any) { this.onChange = fn; }
    registerOnTouched(fn: any) { this.onTouched = fn; }
}
Fritz
  • 173
  • 1
  • 6
  • 1
    It seems it did not change in angular 4, it looks like HostBinding en HostListener have always been around. Both ways work but according the style guide this one is indeed preferred(https://angular.io/guide/styleguide) – S. Robijns Nov 23 '17 at 11:20
  • 1
    Just ran into a case that proves it's definitely better to use host bindings, since they can be inherited and when you put it in the decorator it can't. More here: https://medium.com/@ttemplier/angular2-decorators-and-class-inheritance-905921dbd1b7 I'll probably modify my answer sometime later – S. Robijns Nov 24 '17 at 15:33
  • 1
    @MrBoJangles yeah idd.. don't count on it anymore, time is of the essence my friend :) – S. Robijns Oct 17 '18 at 11:58
  • @Fritz this is not working in Angular 5+ here is example [angular-input-flie-validation-reactive-form](https://stackblitz.com/edit/angular-input-flie-validation-reactive-form) – Sanoj_V Sep 10 '19 at 10:43
2

Input with type file is not supported currently, see #7341

kemsky
  • 14,727
  • 3
  • 32
  • 51
  • 2
    It is not supported but you can make it work. In the linked discussion there is an example of how to do it. – Sebastian Jan 27 '17 at 09:06