4

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.

Luctia
  • 322
  • 1
  • 5
  • 17

2 Answers2

2

Edit 1:

DEMO with validation on the sub-form (where the inputs are located) and also on the main form

Original answer:

You can use cross-fields validation to check multiple fields in combination. Here I am having 1 checkbox (initial state is false or unchecked) and 3 input fields. In my custom validator I just check the value and set the corresponding form validation errors:

DEMO

export class ProfileEditorComponent {
  myForm = new FormGroup({
    'checker': new FormControl(false),
    'name': new FormControl(),
    'middleName': new FormControl(),
    'lastName': new FormControl()
}, { validators: myCustomValidator });

  constructor(private fb: FormBuilder) { }
}

export const myCustomValidator : ValidatorFn = (control: FormGroup): ValidationErrors | null => {
  const checker = control.get('checker').value;
  const name = control.get('name');
  if (checker) {
    if (name.value===null || name.value==="") {
      return {'firstNameMissing': true};
      // TODO: do the same for the other fields or any field combination
    } else {
      return null;
    }
  }
  return null;
};

Then the showing of the error messages is done as follows:

  <label>
    First Name:
    <input type="text" formControlName="name">
  </label>
  <div *ngIf="myForm.errors?.firstNameMissing && (myForm.touched || myForm.dirty)" class="cross-validation-error-message alert alert-danger">
    First name required.
  </div>
Anton Sarov
  • 3,712
  • 3
  • 31
  • 48
  • This solution seems to get me very close to to something, I think I'm just missing one thing - how can I use this in nested FormGroups? Everything seems to be working, but in the template, `ngIf` doesn't seem to be able to read `myForm.get('subFormGroup').errors...`. I don't get errors, but I can see in logs that the group has thrown the `firstNameMissing` error, and it's not triggering the `ngIf`. Any suggestions? – Luctia May 14 '20 at 20:28
  • @Luctia So you want to have the validation being performed only in the sub-group. Where is the checkbox located - in the main or in the sub-group? – Anton Sarov May 14 '20 at 21:15
  • I have updated my question with a better representation of the structure in my `FormGroup`, I hope you can deduce from that what I mean. I have already accepted an answer though, so only put more time into this if you think the current answer is not right. Thank you for your help! – Luctia May 14 '20 at 21:32
  • 1
    All is fine, now I get what you want to achieve and I think both solutions go into the same direction. Good luck! – Anton Sarov May 14 '20 at 21:33
2

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

developer033
  • 24,267
  • 8
  • 82
  • 108
  • I've seen this possibility, but as I mentioned in my post, this doesn't really work for me since I want to "link" one checkbox to three inputs. As far as I can tell, this turns the whole `FormGroup` invalid, which is not what I want; only the fields that are empty (when the checkbox is checked) should be invalid. – Luctia May 14 '20 at 19:54
  • Perhaps your post isn't clear at all. Could you create a stackblitz demonstrating the problem? – developer033 May 14 '20 at 19:58
  • @Luctia "_As far as I can tell, this turns the whole FormGroup invalid, which is not what I want; only the fields that are empty (when the checkbox is checked) should be invalid._" I forgot to quote your sentence before, but it worth to say something about this: if **any** of the `AbstractControl` present in `FormGroup` is invalid, the `FormGroup` is **also** invalid. So, you can't mark **only** a specific `AbstractControl` as invalid without marking, automatically, **the** `FormGroup`. – developer033 May 14 '20 at 20:47
  • That being said, I'm trying to deduce what you're trying to accomplish with your question and I came up with [**this**](https://stackblitz.com/edit/angular-yawjtb-wz5sds). Can you please take a look? – developer033 May 14 '20 at 20:47
  • that example works for me, I think. I have more than one of these cases in my group, but I think I can work with that. One more note on your previous comment, I think I had a wrong idea of what that meant for a while, and while struggling with this issue, I have started to understand what that means, and you're right, it might've been good to phrase that differently. I'm going to take another look at my question, if you add what you came up with to your answer I'll mark it as answered. Thanks for your help! – Luctia May 14 '20 at 21:20
  • I've also updated the question, if you want to take a look and let me know if what I want is not possible with my current structure, that would be appreciated :P I think it should be possible, though. – Luctia May 14 '20 at 21:29
  • @Luctia I'll take a look. Gimme some minutes :) – developer033 May 14 '20 at 21:44
  • One more question: this seems to mess with existing errors, like `Validators.pattern()`. I have my declarations as in my question, and let's say I have added a pattern validator to my license plate. After implementing your suggestion, the field's validity doesn't rely on that anymore. Can a group-wide validator and a control-specific validator not co-exist? – Luctia May 14 '20 at 22:02
  • Ah, that's a known problem I've faced many times in my apps. I edited the answer explaining it. Btw, you can check the [**old issue**](https://github.com/angular/angular/issues/17090) and [**old PR**](https://github.com/angular/angular/pull/28976) to make it more friendly. – developer033 May 14 '20 at 22:05
  • @Luctia I updated the answer again :) Note that someone could came up with this solution much earlier than now if you explained that in your question. Please, always include all relevant details :D – developer033 May 14 '20 at 22:53
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/213964/discussion-between-luctia-and-developer033). – Luctia May 15 '20 at 20:14