Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Linking two fields in an Angular validator

I have an Angular 9 form where four fields are related. One is a checkbox, and the rest are inputs. When the checkbox is checked, the inputs should not be empty, but when it is not checked, it doesn't matter. I want to make validators for this, so that errors appear only when a field is empty and the first field is set to true.

I have also considered creating a local boolean representing the state of the checkmark, and passing this to the validator like so.

export function linkedFieldValidator(toggler: boolean): ValidatorFn {
  console.log('updated');
  return (control: AbstractControl): {[key: string]: any} | null => {
    return (toggler && control.value === '') ? {linkedField: {value: control.value}} : null;
  };
}

...
field: new FormControl('', linkedFieldValidator(this.checkboxvalue)),
...

This doesn't work, however, I imagine since it only passes the value of the boolean once and doesn't update after. Even calling updateValueAndValidity() doesn't work, which is odd to me (if this is not it, then what is its purpose?).

The structure of my FormGroup looks something like this:

this.form = this.formBuilder.group({
  name: new FormControl(''), // don't care
  address: new FormControl(''), // don't care
  car: new FormControl(false), // do care - this is the checkmark
  license_plate: new FormControl('', Validators.pattern(MY_LICENSE_PLATE_REGEX)), // shouldn't be empty when car
  mileage: new FormControl('') // shouldn't be empty when car
  hair: new FormControl(false), // do care - this is the checkmark
  hair_color: new FormControl(''), // shouldn't be empty when hair
});

As you can see, I have a couple of FormControlls through each other and I only want a couple of them to be linked. Another important thing to note is that, while the whole form can be made invalid if one of these conditions is violated, I want to be able to address each error individually so that I can display proper messages in the proper places.

I have no more ideas, can anyone help me? I am using reactive forms.

like image 990
Luctia Avatar asked Oct 25 '25 22:10

Luctia


1 Answers

The problem is that you're passing only the initial value to the linkFieldValidator function.

In order to have the value dynamically, you could pass the linkFieldValidator through the FormGroup, like this:

readonly formGroup = this.formBuilder.group(
  {
    checkbox: '',
    name: ''
  },
  { validator: linkedFieldValidator }
);

Full sample:

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

export const linkedFieldValidator = (formGroup: FormGroup): ValidationErrors | null => {
  const [checkboxFormControlValue, nameFormControlValue] = [
    formGroup.get('checkbox')!.value,
    formGroup.get('name')!.value
  ];

  return checkboxFormControlValue && !nameFormControlValue
    ? { linkedField: { value: nameFormControlValue } }
    : null;
};

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'input-overview-example',
  styleUrls: ['input-overview-example.css'],
  templateUrl: 'input-overview-example.html'
})
export class InputOverviewExample {
  readonly formGroup = this.formBuilder.group(
    {
      checkbox: '',
      name: ''
    },
    { validator: linkedFieldValidator }
  );

  constructor(private readonly formBuilder: FormBuilder) {}
}

DEMO


Edit 1: If you need the error to reside in each form control, you can change your linkedFieldValidator to:

export const linkedFieldValidator = (formGroup: FormGroup): null => {
  const { value: checkboxFormControlValue } = formGroup.get('checkbox')!;
  const inputFormControls = [
    formGroup.get('input1')!,
    formGroup.get('input2')!,
    formGroup.get('input3')!,
  ];

  inputFormControls.forEach(inputFormControl => {
    const { value } = inputFormControl;
    const errors = checkboxFormControlValue && !value ? { linkedField: { value } } : null;
    inputFormControl.setErrors(errors);
  });

  return null;
};

Note that if you need to keep other errors, you may need do some handling before setErrors.

DEMO

Edit 2:

For a generic approach that you can have multiple linked fields, you can do something like this:

type LinkedFormControl = Record<string, string | readonly string[]>;

const arrayify = <T>(itemOrItems: T | readonly T[]): readonly T[] => {
  return Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
};

const getErrorObjectSanitized = <T extends object>(obj: T): T | null => {
  return Object.keys(obj).length === 0 ? null : obj;
};

const getErrorsFor = (
  checkerValue: boolean,
  formControl: FormControl,
): object | null => {
  const { errors, value } = formControl;
  const { error, ...oldErrors } = errors || {};
  const processedErrors = {
    ...(checkerValue && !value ? { error: true } : {}),
    ...oldErrors,
  };

  return getErrorObjectSanitized(processedErrors);
};

export const linkedFieldValidator = (linkedFormControls: LinkedFormControl) => {
  return (formGroup: FormGroup): ValidationErrors | null => {
    Object.keys(linkedFormControls).forEach(key => {
      const { value: checkerValue } = formGroup.get(key)!;
      const dependentKeys = arrayify(linkedFormControls[key]);

      dependentKeys
        .map(dependentKey => formGroup.get(dependentKey)!)
        .forEach((dependentFormControl: FormControl) => {
          dependentFormControl.setErrors(
            getErrorsFor(checkerValue, dependentFormControl),
          );
        });
    });

    return null;
  };
};

... and the call would be like this:

{
  validator: linkedFieldValidator({
    car: ['license_plate', 'mileage'],
    hair: 'hair_color',
  }),
},

DEMO

like image 77
developer033 Avatar answered Oct 27 '25 16:10

developer033



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!