skip to Main Content

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>

2

Answers


  1. 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});
        }
      }
    
    Login or Signup to reply.
  2. 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]="folder"
          >
          </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;
      }
    }
    

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search