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:
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:
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.
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;
}
});
}
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:
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.