Check all form fields for a unique value

61 Views Asked by At

I have a formarray consisting of a formgroup. How can I check that the fields inside the created formgroups are unique.

I have this form:

form: FormGroup = this.formBuilder.group({
  fields: this.formBuilder.array([]),
});

private createField() {
  return this.formBuilder.group({
    label: ['', [Validators.required, uniqueLabelsValidator()]],
  });
}

public addField() {
  (this.form.get('fields') as FormArray).push(this.createField());
}

How can I check that all "label" inside "fields" are unique and if this is not the case, then assign a validation error to a field with a duplicate value

3

There are 3 best solutions below

0
Yong Shun On

You may consider using the @rxweb/reactive-form-validators library.

Reference: Unique Validation (Note: The link also provides the code implementation for unique values without RxWeb validators)

  1. Import RxReactiveFormsModule into the app module or standalone component.
import {
  RxReactiveFormsModule,
  RxwebValidators,
} from '@rxweb/reactive-form-validators';
  1. Add RxwebValidators.unique() validation with message (optional) into the form control.
private createField() {
  return this.formBuilder.group({
    label: ['', [Validators.required, RxwebValidators.unique({ message: "Label must be unique." })]],
  });
}
  1. Render the unique validation message in HTML.
<small
  class="form-text text-danger"
  *ngIf="getField(i).controls['label'].errors"
>
  {{getField(i).controls["label"].errors?.["unique"]?.message}}
</small>

Demo @ StackBlitz

0
aaadraniki On

this validator returns null, but set errors on control with duplicated value

import { ValidatorFn, AbstractControl, ValidationErrors, FormArray } from '@angular/forms';

export function uniqueLabelsValidator(fieldName: string): ValidatorFn {
  return (formArray: AbstractControl): ValidationErrors | null => {
    const labels = new Set<string>();

    if (formArray instanceof FormArray) {
      formArray.controls.forEach((control) => {
        const labelControl = control.get(fieldName);
        const label = labelControl.value as string;

        if (!labelControl.hasError('required')) {
          if (labels.has(label)) {
            labelControl.setErrors({ duplicateLabel: true });
          } else {
            labels.add(label);
            if (labelControl.hasError('duplicateLabel')) {
              labelControl.setErrors(null);
            }
          }
        }
      });
    }

    return null;
  };
}

and then apply this custom validator

form: FormGroup = this.formBuilder.group({
  fields: this.formBuilder.array([], [uniqueLabelsValidator('label')]),
});
0
Eliseo On

Your formArray is a FormArray of FormGroups and you can check one FormControl of the FormGroup

(If your formArray would be a FromArray of FormControls you sould create a validators over the "formArray")

When we have a validator over a control, we can access to the parent and parent of the parent. In your case the parent of the component is the formGroup and the parent of the parent is the formArray

The problem when we add a validator to a FormControl that depends from ohers if that, in anyway, we need also check the control when we change the others

I'll try explain in comments, be sure understand, not only copy the code

  uniqueLabelsValidator() {
    return (control: AbstractControl) => {
      //only check if the control has value
      if (control.value) {
        //get the formGroup
        const group = control?.parent as AbstractControl;

        //get the FormArray
        const array = control?.parent?.parent as FormArray;

        if (array) {
          //we get the "index" of the formGroup
          //e.g. if we are changing the second "label" 
          //the group is the second formGroup (index=1)

          const index = array.controls.findIndex(
            (x: AbstractControl) => x == group
          );

          //we updateAndValidity all the "labels" after the index
          //in the e.g. the labels in third, or fourth place

          setTimeout(()=>{
            array.controls.forEach((group:AbstractControl,i:number)=>{
              i>index && group.get('label')?.updateValueAndValidity()
            })
  
          })

          //we get the values of the labels in an array

          const values = array.value.map((group: any) => group.label);

          //if we find one value berofe the "label" we are changing
          if (
            values.find((x: any, i: number) => x == control.value && i < index)
          )
            return { duplicate: true };
        }
      }
      return null;
    };
  }

We can check this stackblitz

UPDATE The before validator only mark as error the "duplicate", not both. If you want to mark both replace the i<index and i>index by i!=index

uniqueLabelsValidator() {
    return (control: AbstractControl) => {
      ...
          setTimeout(()=>{
            array.controls.forEach((group:AbstractControl,i:number)=>{
              i!=index && group.get('label')?.updateValueAndValidity()
            })
  
          })
          ...
          if (
            values.find((x: any, i: number) => x == control.value && i != index)
          )
            return { duplicate: true };
        }
      }
      ...
    };
  }