4

I'm validating a form for a call center where usually fields will be filled in a specific order. I want to raise an error for multiple fields if the user skips ahead. I've found that the below works:

  export const recordValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {

    if(!firstField.value && !secondField.Value && !thirdField.value)    
    {
      firstField.setErrors({ "firstFieldError" : true});
      secondField.setErrors({ "secondFieldError" : true});

      return {"firstFieldError" : true, "secondFieldError" : true};

    }
  }

And both firstField and secondField show errors correctly.

Now according to the docs ValidationErrors is just a map of the errors. But it obviously doesn't have any methods, so I thought I'd just cast an existing map to ValidationErrors and return that:

  export const recordValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {

    if(!firstField.value && !secondField.Value && !thirdField.value)    
    {
      firstField.setErrors({ "firstFieldError" : true});
      secondField.setErrors({ "secondFieldError" : true});

      let errorMap = new Map();

      errorMap.set("firstFieldError",true);
      errorMap.set("secondFieldError",true);

      let errorValidators:ValidationErrors = errorMap;

      return errorValidators;

    }
  }

but it doesn't raise any errors.

My template looks like this:

<mat-form-field>
  <input formControlName="firstField" type="datetime-local" placeholder="First Field" [errorStateMatcher]="errorMatcher" matInput>                        
      <mat-error *ngIf="Form.errors?.firstFieldError">
      First field error!
      </mat-error>
</mat-form-field>

Can anyone help me to see why the first one works and the second doesn't

3
  • Because it's not a Map. It's an object, with properties. And your validator is wrong, too. It's not spposed to set errors. It's supposed to return an object telling if the validated form group or the validated form control has errors (and which ones). Angular then sets the group or control errors and status based on what the validator has returned. Commented Jan 21, 2020 at 7:11
  • Ok, that makes sense. So what's the best practice for setting errors on multiple fields? Should each control have its own validator? Commented Jan 21, 2020 at 7:22
  • 1
    If a control's validation depends only on its own value, then it should have its own validator. If it depends on other controls, then they should be in a form group, and the validator should be set on the form group. Commented Jan 21, 2020 at 8:11

2 Answers 2

3

Jim, a custom Validator don't work as you say. You need return an object (or null). So your valitadion must like some like

export const recordValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
    let invalid=false
    const error={}
    if (!control.value.firstField && control.value.secondField)
    {
        error.firstFieldError=true;
        invalid=true;
    }
    if (!control.value.secondField && control.value.thirdField)
    {
        error.secondFieldError=true;
        invalid=true;
    }
    return invalid?error:null;
  }

See how we get value from "control" -it's the formGroup- and how we create an object with one or two properties -you can see in your .html if you write to check-

{{form.errors|json}}

NOTE: Really I don't understand your validator and imagine one taking acount the description in your question

Sign up to request clarification or add additional context in comments.

Comments

3

I've had the need to come back to this one and managed to solve it properly, with the help of this blog and this reference from angular.io.

Here's a validator that returns a map of validation errors:

import { Injectable } from '@angular/core';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

@Injectable({ providedIn: 'root' })
export class ShiftTimeValidator {
    constructor() {}

    //validate(time: string, shifts: VehicleShift[]): ValidatorFn {
    validate(): ValidatorFn {

        return (
            control: AbstractControl,
        ): ValidationErrors => {

            let errors:ValidationErrors = {};

            // If the form hasn't been touched then don't validate
            if (control.pristine) {
                null;
            }

            // Check if the time falls inside any existing shifts
            if(1 == 1){
                errors["inside-other-shift"] = true;
            }

            // If the time is the start time, then check that it's before the end time
            if(1 == 1){
                errors["start-before-end"] = true;
            }

            // If the time is an end time, check that it's after the start time.
            if(1 == 1){
                errors["end-before-start"] = true;
            }

            // If the this time has an existing match, check that the new shift doesn't overlap any other shifts.
            if(1 == 1){
                errors["shift-overlap"] = true;
            }

            console.log(errors);


            return errors;
        };
    }
}

Here's how to add the validator to your form:

return this.fb.group({
  vehicleShiftId: [],
  vehicleId: [this.vehicle.vehicleId, Validators.required],
  shiftStartTimeString: [, [Validators.required, this.shiftValidator.validate()]],
  shiftEndTimeString: [, Validators.required, this.shiftValidator.validate()],
  vehicleStaff: staffArray
});

And here is how you can display the error messages:

<mat-form-field>
    <input formControlName="shiftStartTimeString" type="time"
    name="shiftStartTime"
    placeholder="Shift start time" [errorStateMatcher]="errorMatcher" matInput>

        <mat-error *ngIf="shiftStartTime?.hasError('inside-other-shift')">
        Shift start time is inside another shift.
        </mat-error>

        <mat-error *ngIf="shiftStartTime?.hasError('start-before-end')">
        Shift start time is after shift end time.
        </mat-error>

        <mat-error *ngIf="shiftStartTime?.hasError('end-before-start')">
        Shift end time is before shift start time.
        </mat-error>

        <mat-error *ngIf="shiftStartTime?.hasError('shift-overlap')">
        This shift overlaps another shift.
        </mat-error>
</mat-form-field>

Whether you should do this in favour of creating individual validators for each thing you're trying to validate is probably up for discussion, but this proves it is possible to properly return multiple errors from a single validator.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.