5
this.form = this.fb.array([
  this.fb.group({ 
    username: [null, Validators.required]
  }),
  this.fb.group({ 
    username: [null, Validators.required]
  }),
  ...
], uniqueUsernameValidator)

const uniqueUsernameValidator = control => {
  // find duplicate
  
  // when flagging the error
  control.get('0.username').setErrors(ifThereIsAnError) // where `ifThereIsAnError` could be null
}

Somehow uniqueUsernameValidator is silencing the required validation specified by child form fields. How to solve this problem?

user2167582
  • 5,986
  • 13
  • 64
  • 121
  • NOT use setErrors. your custom validators must return null if all is ok and an object if there are duplicate. You can has: this.form.errors, this.form.at(0).get('userName').errors and/or this.form.at(1).get('userName').errors – Eliseo Sep 22 '20 at 20:43

4 Answers4

2

Try below approach. See below Link on Stackblitz

const uniqueUsernameValidator = (control: FormArray) => {
  let nonUnique = (control.value.map(({username}) => username) as any[]).filter((v,i,a) => 
    a.indexOf(v) !== i)
    if(nonUnique.length > 0) {
      return {nonUnique}
    }
  return null
}

This will validate that the username provided in a by user are all unique

Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
2

answering the more general problem of setting errors without possibly silencing errors from other validators:

// Add - retaining existing
const errors = {
    ...control.errors,
    myError: myValue
  };
control.setErrors(errors);

// Remove - only the one we care about
const errors = { ...control.errors };
delete errors.myError;
control.setErrors(Object.keys(errors).length ? errors : null);
Craig
  • 1,648
  • 1
  • 20
  • 45
  • as others have said though, whether you want to set errors this way is debatable. The intended use is to return an object or null I think, but in one case I needed to interact with 2 controls so used a formgroup validator instead, and I needed to surface errors in the UI which for me made sense to put the errors on the controls rather than the form. So in my opinion there are use cases for calling setErrors from within a validator - although rarer. – Craig Sep 29 '20 at 13:07
  • Thank you man ! I was looking for your syntax to add an element to the map of errors without overwriting the other ones (and yes, as you mentionned in your comment : my validator uses multiple controls of my form, to check consistency, so it seems to be the correct way to do this in that case). Surprisingly, it was difficult to find someone who does this. – StøcciD Aug 02 '22 at 15:32
0

When creating a custom validator for a control, you should implemente the ValidatorFn interface:

interface ValidatorFn {
  (control: AbstractControl): ValidationErrors | null
}

The implementation could look like this:

const uniqueUsernameValidator = control => {
  const ifThereIsAnError = /* your logic .. find duplicate */
  
  if(!ifThereIsAnError) {
    return null
  }

  return { unique: true  } 

}

In your case, I think you meant setErrors and not setError? When using the errors setter of the AbstractControl you are overriding all of the errors with setErrors. If you want to keep your solution and not using a ValidatorFn, you should get the errors before setting a new error:

const uniqueUsernameValidator = control => {
  const errors: ValidationErrors = this.control.errors;
  const keyNameOfYourError = 'unique';

  // reset the last error before revalidating
  if (errors && errors[keyNameOfYourError]) {
    delete errors[keyNameOfYourError];
  }

  // ifThereIsAnError = execute your logic ... find duplicate
  if (ifThereIsAnError) {
    errors[keyNameOfYourError] = true;
    this.control.setErrors(errors);
  }
}

But I recommend you to use the implementation ValidatorFn.

acorbeil
  • 141
  • 1
  • 4
0

I would solve it like this:

https://stackblitz.com/edit/form-array-angular-ax9vna?file=src%2Fapp%2Fapp.component.ts

my tips and tricks

  1. I like when the validation is there where the error should show. So the control itself is handling its errors property.

  2. make sure your controls know when something changed

    this.sub = this.usernamesArr.valueChanges.subscribe(_ => updateControls(this.usernamesArr) );

  3. we need to access the formArray-value in our username controls. By binding this we will have access to it.

    uniqueInArray = uniqueInArray.bind(this);

(copied code from stackblitz)

html

<div [formGroup]="usernamesArr"
  *ngFor="let item of usernamesArr.controls; let i = index;">
  <div [formGroupName]="i">
    <input formControlName="username" [placeholder]="'username' + i">
    
    <pre style="color: red">
      {{usernamesArr.controls[i].controls.username.errors | json}}
    </pre>

  </div>
</div>

<button (click)="addItem()">Add</button>

ts

import { Component } from "@angular/core";
import {
  FormBuilder,
  FormGroup,
  FormArray,
  FormControl,
  Validators,
  AbstractControl
} from "@angular/forms";
import { Subscription } from "rxjs";

const uniqueInArray = function(
  control: AbstractControl
): { uniqueInArray: string } | null {

  const value = control.value;
  const arr = this.usernamesArr.value as { username: string }[];
  if (value == null || value.length === 0 || arr == null || arr.length < 2) {
    return null;
  }

  return arr.map(_ => _.username).filter(_ => _ == value).length > 1
    ? { uniqueInArray: `${value} already exists.` }
    : null;
};

const updateControls = function(control: FormArray): null {

  control.controls.map((c: FormGroup) =>
    c.controls.username.updateValueAndValidity({
      onlySelf: true,
      emitEvent: false
    })
  );
  return null;
};

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {

  sub: Subscription;

  usernamesArr: FormArray;

  uniqueInArray = uniqueInArray.bind(this);

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.usernamesArr = new FormArray([]);

    this.sub = this.usernamesArr.valueChanges.subscribe(_ =>
      updateControls(this.usernamesArr)
    );
  }

  createItem(): AbstractControl {
    return new FormGroup({
      username: new FormControl("", [Validators.required, this.uniqueInArray])
    });
  }

  addItem(): void {
    this.usernamesArr.push(this.createItem());
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}
Andre Elrico
  • 10,956
  • 6
  • 50
  • 69