Skip to main content

Command Palette

Search for a command to run...

Detecting Duplicates in Angular FormArray

Updated
3 min read

If you’ve worked with Angular Reactive Forms, you’ve probably built a form that dynamically grows — you click Add, and a new input appears. It’s clean, declarative, and Reactive in the truest sense.

But one fine day, you face a subtle problem:

“How do I make sure no two inputs have the same value?”

That’s where this story begins.

The Setup

Let’s imagine a form where users can add their skills dynamically.

Each skill sits inside a FormGroup, and all such groups live inside a FormArray called userHobby.

userForm = new FormGroup({
  userHobby: new FormArray([], [Validators.required]),
});

When the user clicks Add New Row, we push a new FormGroup into the array:

addNewRow(): void {
  this.userHobbyControl.push(
    new FormGroup({
      skill: new FormControl<string | null>(null, [
        Validators.required,
        this.uniqueSkillValidator(),
      ]),
    })
  );
}

Nothing fancy yet — a normal reactive form with required validation.
The fun starts now.


The Problem — Detecting Duplicates
Let’s say the user adds:

  • “Angular”

  • “React”

  • “Angular”

We want to catch that third input — it shouldn’t be allowed.
You’d think this is simple: just look for duplicates in the array and mark errors. But the challenge here is context.
Each validator in Angular runs in isolation — it only knows about its control, not its siblings.
So how do we make one control’s validator aware of the others?


The Trick — Closing Over the Parent

The key is closure — using a function that captures the parent context (this.userHobbyControl) while validating each control.

uniqueSkillValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    let skills: string[] = [];

    for (
      let index = 0;
      index <= this.userHobbyControl.controls.length;
      index++
    ) {
      const group = this.userHobbyControl.controls[index];
      const skill = group?.get('skill')?.value;
      if (skill) skills.push(skill);
    }

    if (this.hasDuplicates(skills)) {
      return { duplicate: true };
    }

    // Reset errors if no duplicates
    for (let group of this.userHobbyControl.controls) {
      group.get('skill')?.setErrors(null);
    }

    return null;
  };
}

Here, uniqueSkillValidator() creates a validator function that closes over the userHobbyControl FormArray.
This gives it visibility into all sibling controls — and that’s how we make validation context-aware.


Detecting Duplicates — The Set Trick

The check itself is beautifully simple:

hasDuplicates(skills: string[]) {
  const noDups = new Set(skills);
  return skills.length !== noDups.size;
}

This is a common pattern in JavaScript — using a Set to strip duplicates and comparing lengths.


The Validator Lifecycle

Every time the user types in any skill, Angular re-runs its validator.

Here’s what happens under the hood:

  1. Collect all the skill values.

  2. Check if any duplicates exist.

  3. If yes, mark all the duplicates with { duplicate: true }.

  4. If not, clear all duplicate errors.

The form instantly reacts — that’s the reactive in Reactive Forms.


Why This Works Beautifully

This approach looks deceptively simple, but it captures three deep ideas:

  • Validation by closure — validators can be dynamic and context-aware.

  • State synchronization — every keystroke re-validates all controls.

  • Minimal logic, maximum clarity — the heart of good Angular design.


TL;DR

✅ Use FormArray for dynamic forms.
✅ Write a custom validator using closure to access sibling controls.
✅ Use Set to detect duplicates.
✅ Keep your form reactive — let Angular do the rest.


🧩 Try it yourself

You can explore the full working example on StackBlitz here: