joed4no
joed4no

Reputation: 1363

Why is this *ngFor loop rendering the data twice?

I have an NG Bootstrap Accordion element that, when open, displays a list of contact cards. For some reason the list of contacts output by the ngFor is duplicated and renders twice. The data is not duplicated (checked both the data source and with a debugger).

<div ngbAccordion class="accordion accordion-flush">
  <div ngbAccordionItem class="accordion-item">
    <h3 ngbAccordionHeader class="accordion-header">
      <button
        ngbAccordionButton
        class="accordion-button text-secondary fs-5 ps-0"
      >
        Contacts
      </button>
    </h3>
    <div
      ngbAccordionBody
      ngbAccordionCollapse
      class="accordion-body row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3"
    >
      <ng-container
        *ngIf="card.contacts && card.contacts.length; else noContacts"
      >
        <div *ngFor="let contact of card.contacts" class="col">
          <app-contact-card [contact]="contact"></app-contact-card>
        </div>
      </ng-container>

      <ng-template #noContacts>
        <div class="text-secondary">No contacts</div>
      </ng-template>
    </div>
  </div>
</div>

If I do not include the ng-container tag with the ngIf statement, then the Accordion component errors since ngFor initially returns null before displaying the data. However, with the code as it is, the list of contacts is displayed twice (one full list after another full list, not duplicate contacts back to back).

I have tried adding a trackBy function to the ngFor, but that did not resolve the duplication. Any suggestions or ideas would be greatly appreciated. Thanks.

Edit with more info

The template for the contact card looks like this

<div class="card h-100">
  <div class="card-header d-flex">
    <i class="bi bi-person-circle fs-5 me-3"></i>
    <div class="d-flex flex-column align-self-center">
      <ng-container *ngIf="contact.first_name || contact.last_name; else noName">
        <div>{{ contact.first_name + ' ' }} {{ contact.last_name }}</div>
        <small>{{ contact.phone }}</small>
      </ng-container>

      <ng-template #noName>
        <div>{{ contact.phone }}</div>
      </ng-template>
    </div>
  </div>

  <div class="card-body">
    <ng-container *ngIf="contact.fieldsToShow.length; else noExtraData">
      <div *ngFor="let field of contact.fieldsToShow" class="mini-card">
        <div class="field text-secondary">{{ field }}:</div>
        <div class="value text-truncate ms-1">{{ contact[field] }}</div>
      </div>
      <div *ngIf="contact.totalFields > contact.fieldsToShow.length" class="text-secondary text-center mb-n2">
        <small>More</small>
      </div>
    </ng-container>

    <ng-template #noExtraData>
      <div class="text-secondary">No extra data</div>
    </ng-template>
  </div>
</div>

I do not think it has anything to do with the contact card component however, because if I add more rows in the AccordionBody section under the contact cards, it renders the full set of contacts, then any other rows (which is all correct), then render the contacts again. It does not render any of the other rows however.

Upvotes: 5

Views: 245

Answers (2)

Markus
Markus

Reputation: 122

In addition to your answer:
You could also use the newer @ connotation to avoid all those ng-template and ng-container wrappers.
It was introduced with Angular 17 as part of the new control flow syntax

It would look something like this:

    @if (card.contacts && card.contacts.length) {
      
      // You could also leave the *ngFor instead of @for
      @for (contact of card.contacts; track contact ) {
        <div class="col">
          <app-contact-card [contact]="contact"></app-contact-card>
        </div>
      }
    } @else {
      <div class="text-secondary">No contacts</div>
    }

Upvotes: 1

Naren Murali
Naren Murali

Reputation: 57986

Below the accordion body, the entire content must be wrapped inside an ng-template doing this removed the duplication.

One more thing is that the ngbAccordionCollapse must be in it's own div, then followed by another div with ngbAccordionBody directive.

Accordion Documentation Examples

<div ngbAccordion class="accordion accordion-flush">
  <div ngbAccordionItem class="accordion-item">
    <h3 ngbAccordionHeader class="accordion-header">
      <button
        ngbAccordionButton
        class="accordion-button text-secondary fs-5 ps-0"
      >
        Contacts
      </button>
    </h3>
    <div
      ngbAccordionCollapse
      class="accordion-body row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3"
    >
      <div ngbAccordionBody>
        <ng-template> <!-- changed here! -->
          <ng-container
            *ngIf="card.contacts && card.contacts.length; else noContacts"
          >
            <div *ngFor="let contact of card.contacts" class="col">
              <app-contact-card [contact]="contact"></app-contact-card>
            </div>
          </ng-container>

          <ng-template #noContacts>
            <div class="text-secondary">No contacts</div>
          </ng-template>
        </ng-template> <!-- changed here! -->
      </div>
    </div>
  </div>
</div>

Stackblitz Demo

Upvotes: 3

Related Questions