htmlangulartypescriptangular-directive

Directive to count nested arrays in Angular 14 when is used the component multiple times


I have this simple directive in Angular for counting nested arrays:

import { AfterViewInit, Directive, Input, OnDestroy } from '@angular/core';

@Directive({
  selector: '[rhsNestedArrayCounter]',
  exportAs: 'rhsNestedArrayCounter'
})
export class NestedArrayCounterDirective implements AfterViewInit, OnDestroy  {
  static nextIndex = -1;

  @Input() appCounter: number | string = '';

  index;

  constructor() {
    this.index = NestedArrayCounterDirective.nextIndex++;
  }
  
  ngAfterViewInit(): void {
    if (typeof this.appCounter === 'string') {
      return;
    }
    NestedArrayCounterDirective.nextIndex = this.appCounter;
  }

  ngOnDestroy(): void {
    NestedArrayCounterDirective.nextIndex = -1;
  }
}

And I used it in my html like this, lets call it FormComponent:

<form [formGroup]="detailForm" class="form-container">
  <ng-container rhsNestedArrayCounter="0">
    <div *ngFor="let section of sections; let sectionIndex = index">
      <div class="section">
        <div class="section-content">
          <h3>{{ section.title }}</h3>
          <div formArrayName="responses">
            <div
              *ngFor="
                let questionWithAnswer of section.questionsWithAnswers;
                let questionIndex = index
              ">
              <div class="question">
                <div
                  rhsNestedArrayCounter
                  #counter="rhsNestedArrayCounter"
                  class="question-content"
                  [formGroupName]="counter.index">
                  <h4>
                    {{
                      questionWithAnswer.questionDescription
                        | textReplacer: '#EmployeeName':employeeName
                    }}
                  </h4>
                  <div>
                    <mat-form-field
                      color="accent"
                      id="sectionAnswer-{{ counter.index }}">
                      <textarea
                        matInput
                        placeholder="Answer"
                        formControlName="text"
                        name="text"
                        id="sectionAnswerInput-{{ counter.index }}"
                        [required]="questionWithAnswer.isRequired"
                        cdkTextareaAutosize
                        #autosize="cdkTextareaAutosize"
                        cdkAutosizeMinRows="2"
                        cdkAutosizeMaxRows="6"></textarea>
                      <mat-error
                        *ngIf="responses.controls[counter.index].get('text')?.errors?.['required']"
                        id="sectionAnswerError"
                        >Required field</mat-error
                      >
                      <mat-error
                        *ngIf="responses.controls[counter.index].get('text')?.errors?.['minlength']"
                        id="sectionAnswerMinLengthError"
                        >Min 2 characters</mat-error
                      >
                      <mat-error
                        *ngIf="responses.controls[counter.index].get('text')?.errors?.['maxlength']"
                        id="sectionAnswerMaxLengthError"
                        >Max 6000 characters</mat-error
                      >
                      <mat-error
                        *ngIf="responses.controls[counter.index].get('text')?.errors?.['pattern']"
                        id="sectionAnswerSpecialCharactersPatternError"
                        >Only letters, numbers, and some punctuation marks are
                        allowed.</mat-error
                      >
                    </mat-form-field>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </ng-container>
</form>

When I used FormComponent in my PageComponent one time everything worked fine, now I have a PageTwoComponent and I need to use the FormComponent more than one time, and because this counter is global I got errors about the index. For example the form have 15 questions, and I got the error of index 16 because when start count the second FormComponent it start from 16 because the first FormComponent finish in 15 (using two times the FormComponent in a Parent component).

enter image description here

So I thought maybe used a dictionary in the directive where I have for every component a unique index for every name, but I don't know how to implement this, or maybe another idea (I'm a C# developer) any help is welcome.


Solution

  • If I understood it correctly, you want such usage rhsNestedArrayCounter="0" to restore counter to index 0, and then just rhsNestedArrayCounter to save current index and increment it.

    If that is so then you should firstly use ngOnInit hook rather than ngAfterViewInit, and, considering your usage rhsNestedArrayCounter="0" 0 would be passed as a string "0" here.

    let counterValue = -1; // would work almost the same as the static prop
    class NestedArrayCounterDirective  {
      @Input() rhsNestedArrayCounter: number | string = ''; // name of the input matters.
      index: number;
      ngOnInit() {
        if(this.rhs) {
         counterValue = +this.rhs;
        } else {
          this.index = counterValue++;
        }
      }
      ngOnDestroy() { counterValue = -1;}
    }
    

    also to not make hacks with initializing and clearing the "counter" in directives, you can use a simple service for that counter, which you'll just put in the parent component's providers section

    @Injectable()
    class CounterService {
      private val = 0;  
      public getAndIncrement() {return this.val++;}
    }
    

    and that service should basically created "scoped" version of counter for the FormComponent and it should resolve the problem for you

    provide service like here

    @Component({providers: [CounterService],...}) cass FormComponent {}
    

    and use increment in the directive like here

    class NestedArrayCounterDirective {
      index = inject(CounerService).getAndIncrement();
    }
    

    even without any hooks it should work correctly