Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to validate that 2 folders on the same level cannot have the same name in a recursive Angular form array

I'm not sure if my question title is clear enough but I will try to give more details. I'm trying to create a folder hierarchy form using angular forms. The form can have unlimited nesting. My problem is that now I can add 2 folders with the same name on a certain level but this should not be possible and should warn the user. This is logical because in a normal file system 2 folders cannot have same name

I present a simplified version here for clarity. still a bit long to read but here is reproducible demo in stackblitz with same code

form component

@Component({
  selector: 'my-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css'],
})
export class FormComponent implements OnInit {
  myForm!: FormGroup;
  isHierarchyVisible: boolean = false;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      folderHierarchy: this.formBuilder.array([]),
    });
    if (this.folderHierarchy.length === 0) this.isHierarchyVisible = false;
  }

  removeFolder(index: number): void {
    this.folderHierarchy.removeAt(index);
    if (this.folderHierarchy.length === 0) this.isHierarchyVisible = false;
  }

  addFolder(): void {
    this.folderHierarchy.push(
      this.formBuilder.group({
        name: [null, [Validators.required]],
        subFolders: this.formBuilder.array([]),
        level: 0,
      })
    );
    this.isHierarchyVisible = true;
  }

  getForm(control: AbstractControl): FormGroup {
    return control as FormGroup;
  }

  get folderHierarchy(): FormArray {
    return this.myForm.get('folderHierarchy') as FormArray;
  }
}
<p>folder form. type in form name and press enter</p>
<form [formGroup]="myForm">
  <div formArrayName="folderHierarchy">
    <label for="folderHierarchy">create folder</label>
    <div>
      <button type="button" class="btn btn-custom rounded-corners btn-circle mb-2" (click)="addFolder()" [disabled]="!folderHierarchy.valid">
        Add
      </button>
      <span class="pl-1">new folder</span>
    </div>
    <div>
      <div *ngIf="!folderHierarchy.valid" class="folder-hierarchy-error">invalid folder hierarchy</div>
      <div class="folderContainer">
        <div>
          <div *ngFor="let folder of folderHierarchy.controls; let i = index" [formGroupName]="i">
            <folder-hierarchy (remove)="removeFolder(i)" [folder]="getForm(folder)" [index]="i"></folder-hierarchy>
          </div>
        </div>
      </div>
    </div>
  </div>
</form>

folder-hierarchy component

@Component({
  selector: 'folder-hierarchy',
  templateUrl: './folder-hierarchy.component.html',
  styleUrls: ['./folder-hierarchy.component.css'],
})
export class FolderHierarchyComponent implements OnInit {
  constructor(private formBuilder: FormBuilder) {}
  @Output() remove = new EventEmitter();
  @Input() folder!: FormGroup;
  @Input() index!: number;
  tempName: string = '';

  ngOnInit() {}

  addSubFolder(folder: FormGroup): void {
    (folder.get('subFolders') as FormArray).push(
      this.formBuilder.group({
        name: [null, [Validators.required]],
        subFolders: this.formBuilder.array([]),
        level: folder.value.level + 1,
      })
    );
  }

  getControls(folder: FormGroup): FormGroup[] {
    return (folder.get('subFolders') as FormArray).controls as FormGroup[];
  }

  removeSubFolder(folder: FormGroup, index: number): void {
    (folder.get('subFolders') as FormArray).removeAt(index);
  }

  removeFolder(folder: { value: { subFolders: string | any[] } }): void {
    this.remove.emit(folder);
  }

  disableAdd(folder: { invalid: any }): void {
    return this.folder.invalid || folder.invalid;
  }
  onKeyup(event: KeyboardEvent): void {
    this.tempName = (event.target as HTMLInputElement).value;
  }
  updateName(folder: FormGroup, name: string): void {
    folder.get('name')?.setValue(name);
    if (this.isInvalid(folder)) {
      folder.get('name')?.updateValueAndValidity();
      return;
    }
  }

  isInvalid(folder: FormGroup): boolean {
    return !folder.get('name')?.valid;
  }
}
<div *ngIf="folder" #folderRow class="folder-row">
  <div class="folder-header">
    <div class="folder-name-container">
      <label for="folderName" class="folder-name-label">Name:</label>
      <input #folderName id="folderName" [ngClass]="isInvalid(folder) ? 'invalid-input' : ''" class="folder-name-input" placeholder="Folder Name" type="text" (keyup)="onKeyup($event)" maxlength="50" (keyup.enter)="updateName(folder, $any($event.target).value)" [value]="folder.value.name" autocomplete="off" />
    </div>
    <button type="button" class="btn-remove-folder" (click)="removeFolder(folder)">Remove</button>
    <button type="button" class="btn-add-subfolder" [disabled]="disableAdd(folder)" (click)="addSubFolder(folder)">Add Subfolder</button>
  </div>
  <div *ngIf="folder && folder.value.subFolders.length > 0" class="subfolder-container">
    <div *ngFor="let subFolder of getControls(folder); let i = index" class="subfolder-item">
      <folder-hierarchy (remove)="removeSubFolder(folder, i)" [folder]="subFolder"></folder-hierarchy>
    </div>
  </div>
</div>
like image 391
binga58 Avatar asked Sep 30 '25 10:09

binga58


2 Answers

My answer will be fully working with the Reactive forms.

To summarize your requirements and the work on:

  1. Not allow duplicate names in the same directory.

    1.1. Implement the custom validator function duplicateFolderName based on the provided parentDirectory. We have 2 scenarios:

    1.1.1. The first case is the folder with the first level which itself is the root. We access the folderHierarchy form array (control.parent.parent) to get the same-level object(s).

    1.1.2. The second case is the folder that has the parent. We provide the parentDirectory which contains the subfolders form array and through it to get the same-level object(s).

    1.2. For the FolderHierarchyComponent, you need to implement the parentDirectory @Input decorator to pass the parent form to the component. Note that for first level folder shouldn't provide the value to [parentDirectory].

  2. Perform validation(s) only when pressing Enter key.

    2.1. Angular supports the event that triggers the validation only such as blur, submit, and change (default). For this scenario, add the validateOn: 'blur' to the name control. Next, set the (keyup.enter)="folderName.blur()" to the name control to blur (lose focus) the control when pressing Enter key.

form.component.html

<p>folder form. type in form name and press enter</p>
<form [formGroup]="myForm">
  <div formArrayName="folderHierarchy">
    <label for="folderHierarchy">create folder </label>
    <div>
      <div>
        <button
          type="button"
          class="btn btn-custom rounded-corners btn-circle mb-2"
          (click)="addFolder()"
          [disabled]="!folderHierarchy.valid"
        >
          Add
        </button>
        <span class="pl-1"> new folder</span>
      </div>
      <div>
        <div *ngIf="!folderHierarchy.valid" class="folder-hierarchy-error">
          invalid folder hierarchy
        </div>
        <div class="folderContainer">
          <div>
            <div
              *ngFor="let folder of folderHierarchy.controls; let i = index"
              [formGroupName]="i"
            >
              <folder-hierarchy
                (remove)="removeFolder(i)"
                [folder]="getForm(folder)"
                [index]="i"
              >
              </folder-hierarchy>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</form>

form.component.ts

import { duplicateFolderName } from '../validators/duplicate-folder-name.validator';

export class FormComponent implements OnInit {
  myForm!: FormGroup;
  isHierarchyVisible: boolean = false;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      folderHierarchy: this.formBuilder.array([]),
    });
    if (this.folderHierarchy.length === 0) this.isHierarchyVisible = false;
  }

  removeFolder(index: number): void {
    this.folderHierarchy.removeAt(index);
    if (this.folderHierarchy.length === 0) this.isHierarchyVisible = false;
  }

  addFolder(): void {
    this.folderHierarchy.push(
      this.formBuilder.group({
        name: [
          null,
          {
            validators: [
              Validators.required,
              duplicateFolderName(),
            ],
            updateOn: 'blur',
          },
        ],
        subFolders: this.formBuilder.array([]),
        level: 0,
      })
    );
    this.isHierarchyVisible = true;
  }

  getForm(control: AbstractControl): FormGroup {
    return control as FormGroup;
  }

  get folderHierarchy(): FormArray {
    return this.myForm.get('folderHierarchy') as FormArray;
  }
}

folder-hierarchy.component.html

<div *ngIf="folder" #folderRow class="folder-row" [formGroup]="folder">
  <div class="folder-header">
    <div class="folder-name-container">
      <label for="folderName" class="folder-name-label">Name:</label>
      <input
        #folderName
        id="folderName"
        [ngClass]="nameControl.errors ? 'invalid-input' : ''"
        class="folder-name-input"
        placeholder="Folder Name"
        type="text"
        maxlength="50"
        autocomplete="off"
        name="name"
        formControlName="name"
        (keyup.enter)="folderName.blur()"
      />
    </div>

    <button
      type="button"
      class="btn-remove-folder"
      (click)="removeFolder(folder)"
    >
      Remove
    </button>

    <button
      type="button"
      class="btn-add-subfolder"
      [disabled]="disableAdd(folder)"
      (click)="addSubFolder(folder)"
    >
      Add Subfolder
    </button>
  </div>

  <div
    *ngIf="folder && folder.value.subFolders.length > 0"
    class="subfolder-container"
  >
    <div
      *ngFor="
        let subFolder of getSubFoldersControls(folder);
        let i = index
      "
      class="subfolder-item"
    >
      <folder-hierarchy
        (remove)="removeSubFolder(folder, i)"
        [folder]="subFolder"
        [parentDirectory]="getSubFolderParent(subFolder)"
      >
      </folder-hierarchy>
    </div>
  </div>

  <div
    *ngIf="nameControl.errors && nameControl.errors.required"
    class="folder-hierarchy-error"
  >
    Name is required.
  </div>

  <div
    *ngIf="nameControl.errors && nameControl.errors.duplicateName"
    class="folder-hierarchy-error"
  >
    Name already exists
  </div>
</div>

folder-hierarchy.component.ts

import { duplicateFolderName } from '../validators/duplicate-folder-name.validator';

@Component({
  selector: 'folder-hierarchy',
  templateUrl: './folder-hierarchy.component.html',
  styleUrls: ['./folder-hierarchy.component.css'],
})
export class FolderHierarchyComponent implements OnInit {
  constructor(private formBuilder: FormBuilder) {}
  @Output() remove = new EventEmitter();
  @Input() folder!: FormGroup;
  @Input() index!: number;
  @Input() parentDirectory?: FormGroup;

  ngOnInit() {}

  addSubFolder(folder: FormGroup): void {
    (folder.get('subFolders') as FormArray).push(
      this.formBuilder.group({
        name: [
          null,
          {
            validators: [
              Validators.required,
              duplicateFolderName(this.parentDirectory),
            ],
            updateOn: 'blur',
          },
        ],
        subFolders: this.formBuilder.array([]),
        level: folder.value.level + 1,
      })
    );
  }

  getControls(folder: FormGroup): FormArray {
    return folder.get('subFolders') as FormArray;
  }

  getSubFoldersControls(folder: FormGroup): FormGroup[] {
    return (folder.get('subFolders') as FormArray).controls as FormGroup[];
  }

  removeSubFolder(folder: FormGroup, index: number): void {
    (folder.get('subFolders') as FormArray).removeAt(index);
  }

  removeFolder(folder: { value: { subFolders: string | any[] } }): void {
    this.remove.emit(folder);
  }

  disableAdd(folder: { invalid: any }): void {
    return this.folder.invalid || folder.invalid;
  }

  get nameControl() {
    return this.folder.get('name') as FormControl;
  }

  getSubFolderParent(formGroup: FormGroup) {
    return formGroup.parent[0] as FormGroup;
  }
}

validators/duplicate-folder-name.validator.ts

import {
  AbstractControl,
  FormArray,
  FormGroup,
  ValidatorFn,
} from '@angular/forms';

export const duplicateFolderName = (
  parentDirectory?: FormGroup
): ValidatorFn => {
  return (control: AbstractControl): { [key: string]: any } | null => {
    if (!control.value) return null;

    let folderNamesInSameDirectory: string[] = parentDirectory
      ? (parentDirectory.get('subFolders').value as any[]).map((x) => x.name)
      : (control.parent.parent.value as any[]).map((x) => x.name);

    console.log(folderNamesInSameDirectory);

    if (folderNamesInSameDirectory.indexOf(control.value) > -1)
      return {
        duplicateName: true,
      };

    return null;
  };
};

Demo @ StackBlitz

like image 80
Yong Shun Avatar answered Oct 02 '25 06:10

Yong Shun


As you already have onKeyup listener in the folder component, you could get the parent form, and check whether there's an element with the same name already, for example:

  onKeyup(event: KeyboardEvent): void {

    this.tempName = (event.target as HTMLInputElement).value;

    // filter elements that have the same `name` property, but exclude this folder by index        
    const hasDuplicateName = this.folder.parent.value.filter((el:any, i:number)=> el.name.toLowerCase() === this.tempName.toLowerCase() && i !== this.index);

    // set validity    
    if(hasDuplicateName.length > 0) {
      console.log('hasDuplicateName', hasDuplicateName);
      this.folder.setErrors({'duplicateName': true});
    }
  }
like image 33
traynor Avatar answered Oct 02 '25 04:10

traynor



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!