How to add error to child form controls in angular without silencing child's own validators

Viewed 2281
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?

4 Answers

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

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);

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.

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();
  }
}
Related