angularngforangular-bootstrap

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.


Solution

  • 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