Search code examples
formsangularcustomvalidatorreactive

reactive forms: use one validator for multiple fields


I'm using angular 2 reactive forms and made a validator for a date of birth field. The validator is working, but it turns out the date of birth field is split into three new field: year, month, day. They all have their own validators. My question is, how can I change my code so my original date of birth validator works on three fields.

my original validator that checks one field. input(2000/12/12) is valid

    export function dobValidator(control) {
  const val = control.value;
  const dobPattern = /^\d{4}\/\d{2}\/\d{2}$/ ;
  const comp = val.split('/');
  const y = parseInt(comp[0], 10);
  const m = parseInt(comp[1], 10);
  const d = parseInt(comp[2], 10);
  const jsMonth = m - 1;
  const date = new Date(y, jsMonth, d);
  const isStringValid = dobPattern.test(control.value);
  const isDateValid = (date.getFullYear() === y && date.getMonth()  === jsMonth && date.getDate() === d);

  return (isStringValid && isDateValid) ? null : { invalidDob: ('Date of birth not valid') };

};

new html with 3 fields year has a validator that checks the year day has a validator that checks if the input is between 1 and 31 month has a validator that checks if the input is between 1 and 12. I want to combine the above input of the three field into a new string and use my original date of birth validator.

     <label>Date of birth :</label>
      <div>
        <div class="col-xs-1">
        <input required type="text" formControlName="day" class="form-control" placeholder="dd" id="day"/>
        <p *ngIf="form.controls.day.dirty && form.controls.day.errors">{{ form.controls.day.errors.invalidDay }}</p>
        </div>

        <div class="col-xs-1">
        <input required type="text" formControlName="month" class="form-control" placeholder="mm" id="month"/>
         <p *ngIf="form.controls.month.dirty && form.controls.month.errors">{{ form.controls.month.errors.invalidMonth }}</p>
        </div>

        <div class="col-xs-2">
        <input required type="text" formControlName="year" class="form-control" placeholder="yyyy" id="year"/>
         <p *ngIf="form.controls.year.dirty && form.controls.year.errors">{{ form.controls.year.errors.invalidYear }}</p>
        </div>
    </div>


    <div>
        <button type="submit" [disabled]="form.invalid">Submit</button>
    </di>

Solution

  • I have created a validator for comparing two dates (their format is NgbDateStruct - as used in ng-bootstrap package's datepickers)

    import { Directive, forwardRef, Attribute } from '@angular/core';
    import { Validator, AbstractControl, NG_VALIDATORS, ValidatorFn } from '@angular/forms';
    import { NgbDateStruct } from "@ng-bootstrap/ng-bootstrap";
    import { toDate } from "../helpers/toDate";
    
    export function dateCompareValidator(compareToControl: string, compareToValue: NgbDateStruct, compareType: string, reverse: boolean, errorName: string = 'dateCompare'): ValidatorFn {
        return (c: AbstractControl): { [key: string]: any } => {
    
            let compare = function (self: Date, compareTo: Date): any {
                console.log('comparing ', compareType.toLowerCase());
                console.log(self);
                console.log(compareTo);
                if (compareType.toLowerCase() === 'ge') {
                    if (self >= compareTo) {
                        return true;
                    } else {
                        return false;
                    }
                } else if (compareType.toLowerCase() === 'le') {
                    if (self <= compareTo) {
                        return true;
                    } else {
                        return false;
                    }
                } 
    
                return false;
            };
    
            // self value
            let v = c.value;
    
            // compare vlaue
            let compareValue: Date;
            let e;
            if (compareToValue) {
                compareValue = toDate(compareToValue);
            }  else {
                e = c.root.get(compareToControl);
                if (e) {
                    compareValue = toDate(e.value);
                }
                else {
                    // OTHER CONTROL NOT FOUND YET
                    return null;
                }
            }
    
            let controlToValidate: AbstractControl = reverse ? e : c;
    
            // validate and set result
            let error = null;
            let result = compare(toDate(c.value), compareValue);
            if (result === true) {
                console.log('clearing errors', compareToControl);
                if (controlToValidate.errors) {
                    delete controlToValidate.errors[errorName];
                    if (!Object.keys(controlToValidate.errors).length) {
                        controlToValidate.setErrors(null);
                    }
                }
                else {
                    console.log('errors property not found in control', controlToValidate);
                }
            } else {
                error = {};
                error[errorName] = false;
                controlToValidate.setErrors(error);
                console.log(controlToValidate.errors);
                console.log(controlToValidate.value);
                console.log('Error Control', controlToValidate);
                console.log('returning errors');
            }
            return reverse ? null : error;
        }
    }
    

    Couldn't manage to modify much lot to best describe here as an answer but I believe you would get your query answered in this validator function code.

    Note: Function toDate() used in the code is a small function I created to convert NgbDateStruct into a javascript date object so that comparing dates can get easier. Here goes its implementation:

    import { NgbDateStruct } from "@ng-bootstrap/ng-bootstrap"
    
    export function toDate(ngbDate: NgbDateStruct): Date {
        return ngbDate != null ? new Date(Date.UTC(ngbDate.year, ngbDate.month, ngbDate.day)) : null;
    }