Angular 4 validator to check 2 controls at the same time

I have a reactive form with 2 controls (port_start and port_end) that have the following requirements:

  • Both must have a value
  • Their values must be between 0 and 65535
  • port_start value must be less than port_end value

This is what I tried so far:

[...]
this.formModel.addControl('port_start', 
  new FormControl(object.port_start ? object.port_start : 0, 
  [Validators.required, Validators.min(0), Validators.max(65535), this.minMaxValidator('port_start', 'port_end').bind(this)]));

this.formModel.addControl('port_end', 
  new FormControl(object.ort_end ? object.port_end : 0, 
  [Validators.required, Validators.min(0), Validators.max(65535), this.minMaxValidator('port_start', 'port_end').bind(this)]));
[...]

This is the custom validator function:

minMaxValidator = function(startControl : string, endControl : string): ValidatorFn {
  return (control: FormControl): {[key: string]: any} => {
    let valid = true;
    let valStart = 0;
    let valEnd = 0;

    if(this.formModel.controls[startControl] && this.formModel.controls[endControl]) {
      valStart = <number>this.formModel.controls[startControl].value;

      valEnd = <number>this.formModel.controls[endControl].value;
    }

    valid = valEnd >= valStart;

    return valid ? null : { minmax : true };
  };
}

This works fine except for this problem:

  • Let's say I type '2' in the 'port_start' field. Angular marks it as non valid because it's more than the value of 'port_end' (which is 0 by default). If I type '5' in the 'port_end' field, the app still shows 'port_start' as invalid, although now it's correct.

I understand that the problem is that I need to re-check the associated field each time I change the other one's value, but I don't know how to do it.

Any ideas? Thanks,


Solution 1:

The min, max and required validators can be kept as is. If you want to validate one control based on the value of another, you need to lift the validation to the parent control.

import { Component } from '@angular/core';
import { ValidatorFn, FormBuilder, FormGroup, Validators } from '@angular/forms';

const portStartEnd: ValidatorFn = (fg: FormGroup) => {
   const start = fg.get('portStart').value;
   const end = fg.get('portEnd').value;

   return start && end && start < end ? null : { startEnd: true };
}

@Component({
  selector: 'my-app',
  template: `
   <input [formControl]="form.get('portStart')" type="number" >
   <input [formControl]="form.get('portEnd')" type="number" >

   {{ form.valid }}
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      portStart: [null, [Validators.required, Validators.min(0), Validators.max(65535)]],
      portEnd: [null, [Validators.required, Validators.min(0), Validators.max(65535)]]
    }, { validator: portStartEnd } );
  }
}

Live demo

Solution 2:

Unlike @Tomasz said, you can keep the validation at control level.

The secret : listen to changes on your form, and when it occurs, set the validators with the current values.

Here is a stackblitz that shows you how it works.

And here is the code I used :

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms';

@Component({
  selector: 'my-app',
  template: `
<form [formGroup]="form">
  <input type="number" formControlName="start">
  <input type="number" formControlName="end">
</form>

<div *ngIf="form.get('start').hasError('lessThan')">Min should be less than max</div>
<div *ngIf="form.get('end').hasError('moreThan')">Max should be less than min</div>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = fb.group({
      start: [0],
      end: [65535]
    });

    this.form.valueChanges.subscribe(changes => {
      this.form.get('start').setValidators(LessThanEnd(+this.form.value.end));
      this.form.get('end').setValidators(MoreThanStart(+this.form.value.start));
    });

  }
}

export function LessThanEnd(end: number): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } => {
    return control.value > end ? { 'lessThan': true } : null;
  };
}

export function MoreThanStart(end: number): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } => {
    return control.value < end ? { 'moreThan': true } : null;
  };
}