5

I am trying to figure out the best way to implement cross field validation in Angular.

For example, I have a select field that makes another field mandatory.

I want to be able to:

  • Change the border color of the field if it is invalid
  • Display a * in front of the field whenever it becomes mandatory
  • Display a specific error message that explains what validation rule is broken.

So far, I came up with three solutions but they don't feel so convincing to me.

  • Listen to select field changes and update second field's validators.
  • Listen to both fields changes and manually perform setErrors
  • Lift validation to formGroup (which can feel extremely cumbersome since, validation state is now stored in formGroup and not directly available in formControl).

Here is a Stackblitz implementation that demos my investigations.

3
  • 1
    Does it work though? Commented Jul 20, 2019 at 0:19
  • All of them do work, but none feels right. This is why I am asking the community's advice. By the way, I don't understand why I got downvoted. Commented Jul 20, 2019 at 6:28
  • 1
    Angular's preferred way to perform cross-validation is to lift validation up (angular.io/guide/form-validation#cross-field-validation), i.e. the third option. Agreed on that it's not quite convenient since the errors are not reflected in the relevant child inputs, however it definitely beats everything else in terms of performance and code complexity. Commented Mar 16, 2021 at 15:24

4 Answers 4

20

UPDATE - ANOTHER APPROACH See this SO

UPDATE - A BETTER APPROACH

Create the customValidator over the form and use the validator to use setError to the control required. Using setError, make that Angular adds ng-invalid for us, ad we needn't subscribe to value change. See:

form: FormGroup = new FormGroup(
  {
    input1: new FormControl('optional'),
    input2: new FormControl(null),
  },
  { validators: this.customValidatorForm() },
);

customValidatorForm() {
  return (form: FormGroup) => {
    const error =
      form.get('input1').value != 'optional' && !form.get('input2').value
        ? { required: true }
        : null;
    form.get('input2').setErrors(error); //<--see the setErrors
    return error;
  };
}

See stackblitz

OLD ANSWER

Just use a customValidator like:

form: FormGroup = new FormGroup({
  input1: new FormControl('optional'),
  input2: new FormControl(null, this.customValidator()),
});

customValidator() {
  return (control: any) => {
    if (!control.parent) return null;

    let mandatory = control.parent.get('input1').value;
    return mandatory != 'optional' && !control.value ? { required: true } : null;
  };
}

Another option for not ask for control.parent it's use .bind(this). This allow us have inside the validator to all the variables of our component, and of course access to this.form:

form: FormGroup = new FormGroup({
  input1: new FormControl('optional'),
  input2: new FormControl(null, this.customValidator().bind(this)), //<--bind(this)
});

customValidatorBind() {
  return (control: any) => {
    if (!this.form) return null;

    let mandatory = this.form.get('input1').value;
    return mandatory != 'optional' && !control.value ? { required: true } : null;
  };
}

Well, as we want that when change input1 input2 was checked, you need use, after create the form subscribe to valueChanges:

this.form.get('input1').valueChanges.subscribe(() => {
  this.form.get('input2').updateValueAndValidity();
});
Sign up to request clarification or add additional context in comments.

9 Comments

I have to admit, your solution has one major advantage over replacing Validators. It preserves other validators that could exist on the field, whereas replacing validators requires to determine the other validators to keep.
@Heyjojo, I updated my answer, creating a customValidator over the form and use setError in the validator. I'm sure this aproach like you much more than the others becaouse you don't need susbcribe to values changes
I don't like this solution however. 1/ The validator gets triggered too many times 2/ It uses formGroup validator to trigger side effects (which is kind of a hack in my opinion).
You can use {updateOn:'blur'} or { updateOn: 'submit' }, see angular.io/guide/form-validation#note-on-performance
@CodeNameJack, Really it's a bit old answer (and I'm not very proud to the result). For me a better approach is in this SO
|
3

For cross field validation, you can use required validation of @rxweb/reactive-form-validation.

You just have to mention conditionalExpression in your formControl like this:

input2:['', RxwebValidators.required({conditionalExpression:'x => x.input1 == "mandatory"' })]

and set the error message in your app.component.ts like this

ngOnInit(){
    ReactiveFormConfig.set({"validationMessage":{"required":"This field is required"}});
  }

Here is your complete component code:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from "@angular/forms"
import { RxwebValidators } from '@rxweb/reactive-form-validators';

@Component({
    selector: 'app-required-conditionalExpression-validator',
    templateUrl: './required-conditional-expression.component.html'
})
export class RequiredConditionalExpressionValidatorComponent implements OnInit {
    userFormGroup: FormGroup

    constructor(
        private formBuilder: FormBuilder )
    { }

    ngOnInit() {
        this.userFormGroup = this.formBuilder.group({
            input1:[''], 
            input2:['', RxwebValidators.required({conditionalExpression:'x => x.input1 == "mandatory"' })], 
        });
    }
}

Here is your Complete HTML Code:

<div>
    <form [formGroup]="userFormGroup">

        <div>
            <label>Mandatory/Optional </label>
        <select formControlName="input1">
          <option value="optional">Optional</option>
          <option value="mandatory">Mandatory</option>
        </select>
      <br/><br/>
    </div>
    <div>
      <label>Input2</label>
      <input type="text" formControlName="input2"/><br/>
      <span>
        {{userFormGroup.controls.input2.errors?.required?.message}}
      </span>
    </div>
  </form>
</div>

Here is the Working Example

2 Comments

I just checked this library's code. I am really not fan of all this. There is a lot of magic involved. Plus it seems, it can only to go up one formGroup level, which can be restrictive.
@Heyjojo No problem, this also works with nested formgroup or formarray level as well.
2

Based on the "better approach" on this comment: https://stackoverflow.com/a/57123631/8126632

I would edit it this way to prevent any other validation to be overwritten:

input2: new FormControl('', [Validators.minLength(4)]),

form.get('input2').setErrors({...error, ...(form.get('input2').errors)});

Otherwise a perfect answer.

Comments

0

I also struggled with cross-field validation, and I recently came up with a fourth option: defining validators directly on the form control itself.

For instance, we can create a custom sameAs validator function that accepts a path as its parameter, and use FormControl.root together with that parameter to locate the other form controls:

type T = FormGroup<{
  password: FormControl<string>;
  confirmPassword: FormControl<string>;
}>;

const formGroup = new FormGroup<T['controls']>({
  password: new FormControl('', {
    nonNullable: true,
    validators: [Validators.required],
  }),
  confirmPassword: new FormControl('', {
    nonNullable: true,
    validators: [Validators.required, ExtraValidators.sameAs('password')],
                                   // ^ here is the magic!
  }),
});

This approach also works well with other scenarios, such as requiring one input’s value to be within the range of two other inputs.

However, you still need to manually trigger updateValueAndValidity when the password value changes (You could also pass an injector to get a DestroyRef and subscribe to password's valueChange instead of manually invoke updateValueAndValidity everywhere):

<input
  [formControl]="formGroup.controls.password"
  (input)="formGroup.controls.confirmPassword.updateValueAndValidity()"
/>

If you find this solution helpful, you may also take a look at https://soc221b.github.io/ngx-cross-field-validation, which includes more scenarios you might encounter and provides utilities that make this approach easier to implement.

Hope this helps. :)

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.