Nicolas
Nicolas

Reputation: 4756

Angular 4 - Custom validator with dynamic parameter value

I have written a custom validator that checks if a date is above a certain minimum date.

the code looks like this:

export function validateMinDate(min: Date): ValidatorFn {
    return (c: AbstractControl) => {

        if (c == null || c.value == null)
            return null;

        let isValid = c.value >= min;
        if (isValid) {
            return null;
        } else {
            return {
                validateMinDate: {
                    valid: false
                }
            };
        }
    };
}

I initate my form like this

    this.definitionForm = this.fb.group({            
        "from": [this.details.From, Validators.required],
        "to": [this.details.To, [Validators.required, validateMinDate(this.details.From)]]
    });

I can see that the validator is being applied, but when I console.log() my min value in the validator I can see that it equal null.

this.details.From starts at null when I initiate the form, so I assume the parameter is not dynamic and just takes the value when the form is being set?

How can I make sure the min date is being updated when a users picks a from date, and thus changes the value of this.details.From?

Upvotes: 14

Views: 21135

Answers (4)

V.M.
V.M.

Reputation: 11

As an alternative to the given answers, at least in Angular 6 you can pass the component as ValidatorFn argument, so you can use its properties at runtime to validate your form control. Here is a working example:

Component declarations:

@Component({
    templateUrl: './create-application.component.html'
})
export class CreateApplicationComponent implements OnInit, OnDestroy {
   
    viewMode: ViewMode;
    application: Application;
    form: FormGroup;
    enterprisesData: any[] = [];

Form:

    this.form = this.formBuilder.group({
        name: new FormControl(
            { 
                value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.name : '', 
                disabled: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) 
            }, 
            { 
                validators: [ Validators.required, Validators.minLength(3), Validators.maxLength(80) ], 
                updateOn: 'blur' 
            }
        ),
        area: new FormControl(
            {
                value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.area : '',
                disabled: (this.viewMode === ViewMode.CONSULT) 
            }, 
            {
                validators: [ Validators.required ]
            }        
        ),
        country: new FormControl(
            {
                value: '',
                disabled: (this.viewMode === ViewMode.CONSULT)
            }, 
            {
                validators: [ applicationCountryEnterprisesValidator(this) ]
            }
        )
    });

ValidatorFn:

export function applicationCountryEnterprisesValidator(component: CreateApplicationComponent): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
        return (component.enterprisesData && component.enterprisesData.length > 0) ? null : { noEnterprisesSelected: true };
    };
}

Upvotes: 1

newbieCoder
newbieCoder

Reputation: 231

You can modify your custom validator to take function as parameter like

export function validateMinDate(min: DateFunc): ValidatorFn {
return (c: AbstractControl) => {

    if (c == null || c.value == null)
        return null;

    let isValid = c.value >= min();
    if (isValid) {
        return null;
    } else {
        return {
            validateMinDate: {
                valid: false
            }
        };
    }
};

and initiate the form like this

 this.definitionForm = this.fb.group({            
    ...
    "to": [this.details.To, [Validators.required, validateMinDate(() => this.details.From)]]
});

the DateFunc is just a type that you can create like

export interface DateFunc{
 (): Date
}

and expect this.details.From to return value of type Date

Upvotes: 14

dharmendra vaishnav
dharmendra vaishnav

Reputation: 2111

@Nicolas Validator takes value only once it does not look for it changes. So we can change parameters value dynamically by assigning new validator on value changes. In your case you can do in this way:

 onChanges(){
   var self=this;
    this.definitionForm.get('from').valueChanges.subscribe(val => {
     this.from=val;
     this.definitionForm.controls['to'].
     setValidators(Validators.compose([Validators.required, 
     TimeValidators.isTimeAfter(this.from)]));
})}

Here i created a separate custom validator for comparing the time. You can either use this or modify yours

import { FormControl, Validators,ValidatorFn, AbstractControl} from '@angular/forms';

export class TimeValidators extends Validators{

  static isTimeBefore(timeStr : string): ValidatorFn{
    return(c: AbstractControl): {[key:string]: boolean} | null => {
      if(c.value!==undefined && (isNaN(c.value)) || c.value > timeStr || c.value== timeStr){
        return {
          'isTimeBefore':true
        }
      }
      return null;
    }
  }
  static isTimeAfter(timeStr : string): ValidatorFn{
    return(c: AbstractControl): {[key:string]: boolean} | null => {
      if(c.value!==undefined && (isNaN(c.value)) && (c.value < timeStr || c.value == timeStr)){
        return {
          'isTimeAfter':true
        }
      }
      return null;
    }
  }
}

Call onChanges() function after you initialize your definitionForm FormGroup.

Upvotes: 16

AVJT82
AVJT82

Reputation: 73357

How I see it, would be to apply the validator on the form group, or if you have a large form, I suggest you create a nested group for from and to and apply the validator on that, since otherwise this custom validator would be fired whenever any changes happen to form. So it would mean to update the validator and formgroup to such:

this.definitionForm = this.fb.group({            
  "from": [this.details.From, Validators.required],
  "to": [this.details.To, [Validators.required]]
}, {validator: validateMinDate()});

export function validateMinDate(): ValidatorFn {
    return (c: AbstractControl) => {
      if(c) {
        let isValid = c.get('to').value >= c.get('from').value;
        if (isValid) {
            return null;
        } else {
            return {validateMinDate: true};
        }
      }
    };
}

Of course there are other options as well, such as listening for change event and then do the check of the dates, if not valid, use setErrors on form.

Upvotes: 5

Related Questions