10

I created a custom validator to validate uniqueness in my FormArray. I want to show error when specific(s) value(s) is/are already in array.

The problem is that it isn't working as expected.

Actual behavior:

Steps to reproduce:

  • Add 3 "inputs" - address;
  • Fill input 1;
  • Fill input 2 with different value;
  • Fill input 3 with the same value of input 1; (no errors appear, neither in input 1 nor in input 3)

Expected behavior:

If the same values appears in "X groups", their specific inputs must show the error.

In the case described above the errors should appear on input 1 and 3.

Supposing that I have 4 inputs:

  1. value: stack
  2. value: overflow
  3. value: stack
  4. value: overflow

The 4 inputs must show an error, because all of them are duplicates.


static uniqueBy = (field: string, caseSensitive = true): ValidatorFn => {
  return (formArray: FormArray): { [key: string]: boolean } => {
    const controls = formArray.controls.filter(formGroup => {
      return isPresent(formGroup.get(field).value);
    });
    const uniqueObj = { uniqueBy: true };
    let found = false;

    if (controls.length > 1) {
      for (let i = 0; i < controls.length; i++) {
        const formGroup = controls[i];
        const mainControl = formGroup.get(field);
        const val = mainControl.value;    
        const mainValue = caseSensitive ? val.toLowerCase() :  val;

        controls.forEach((group, index) => {
          if (i === index) {
            // Same group
            return;
          }

          const currControl = group.get(field);
          const tempValue = currControl.value;
          const currValue = caseSensitive ? tempValue.toLowerCase() : tempValue;
          let newErrors;

          if ( mainValue === currValue) {
            if (isBlank(currControl.errors)) {
              newErrors = uniqueObj;
            } else {
              newErrors = Object.assign(currControl.errors, uniqueObj);
            }

            found = true;
          } else {
            newErrors = currControl.errors;

            if (isPresent(newErrors)) {
              // delete uniqueBy error
              delete newErrors['uniqueBy'];

              if (isBlank(newErrors)) {
                // {} to undefined/null
                newErrors = null;
              }
            }
          }

          // Add specific errors based on condition
          currControl.setErrors(newErrors);
        });
      }

      if (found) {
        // Set errors to whole formArray
        return uniqueObj;
      }
    }

    // Clean errors
    return null;
  };
}

You can check it here DEMO.

3
  • you can try with ui-validate library Commented Apr 24, 2017 at 6:20
  • 2
    Try something like this plnkr.co/edit/legvjz4vVQDIotr633KX?p=preview Commented Apr 24, 2017 at 10:33
  • @yurzui Thanks for your demo. It seems it's working perfectly. Can you add it as answer so I can check it? Also, if you don't mind you could provide some explanation on what you did to make it work. Commented Apr 24, 2017 at 14:40

2 Answers 2

10

In your code is used nested for loop where you are interleaving errors.

Here is how validation state looks for each iteration:

  0      [null, "{"uniqueBy":true}", null]

  1      ["{"uniqueBy":true}", "{"uniqueBy":true}", null]

  2      [null, "{}", null]

http://plnkr.co/edit/MTjzQ9KiJHJ56DVAZ155?p=preview (Add three addresses and observe output)

In code below i am clearing errors only once before for loop statement and don't delete errors anymore.

controls.map(formGroup => formGroup.get(field)).forEach(x => x.errors && delete x.errors['uniqueBy']);
for (let i: number = 0; i < controls.length; i++) {
    const formGroup: FormGroup = controls[i] as FormGroup;
    const mainControl: AbstractControl = formGroup.get(field);
    const val: string = mainControl.value;

    const mainValue: string = caseSensitive ? val.toLowerCase() :  val;
    controls.forEach((group: FormGroup, index: number) => {
        if (i === index) {
            return;
        }

        const currControl: any = group.get(field);
        const tempValue: string = currControl.value;
        const currValue: string = caseSensitive ? tempValue.toLowerCase() : tempValue;
        let newErrors: any;

        if ( mainValue === currValue) {
            if (isBlank(currControl.errors)) {
                newErrors = uniqueObj;
            } else {
                newErrors = Object.assign(currControl.errors, uniqueObj);
            }
            currControl.setErrors(newErrors);
            find = true;
        }
    });
}

Plunker Example

Sign up to request clarification or add additional context in comments.

Comments

8

I couldn't add a comment so I'm just putting this as a new answer. I modified yurzui's plunker a little as the form was staying invalid after correcting a field with a duplicate value.

How to reproduce error in yurzui's plunker:

  1. Fill in name correctly
  2. Fill in street with value 'x'
  3. Add a new address with a street value of 'x'
  4. Change the second street to something other than 'x'

The form is still invalid because the first 'street' FormControl has errors set to '{}' instead of null. I just noticed that dev_054 did it in his original post but anyway, here is a plunker with the changes: http://plnkr.co/edit/nQeQ01fjnTlcp3FgEnWL?p=preview

code was:

controls.map(formGroup => formGroup.get(field)).forEach(x => x.errors && delete x.errors['uniqueBy']);

code is now:

controls.map(formGroup => formGroup.get(field)).forEach(x => {
    if (x.errors) {
        delete x.errors['unique-by'];
        if (isBlank(x.errors)) {
            x.setErrors(null);
        }
    }
});

In the plunker I also fixed the way the parameter caseSensitive was interpreted. It'll work as excepted now.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.