2

I am creating a custom form control component (email) that contains an input field, I forward the value of the input field (done correctly) but also want to forward its errors.

Using the following code I successfully pass the field's errors when the input changes but it does not pick up the initial errors correctly.

For example this email field will still report errors = {'required':true} even after the view is fully loaded and a value of '[email protected]' is passed to it. Starting to type in the field and it will pass the errors correctly.

So my question is, how can I pass the errors after the initial load of data?

note: the problem is resolved by running this.propagateChange(this.value); in DoCheck lifecycle but I don't like it, I need something that is more efficient but no other hook seems to do the trick.

Here is the example:

import {Component, forwardRef, Input, ViewChild} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl, NgModel} from '@angular/forms';

@Component({
  selector: 'input-email',
  template:`<form-group
  [errors]="{'required':'Email required', 'email':'Invalid email format'}"
  [info]="'Email*'"
>

  <input
    type        = "email"
    name        = "email"
    class       = "form-control"
    [(ngModel)] = "value"
    placeholder = "{{placeholder}}"
    (input)="onChange()"
    email
    required
    #f          = "ngModel"
  >
  {{f.errors | json}}
</form-group>`,
  styleUrls: ['./email.component.css'],
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputEmailComponent), multi: true },
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputEmailComponent), multi: true }
  ]
})
export class InputEmailComponent implements ControlValueAccessor {


  value:String = null;

  @ViewChild('f') f:NgModel;

  @Input()
  placeholder:String = "Email";

  propagateChange:any = (val) => {};

  constructor() {}



  onChange(){
    this.propagateChange(this.value);
  }

  /**
   * Write a passed NgValue value to the element.
   */
  writeValue(value) {
    if (value && this.value != value) {
      this.value = value;
    }
  }

  /**
   * Set the function to be called
   * when the control receives a change event.
   * registers 'fn' that will be fired when changes are made
   * this is how we emit the changes back to the form
   */
  registerOnChange(fn) {
    this.propagateChange = fn;
  }

  /**
   * Set the function to be called
   * when the control receives a touch event.
   */
  registerOnTouched(fn) {}


  /**
   * Set the function to be called
   * to validate if input has errors.
   */
  validate(c: FormControl):any {

    console.log('validate email');
    return this.f.errors;

  }
}

3 Answers 3

1

The problem can be resolved by using NG_ASYNC_VALIDATORS instead of NG_VALIDATORS, I am posting here the solution that worked for me:

import {Component, forwardRef, Input, ViewChild, KeyValueDiffers} from '@angular/core';
import {
  ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, NgModel,
  NG_ASYNC_VALIDATORS
} from '@angular/forms';
import {Observable} from "rxjs/Rx";

@Component({
  selector: 'input-email',
  templateUrl: './email.component.html',
  styleUrls: ['./email.component.css'],
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputEmailComponent), multi: true },
    { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => InputEmailComponent), multi: true }
  ]
})
export class InputEmailComponent implements ControlValueAccessor {


  value:String = null;
  differ: any;

  @ViewChild('f') f:NgModel;

  @Input()
  info:String = "Email";

  @Input()
  placeholder:String = "Email";

  propagateChange:any = (val) => {};

  constructor(private differs: KeyValueDiffers) {
    this.differ = differs.find({}).create(null);
  }

  onChange(){
    this.propagateChange(this.value);
  }

  /**
   * Write a passed NgValue value to the element.
   */
  writeValue(value) {
    if (value && this.value != value) {
      this.value = value;
      //setTimeout(()=>{this.propagateChange(this.value);},0)
    }
  }

  /**
   * Set the function to be called
   * when the control receives a change event.
   * registers 'fn' that will be fired when changes are made
   * this is how we emit the changes back to the form
   */
  registerOnChange(fn) {
    this.propagateChange = fn;
  }

  /**
   * Set the function to be called
   * when the control receives a touch event.
   */
  registerOnTouched(fn) {}


  /**
   * Set the function to be called
   * to validate if input has errors.
   */
  validate(c: FormControl):any {

    return new Promise((resolve, reject) => {
      Observable.of(c)
        .debounceTime(300)
        .switchMap(val => {
            return Observable.of(val.errors);
        })
        .subscribe(result => {
          console.log('RESOLVING ASYNC VALIDATOR: ' + JSON.stringify(result, null, 2));
          resolve(result);
        });
    });

  }
}
Sign up to request clarification or add additional context in comments.

Comments

0

I ran into trouble employing your proposal because of the issue FormGroup & FormControl statusChanges are not emitted on creation #14542.

Therefore I went with sync. validators like this:

export class MyInputComponent implements AfterViewInit, ControlValueAccessor, Validator {
  ...
  @ViewChild('datePicker') public datePickerInput: NgbInputDatepicker;
  ...
  ngAfterViewInit(): void {
    this.ngControl = this.injector.get(NgControl);

    // Force restart of validation
    if (this.ngControl && this.ngControl.control) {
      this.ngControl.control.updateValueAndValidity({
        onlySelf: true
      });
    }
  }
  ...
  public validate(control: AbstractControl): ValidationErrors | null {
    return this.datePickerInput ? this.datePickerInput.validate(control) : null;
  }

Comments

0

I got around this by calling the onValidatorChange callback (from the Validator interface) in ngAfterViewInit:

  ngAfterViewInit(): void {
    // Force the validation to trigger after the view has been initialised
    // otherwise the initial value is not marked as invalid
    if (this.onValidatorChange) {
      // you need a setTimeout here to make the change after Angular's change detection
      setTimeout(() => {
        this.onValidatorChange();
        this.cdr.markForCheck();
      }, 0);
    }
  }

  registerOnValidatorChange(fn: () => void): void {
    this.onValidatorChange = fn
  }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.