New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FormControl.setValidators...why is there no getValidators? #13461

Open
Icepick opened this Issue Dec 14, 2016 · 29 comments

Comments

Projects
None yet
@Icepick

Icepick commented Dec 14, 2016

I'm submitting a ... (check one with "x")

[ ] bug report => search github for a similar issue or PR before submitting
[x] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior
We have an AbstractControl with basic validators like (required, maxLength) in a ReactiveForm via TypeScript FormBuilder. To those basic validators we added a custom validator which dynamically change the validation strategy based on a dropdown value. Currently we are using the setValidators() method inside another component (AbstractControl is included via @Input()). The main problem is that it overwrites existing validators.
Example:

App Component

this.formBuilder.group({
      zip: ['', [Validators.required, Validators.maxLength(10)]] 
    });

App Template

<zip ...
        [control]="form.controls.zip">
</zip>

ZIP Component

@Input() control: AbstractControl;

this.control.setValidators([
      // redundant to formBuilder
      Validators.required,
      Validators.maxLength(10),
      // custom validation based on a dropdown value (via. valueChange Detection)
      validateZipFn(countryCode)]
    );

Expected behavior
To stay flexible we don't want to overwrite all validators. The below code would illustrate the behavior with a getValidators() method.

App Component

this.formBuilder.group({
      zip: ['', [Validators.required, Validators.maxLength(10)]]
    });

App Template

<zip ...
        [control]="form.controls.zip">
</zip>

ZIP Component

@Input() control: AbstractControl;

let listOfAllValidationRules = this.control.getValidators().push(validateZipFn(countryCode)]);
this.control.setValidators(listOfAllValidationRules);

What is the motivation / use case for changing the behavior?
Being more flexible with dynamic validation.

  • Angular version: ~2.1.2

  • Browser: all

  • Language: TypeScript / ES6

@DzmitryShylovich

This comment has been minimized.

Contributor

DzmitryShylovich commented Mar 10, 2017

@Icepick there's nothing to return. validators are stored as a single object, not an array.

@Bidthedog

This comment has been minimized.

Bidthedog commented Mar 15, 2017

@DzmitryShylovich Does that mean this cannot be implemented? Personally, I only need a readonly copy of the validators with the metadata attached.

@xeii

This comment has been minimized.

xeii commented Mar 17, 2017

I am building a directive where an object of validation messages is passed in. This is simply an object with the validation rule name is the key and the value is the message to be displayed. I would like to compare this object and the set validators of the NgControl, so that the user is warned if a certain message is missing.

@oehm-smith

This comment has been minimized.

oehm-smith commented Mar 20, 2017

I would like to see this also for similar reasons to @Bidthedog. For example, if a Validator.requried is on a control then we'd like to programatically indicate this on the form. Read-only is all we require. Currently the best I can do is define an array on the component and pass that to the form control definition. Its going to get ugly pretty quickly!

Is this on the road map and if so is there any ETA on this functionality?

@Bidthedog

This comment has been minimized.

Bidthedog commented Mar 20, 2017

Exactly. I basically have three big nasty duplicate definitions in my form service now. The below code snippets are a tiny excerpt from a rather large, 6 page form I'm putting together. This is for page one (details) and the start of page 2 (medical):

To create / define:

  private createForm() {
    this.form = this.fb.group({
      details: this.fb.group({
        NHSNumber: [null, [Validators.required, Validators.pattern(/^\d{3}-\d{3}-\d{4}$/)]],
        titleId: [null, [Validators.required]],
        firstName: [null, [Validators.required, Validators.maxLength(10)]],
        middleNames: null,
        lastName: [null, [Validators.required]],
        DOB: [null, [Validators.required]],
        genderId: [null, [Validators.required]],
        regionId: [null, [Validators.required]],
        maritalStatusId: [null, [Validators.required]],
        occupationTypeId: [null],
        occupationId: null,
        telephoneNo: null,
        mobileNo: null,
        houseNumber: null,
        streetName: null,
        town: null,
        city: null,
        county: null,
        postcode: null,
        GPId: null,
        nextOfKinId: null,
        livesWith: null
        //vehicleLicenseTypeId?: number[]
      }),
      medical: this.fb.group({
        gpId: [null, [Validators.required]]
      })
    });
  }

To populate:

  private populateForm() {
    let m = this.model;
    this.form.setValue({
      details: {
        NHSNumber: this.getTextValue(m.NHSNumber),
        titleId: this.getLookupValue(m.titleId),
        firstName: this.getTextValue(m.firstName),
        middleNames: this.getTextValue(m.middleNames),
        lastName: this.getTextValue(m.lastName),
        DOB: this.getDateValue(m.DOB),
        genderId: this.getLookupValue(m.genderId),
        regionId: this.getLookupValue(m.regionId),
        maritalStatusId: this.getLookupValue(m.maritalStatusId),
        occupationTypeId: this.getLookupValue(m.occupationTypeId),
        occupationId: this.getLookupValue(m.occupationId),
        telephoneNo: this.getTextValue(m.telephoneNo),
        mobileNo: this.getTextValue(m.mobileNo),
        houseNumber: this.getTextValue(m.houseNumber),
        streetName: this.getTextValue(m.streetName),
        town: this.getTextValue(m.town),
        city: this.getTextValue(m.city),
        county: this.getTextValue(m.county),
        postcode: this.getTextValue(m.postcode),
        GPId: this.getLookupValue(m.GPId),
        nextOfKinId: this.getLookupValue(m.nextOfKinId),
        livesWith: this.getTextValue(m.livesWith)
      },
      medical: {
        gpId: this.getLookupValue(m.GPId) || 1
      }
    });

Validation rules. Not complete, but you can see how this is going to get messy, fast

  private formValidationMessages = {
    details: {
      NHSNumber: {
        required: '\'NHS Number\' is required',
        pattern: '\'NHS Number\' must be in the format \'NNN-NNN-NNNN\' where N is a digit'
      },
      titleId: {
        required: '\'Title\' is required'
      },
      firstName: {
        required: '\'First Name\' is required'
      }
    },
    medical: {
    }
  };

I started working on defining all the form rules in one object set, which I'm thinking of iterating through / projecting into the functions listed above (though I've not got that far yet):

  private formValidationMessages = {
    details: {
      NHSNumber: {
        required: '\'NHS Number\' is required',
        pattern: '\'NHS Number\' must be in the format \'NNN-NNN-NNNN\' where N is a digit'
      },
      titleId: {
        required: '\'Title\' is required'
      },
      firstName: {
        required: '\'First Name\' is required'
      }
    },
    medical: {
    }
  };

On top of this, of course, I have the data model definition, and then all the layers on the back-end; there has to be something we can do to reduce code bloat?

@JamesHenry

This comment has been minimized.

JamesHenry commented Apr 2, 2017

I also just got hit by not being able to retrieve the configured Validators for a FormControl.

I worked around it by adding a utility service which wraps the config object before it is passed into the FormBuilder, and keeps track of a mapping between control names and their given Validators.

It would naturally be great to remove the need for this extra service 😄

@Bidthedog

This comment has been minimized.

Bidthedog commented Apr 3, 2017

Well, since I posted here I've consolidated all my form configuration into one big object, and wrote a few helper methods that do the mapping between form and model based on the config. It all seems way too complicated, but it could just be the learning curve I've had.

@RabbitHunter99

This comment has been minimized.

RabbitHunter99 commented May 24, 2017

Hi,
just wondering if there is any word on this feature. It seems to me that being able to visually mark certain items as required is a basic feature expected of any modern forms UI. Yet since there is no way to retrieve the information whether a field is required or not, it is impossible to implement this feature using reactive forms.

@Bidthedog

This comment has been minimized.

Bidthedog commented May 25, 2017

It's not impossible, it's just a LOT of work.

@Toub

This comment has been minimized.

Toub commented Aug 2, 2017

@Bidthegod do you mean it would be a LOT of work to implement this feature in angular, or it would be a LOT of work to retrieve the validators in an angular component.

If it is possible to retrieve the validators in a component, any input about how to do is welcome!

Or if it is easier for certain component types (input, select), it can be useful too.

@Bidthedog

This comment has been minimized.

Bidthedog commented Aug 2, 2017

@Toub What I ended up doing was building a "config" object structure that contained every individual form field, and all its settings (display, validators and rules etc). I then wrote a few helper methods to query the config, which builds up the FormGroup. Each form has a form service, and inherits from a base component.

I wouldn't say it's an easy fix, and it's tons of code, but it ended up being fairly elegant and easy to create a fairly complex form. Threw some of the code in a gist for you to look at, but I've not got time to modularise properly it at the moment. There are quite a few files missing from this - including all the custom form input components I wrote.

https://gist.github.com/Bidthedog/1dc7d10cda1759061c09f44f7b48cbf3

@Toub

This comment has been minimized.

Toub commented Aug 2, 2017

@Bidthedog it doesn't feet my needs but thanks for your answer, as the gist can be useful for others.

@rodolfojnn

This comment has been minimized.

rodolfojnn commented Aug 30, 2017

Hey guys, I created a simple helper to return the validators (just for the required, but can be extended for others):

getValidators(_f) {
  return Object.keys(_f).reduce((a, b) => {
    const v = _f[b][1];
    if (v && (v === Validators.required || v.indexOf(Validators.required) > -1)) {
      if (!a[b]) { a[b] = {}; }
      a[b]['required'] = true;
    }
    return a;
  }, {});
}

const _f = {
  id: [],
  name: [null, Validators.required],
  phone: [],
  email: [null, [Validators.required, Validators.email]]
};
this.frmMain = this._fb.group(_f);
console.log(this.getValidators(_f));    // {name: {"required": true}, email: {"required": true}}

Fits my needs 👍

@erikrasmussen23

This comment has been minimized.

erikrasmussen23 commented Sep 11, 2017

I agree, having a read-only list of validators that could be obtained by a method on the current control would be very helpful.

@ghost

This comment has been minimized.

ghost commented Sep 14, 2017

+1 for this feature

@darkolakovic

This comment has been minimized.

darkolakovic commented Sep 15, 2017

+1 for read-only list

@padilla-jm

This comment has been minimized.

padilla-jm commented Oct 13, 2017

+1

@RidelHI

This comment has been minimized.

RidelHI commented Oct 23, 2017

I need that functionality to dynamically add validators without losing those that already exist. Something like:
Object.keys(this.myForm.controls).forEach(key => { if (map.get(key)) { this.myForm.get(key).setValidators(this.myForm.get(key).getValidators().push(Validators.required)) } });
Fully explained in: https://stackoverflow.com/questions/46852063/how-to-dynamically-add-validators-to-the-forms-in-angular-2-4-using-formbuilde.
This must be something common

@ppham27

This comment has been minimized.

Contributor

ppham27 commented Oct 23, 2017

It's actually not too hard to dynamically add validators with composeValidators (https://github.com/angular/angular/blob/master/packages/forms/src/directives/shared.ts#L139). I think removing validators is the bigger issue. You can do it with a boolean but then your validator is no longer a pure function.

@mtinner

This comment has been minimized.

mtinner commented Oct 30, 2017

this function should work wor FormGroups and FormControls to determine required Validators

      export const hasRequiredField = (abstractControl: AbstractControl): boolean => {
    	if (abstractControl.validator) {
    		const validator = abstractControl.validator({}as AbstractControl);
    		if (validator && validator.required) {
    			return true;
    		}
    	}
    	if (abstractControl['controls']) {
    		for (const controlName in abstractControl['controls']) {
    			if (abstractControl['controls'][controlName]) {
    				if (hasRequiredField(abstractControl['controls'][controlName])) {
    					return true;
    				}
    			}
    		}
    	}
    	return false;
    };
@strizzaflex

This comment has been minimized.

strizzaflex commented Dec 13, 2017

@mtinner That was a really great find. That is getting me the initial set. I think it would still be worth adding to the API to allow for a return of an observable of the validators. That way finding a conditional required control would be very simple. I'm not sure the proposed workaround here can be applied to catch those instances.

@paolo-galfione

This comment has been minimized.

paolo-galfione commented Jan 5, 2018

In a very nice presentation of Angular Connect 2017 @kara ends by saying "custom forms controls are awesome & not scary". Good, but this important feature for custom controls is missing

@danderwald

This comment has been minimized.

danderwald commented Feb 22, 2018

+1

@seedy

This comment has been minimized.

seedy commented Mar 16, 2018

No time for a PR but here's what I came up with :

import {FormControl} from '@angular/forms';
import {AsyncValidatorFn, ValidatorFn} from '@angular/forms/src/directives/validators';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {AbstractControlOptions} from '@angular/forms/src/model';

export class DescendingFormControl extends FormControl {

    private asyncValidationChangesSubject = new Subject<AsyncValidatorFn | AsyncValidatorFn[]>();
    private validationChangesSubject = new Subject<ValidatorFn | ValidatorFn[] | null>();

    public readonly validationChanges: Observable<ValidatorFn | ValidatorFn[] | null>;
    public readonly asyncValidationChanges: Observable<AsyncValidatorFn | AsyncValidatorFn[]>;

    constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
        super(formState, validatorOrOpts, asyncValidator);
        this.validationChanges = this.validationChangesSubject.asObservable();
        this.asyncValidationChanges = this.asyncValidationChangesSubject.asObservable();
    }

    public setValidators(newValidator: ValidatorFn | ValidatorFn[] | null): void {
        super.setValidators(newValidator);
        this.validationChangesSubject.next(newValidator);
    }

    public setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[]): void {
        super.setAsyncValidators(newValidator);
        this.asyncValidationChangesSubject.next(newValidator);
    }
}

If anyone's up to do the whole PR job before I get some free time, go for it!

@pete4dev

This comment has been minimized.

pete4dev commented Sep 14, 2018

+1

2 similar comments
@mihaimascas

This comment has been minimized.

mihaimascas commented Oct 5, 2018

+1

@Lowkey2224

This comment has been minimized.

Lowkey2224 commented Oct 8, 2018

+1

@jeremyben

This comment has been minimized.

jeremyben commented Nov 27, 2018

The problem : It's trapped

An array of present validators exists but is not available to us...
Have a look at the Validators static method compose, which is the method used behind the scenes when you define an array of Validators on your form control :

 static compose(validators: (ValidatorFn|null|undefined)[]|null): ValidatorFn|null {
    if (!validators) return null;

    // Here it is
    const presentValidators: ValidatorFn[] = validators.filter(isPresent) as any; 

    if (presentValidators.length == 0) return null;

    return function(control: AbstractControl) {
      return _mergeErrors(_executeValidators(control, presentValidators));
    };
  }

https://github.com/angular/angular/blob/master/packages/forms/src/validators.ts#L342

As you can see, presentValidators is what we're looking for, but it's trapped in a closure, so inaccessible, and only value you get from compose is an object with all the triggered errors for a given value.

So the only way to get infer the validators is to manually trigger the errors on your form control, which will reference the validators names.

The hack : Trigger an error on a different control

You don't want to trigger an error on your binded form control. You should instead create a new form control, free from any binding to your template, and apply the same Validator functions to him, then trigger the errors you want by setting a value :

// Your form control, actually binded to a form in your template

emailControl = new FormControl('', [Validators.required, Validators.email])

// Elsewhere in your code
// You use dummy form controls, on which you apply your original form control validators and various values, to trigger the errors.

const errorsWhenEmpty = new FormControl('', this.emailControl.validator, this.emailControl.asyncValidator).errors
const errorsWhenOneChar = new FormControl('x', this.emailControl.validator, this.emailControl.asyncValidator).errors

// Or you can simply reuse the first dummy control with `setValue()`

It is far from ideal, but it's the least dirty way I have found, until we can get our hands on the validators themselves.

@GGGErnest

This comment has been minimized.

GGGErnest commented Dec 14, 2018

I will suggest just to extend from the FormControl class and save in the fields of the children class a copy of the Validator functions passed into the constructor, something like this:

export class ExtFormControl extends FormControl {
  private _syncValidators: ValidatorFn | ValidatorFn[];

  private _asyncValidators: AsyncValidatorFn | AsyncValidatorFn[];

  constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | null,
              asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
    super(formState, validatorOrOpts, asyncValidators);
    this._syncValidators = validatorOrOpts;
    this._asyncValidators = asyncValidators;
  }


  getSyncValidators(): ValidatorFn[] {
    if (typeof this._syncValidators === 'function') {
      return [this._syncValidators];
    } else {
      return this._syncValidators;
    }
  }

  getAsyncValidators(): AsyncValidatorFn[] {
    if (typeof this._asyncValidators === 'function') {
      return [this._asyncValidators];
    } else {
      return this._asyncValidators;
    }
  }

  setValidators(newValidator: ValidatorFn | ValidatorFn[] | null): void {
    super.setValidators(newValidator);
    this._syncValidators = newValidator;
  }

  setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null): void {
    super.setAsyncValidators(newValidator);
    this._asyncValidators = newValidator;
  }

}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment