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.
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.
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.
<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>