0

this is kind of a continuation of an improved version of an old question of mine
so basically I have a recursive Angular form and I’m using to manage a folder hierarchy. Each folder has a radio button for selecting between Exclusive and Regular folder.

Now I'm trying to add a validation so that

  • When a folder is selected as Exclusive at any level in the hierarchy the Exclusive button should be disabled for all other folders at any level. (basically 1 exclusive folder in the folder hierarchy)
  • The Regular option should remain available at all levels regardless of whether Exclusive is selected at any other level

How should I even approach a problem like this because the notification should happen from parent to child level and also from child level to parent level. Is this possible to be done with @Input() or is there a better approach.

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

form component

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators, AbstractControl } from '@angular/forms';
import { duplicateFolderName } from '../validators/duplicate-folder-name.validator';

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

  constructor(private fb: FormBuilder) {}

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

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

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

  get folderHierarchy() { return this.myForm.get('folderHierarchy') as FormArray; }
  getForm(control: AbstractControl) { return control as FormGroup; }
}

<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 *ngIf="!folderHierarchy.valid" class="folder-hierarchy-error">invalid folder hierarchy</div>
    <div class="folderContainer">
      <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>
</form>

folder-hierarchy component

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
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 {
  @Output() remove = new EventEmitter();
  @Input() folder!: FormGroup;
  @Input() index!: number;
  @Input() parentDirectory?: FormGroup;

  constructor(private fb: FormBuilder) {}

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

  updateIsExclusive(folder: FormGroup, value: Event, isExclusive: boolean): void {
    folder.get('isExclusive')?.setValue(isExclusive ? (value.target as HTMLInputElement).checked : !(value.target as HTMLInputElement).checked);
  }

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

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

  removeFolder(folder: FormGroup): void { this.remove.emit(folder); }

  disableAdd(folder: FormGroup): boolean { return this.folder.invalid || folder.invalid; }

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

<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>
    <div class="folder-type-container">
      <div class="folder-type-option">
        <input type="radio" [value]="true" [checked]="folder.value.isExclusive" (change)="updateIsExclusive(folder, $event, true)" />
        <label for="exclusive">Exclusive</label>
      </div>
      <div class="folder-type-option">
        <input type="radio" [value]="false" [checked]="!folder.value.isExclusive" (change)="updateIsExclusive(folder, $event, false)" />
        <label for="regular">Regular</label>
      </div>
    </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?.required" class="folder-hierarchy-error">Name is required.</div>
  <div *ngIf="nameControl.errors?.duplicateName" class="folder-hierarchy-error">Name already exists</div>
</div>

Help is much appreciated

2
  • 1
    can the exclusive be changed after it has been selected, because per your requirement, what is selected as exclusive, can not be changed or reverted? Commented Sep 30 at 12:25
  • 1
    @NarenMurali it can be changed. say i select exclusive then the exclusive button in other folders get disabled. then i change my mind and select the regular. then the other folder levels exclusive is enabled. my goal is to restrict to 1 exclusive folder at all times Commented Sep 30 at 14:33

1 Answer 1

1

As you are working with Reactive Form, you can use formControlName attribute without need the (change) event to set the isExclusive form control.

<div class="folder-type-option">
  <input
    type="radio"
    [value]="true"
    [checked]="folder.value.isExclusive"
    formControlName="isExclusive"
  />
  <label for="exclusive"> Exclusive </label>
</div>
<div class="folder-type-option">
  <input
    type="radio"
    [value]="false"
    [checked]="!folder.value.isExclusive"
    formControlName="isExclusive"
  />
  <label for="regular"> Regular </label>
</div>

Implementation/Concept:

  1. "Flatten" to get all isExclusive controls as key-value pair.
  2. Iterate the key-value pair above to disable the rest isExclusive controls when one of the controls is updated through valueChanges subscription.
  3. As your form is dynamically render (add/remove FormGroup), you need to call setupGlobalExclusiveLogic method when the form (folderHierarchy)'s value is changed.
  4. Remember to destroy the subscriptions when the component is destroy via destroy$ Subject.
import { Subject, takeUntil } from 'rxjs';

private destroy$ = new Subject<void>();

ngOnInit() {
  ...

  this.myForm.get('folderHierarchy').valueChanges.subscribe((x) => {
    this.setupGlobalExclusiveLogic();
  });
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

findControlsByName(
  control: AbstractControl,
  targetName: string,
  collectedControls: AbstractControl[] = []
): AbstractControl[] {
  if (control instanceof FormGroup || control instanceof FormArray) {
    const controls =
      (control as FormGroup).controls || (control as FormArray).controls;

    Object.keys(controls).forEach((key) => {
      const childControl = controls[key];

      if (key === targetName && childControl instanceof FormControl) {
        collectedControls.push(childControl);
      }

      // Recurse deeper
      this.findControlsByName(childControl, targetName, collectedControls);
    });
  }

  return collectedControls;
}

setupGlobalExclusiveLogic(): void {
  const targetName = 'isExclusive';

  // Flatten the form for all 'isExclusive' controls
  const exclusiveControls = this.findControlsByName(this.myForm, targetName);

  exclusiveControls.forEach((triggerControl) => {
    triggerControl.valueChanges
      .pipe(takeUntil(this.destroy$))
      .subscribe((isExclusiveValue: boolean) => {
        const shouldDisable = isExclusiveValue === true;

        for (let controlToToggle of exclusiveControls) {
          // Exclude disable the current triggered control
          if (controlToToggle === triggerControl) {
            continue;
          }

          shouldDisable
            ? controlToToggle.disable({ emitEvent: false })
            : controlToToggle.enable({ emitEvent: false });
        }
      });
  });
}

Demo @ StackBlitz

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.