5

I have the following snippet of code used to validate password and password again fields in an Angular template driven form. The idea is that if the two fields are different an error is attached to both. If they're the same, no errors are removed from both.

validate(control: AbstractControl): ValidationErrors | null {

    // The current value
    let currentValue = control.value;

    // Control.root exposes the parent element of the directory
    let otherField : AbstractControl = control.root.get(this.compareTo);


    if(currentValue && otherField && currentValue !== otherField.value) {

      otherField.setErrors({"fieldsMismatch": true});
      otherField.markAsTouched();

      setTimeout(() => {
        control.setErrors({"fieldsMismatch" : true});
        control.markAsTouched()}
        )


    } else {
      control.setErrors(null)
      otherField.setErrors(null)
    }


    return null;
  }

1) If I remove the logic to set the current element's (control) error from the setTimeout, it stops working. Why?

2) If I remove errors using

control.setError({"fieldsMismatch": null});
otherField.setError({"fieldsMismatch": null});

The error is removed from both. But, for the current field (control), the errors key is set to null, meaning that .ng-invalid is removed from the input tag. For otherField, errors is set to an empty object, meaning the input is still marked as invalid. Why? I can just set errors to null explicitly, but then if I had other validation that would also be removed.

Both objects are of type AbstractControl, so I don't get what drives the difference.

3
  • a custom validator is a function that "return" null if is valid and another thing if is not valid. NOT mark as touched, NOT setErrors (Angular make this work for us), only return null or something Commented Jun 23, 2019 at 9:35
  • @Eliseo thanks for the clarification, but that doesn't really address my question Commented Jun 23, 2019 at 12:32
  • sorry, I read so rapid your question. If you want to validator a Template driven Form you can use a directive, see my answer bellow Commented Jun 23, 2019 at 13:54

3 Answers 3

4

a better way is to create a form group level for compare two fields at that level you access to the form group and set the error to form it self

check this validator

export function checkMatchValidator(field1: string, field2: string) {
  return function (frm) {
    let field1Value = frm.get(field1).value;
    let field2Value = frm.get(field2).value;

    if (field1Value !== '' && field1Value !== field2Value) {
      return { 'match': `value ${field1Value} is not equal to ${field2Value}` }
    }
    return null;
  }
}

password /confirmPassword form

 form: FormGroup
  constructor(formBuilder: FormBuilder) {

    this.form = formBuilder.group({
      password: ['', Validators.required],
      confirmPassword: ['', Validators.required],
    },
      {
        validator: checkMatchValidator('password', 'confirmPassword')
      }
    );
  }
}

style invalid input base on the form state

.login-form.ng-invalid.ng-touched input {
  border:1px solid red;
  background:rgba(255,0,0,0.2);
}
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks, but that doesn't answer the specific questions I had.
the result is the same 🤔🤔 and it 's more claner than check the form in form control validator
my idea if you want to validator more that one form control state you need to create a validator based on the form not the form control it self and try to access to other controls it 's look not the correct place to do it 🤔
Yes I appreciate that, but my question was I was observing the behaviors I got using my approach.
A simple answer is to use asyncValidationFn instead, that way you get access to control.parent
2

In angular Template driven form, you can use a directive that implements validators (if we needn't pass an argument it's not necesary), see the docs

In this case, we can use some like

@Directive({
  selector: '[repeatControl]',
  providers: [{ provide: NG_VALIDATORS, useExisting: RepeatNameDirective, multi: true }]
})
export class RepeatNameDirective implements Validator {
  @Input('repeatControl') control: NgControl;

  validate(control: AbstractControl): { [key: string]: any } | null {
    let invalid: boolean = this.control.value && control.value && 
            this.control.value != control.value;

    if (this.control.invalid != invalid)
      setTimeout(() => {
        this.control.control.updateValueAndValidity({ emitEvent: false })
      })

    return invalid ? { error: "Must be equal" }
      : null;
  }
}

See that we need pass as argument the control witch we want compare, to allow us to make an updateValueAndValidity (else one control will be invalid, and the other valid or viceverse) and we need put this instruction under a setTimeOut

Our form becomes like

<input id="password" name="password" class="form-control"
      [repeatControl]="repeatPassword"
      [(ngModel)]="data.password" #password="ngModel"  >

<input id="repeatPassword" name="repeatPassword" class="form-control"
      [repeatControl]="password"
      [(ngModel)]="data.repeatPassword" #repeatPassword="ngModel" >

<div *ngIf="repeatPassword.errors || password.errors">
  Password and repeat password must be equals
</div>

See that the inputs is the referenceVariable See the stackblitz (and take account how Angular add the class ng-invalid automatically to both inputs)

2 Comments

Thanks, I get that but if doesn't really address either of my questions that were less about how to achieve the result and more why I am observing the resul I do
@MrD, setErrors need as argument a function, not a string, and the function validate must return null or an object. I don't know about what is your otherFields, and markAsTouched don't validate the control
0

Hope this helps someone: //sync

    export const multipleFieldsValidator = (): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    return control.value > control.parent?.get('another-field-id')?.value ? null : { fieldIsInvalid: true }
  };
};

//async

    export const multipleFieldsValidator = (
  simService: SimulationService
): AsyncValidatorFn => {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return simService.generic(control.value, control.parent?.get('another-field-id')?.value).pipe(
      map((response: any) => {
        
        return !response.isValid ? { fieldIsInvalid: true } : null;
      })
    );
  };
};

1 Comment

There will be situations where control.parent is undefined

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.