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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With