AngularChef
AngularChef

Reputation: 14087

How to add an async validator to a CUSTOM field?

Does anyone know how to declare an ASYNC validation method ON THE CLASS of a custom field?

Right now I have a sync validator in the validate() method:

@Component({
  selector: 'my-field',
  template: `<p>Some markup</p>`,
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyFieldComponent), multi: true },
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => MyFieldComponent), multi: true }
  ]
})
export class MyFieldComponent implements ControlValueAccessor {

  validate(c: FormControl) {
    // return null or error
  }

  // Rest of the code omitted for brevity
  // writeValue(), registerOnChange()...
}

But even if I add the NG_ASYNC_VALIDATORS to the providers using the same syntax as above, it doesn't let me declare something like a validateAsync() method.

Unless... both types of validator target the validate() method and I need to do both my sync and async validations in this method and return one big observable (potentially wrapping multiple error keys)? I'm not too sure about this.

SIDE NOTE: What I could get to work is declaring the async validator directly in the providers, either inline with useValue or as a separate class with useClass. But I'd like to have it as a method on the component class with useExisting.

Upvotes: 4

Views: 2266

Answers (2)

LeO
LeO

Reputation: 5258

I followed the sample of the directive. But I a somehow different approach on the end since I haven't had a clue how to set up the Validator properly. It did cost me some effort to figure out how to properly set up the whole stuff. Since the question is hit many times I thought I'll post my conclusions.

I retrieve the component I'm interested in from the constructor like

constructor(@Self() private valueAccessor: RefURLcheckComponent) {}

and within in the validate function I set the errors from the custom component

this.valueAccessor.setErrors(ret);

The approach works well for sync and async validators at the same time. Here is a working Stackblitz

Upvotes: 0

yurzui
yurzui

Reputation: 214085

I don't completely understand what is your requirement but i can give you some idea that maybe will help you.

So let start with our custom field that implements ControlValueAccessor and Validator (sync):

@Component({
  selector: 'my-field',
  template: `<p>Some markup</p>`,
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyFieldComponent), multi: true },
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => MyFieldComponent), multi: true }
  ]
})
export class MyFieldComponent implements ControlValueAccessor, Validator {
  onChange = (_: any) => { };
  onTouched = () => { };

  constructor(private _renderer: Renderer, private _elementRef: ElementRef) { }

  writeValue(value: any): void {
    const normalizedValue = value == null ? '' : value;
    this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
  }

  registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }

  validationResult: any;

  validate(c: FormControl) {
    this.validationResult = { 'sync': true };
    return null;
  }
}

After that declare directive that will implement AsyncValidator:

@Directive({
  selector: 'my-field[formControlName],my-field[ngModel]',
  providers: [{
    provide: NG_ASYNC_VALIDATORS,
    useExisting: forwardRef(() => CustomAsyncValidator),
    multi: true
  }]
})
class CustomAsyncValidator implements AsyncValidator {

  valueAccessor: MyFieldComponent;

  constructor(@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
    this.valueAccessor = valueAccessors.find(x => x.constructor === MyFieldComponent) as MyFieldComponent;
  }

  // the same as above. I would use it
  // constructor(@Self() private valueAccessor: MyFieldComponent) {}


  /**
   * presents global validation result async + sync
   */ 
  validate(control: AbstractControl): Observable<any> {
    return Observable.fromPromise(fakeAsyncValidator(1000)(control)).map(asyncResult => {
      return Object.assign({}, asyncResult, this.valueAccessor.validationResult);
    });
  }
}


function fakeAsyncValidator(timeout: number = 0) {
  return (c: AbstractControl) => {
    let resolve: (result: any) => void;
    const promise = new Promise(res => { resolve = res; });
    const res = { 'async': false };
    setTimeout(() => resolve(res), timeout);
    return promise;
  };
}

In the directive above we use the same selector as our custom field (my-field), provide NG_ASYNC_VALIDATORS and also inject existing component in constructor(you can notice two options).

And finally we have validate method in this directive that is trying to do something similar what you want.

Plunker Example

Upvotes: 7

Related Questions