Using mat-accordion and mat-expansion-panel in ngFor

Viewed 10462

I am trying to loop some items with an ngFor and create a mat-expansion-panel for each item.

I would like to encapsulate the logic for each item into an item component:

    <item></item>

Initial attempt:

<mat-accordion>
  <div *ngFor="let item of items" >
    <item [item]="item"></item>
  </div>
</mat-accordion>

Where item template is:

    <mat-expansion-panel>
      <mat-expansion-panel-header>
        Header Content
      </mat-expansion-panel-header>

      <div>
         Expandable content
      </div>
    </mat-expansion-panel>

The problem with this approach is that there is an extra html element for my component between <mat-accordion> and <mat-expansion-panel>, which messes up the css for the accordion.

Is there a way that my component can provide the header and content?

    <mat-accordion>
      <div *ngFor="let item of items" >
          <mat-expansion-panel>
            <mat-expansion-panel-header>
              <!-- put item component header here->
            </mat-expansion-panel-header>

          <div>
            <!-- put item component content here->
          </div>
        </mat-expansion-panel>
      </div>
    </mat-accordion>

I have read up on ng-content, but I think that is backwards from what I want, I don't want to shove custom content into my component, I want to extract elements from my component and have them render in the parent.

I do realize I could create 2 components, item-header and item-content. However I would really like to keep that login in one component.

5 Answers

I encountered the same problem. A Pull Request is opened on this subject but never merged. We can improve a little the layout (border radius), by adding this code below. To keep things simple, best we can do is to restore border-radius on top and bottom of first and last panel.

But it's more difficult for bottom of previous panel not expanded, and top of next panel not expanded...

main component.html :

<mat-accordion class="custom-accordion">
  <item *ngFor="let item of items" [item]="item"></item>
</mat-accordion>

item.component.ts:

@Component({
  selector: 'app-item',
  template: `
    <mat-expansion-panel>
      <mat-expansion-panel-header>
        Header Content
      </mat-expansion-panel-header>
      <div>
        Expandable content
      </div>
    </mat-expansion-panel>
  `,
  host: {
    'class': 'expansion-panel-wrapper',
    '[class.expanded]': 'expansionPanel && expansionPanel.expanded',
  }
})
export class ItemComponent {
  @ViewChild(MatExpansionPanel, { static: false }) expansionPanel;
  ...
}

in styles.scss :

.custom-accordion.mat-accordion {
  .expansion-panel-wrapper:first-of-type .mat-expansion-panel {
    border-top-right-radius: 4px;
    border-top-left-radius: 4px;
  }

  & > :not(.expanded) .mat-expansion-panel {
    border-radius: 0;
  }

  & > .expansion-panel-wrapper:last-of-type > .mat-expansion-panel {
    border-bottom-right-radius: 4px;
    border-bottom-left-radius: 4px;
  }
}

You were almost there. You don't need to have any wrapping element - you can simply use *ngFor directly on your component:

<mat-accordion>
    <item *ngFor="let item of items" [item]="item"></item>
</mat-accordion>

Working stack-blitz with simple example of working code: https://stackblitz.com/edit/angular-pvse7t

By looking at the code, I think your problem comes from this:

<mat-accordion>
  <div *ngFor="let item of items" > **//this right here creates a 'div' element for each loop**
    <item [item]="item"></item>
  </div>
</mat-accordion>

Try replacing the div with ng-container instead:

<mat-accordion>
  <ng-container *ngFor="let item of items" > **//ng-container is never rendered in the DOM**
    <item [item]="item"></item>
  </ng-container>
</mat-accordion>

From https://stackoverflow.com/a/38716164/11928194 , try

<mat-accordion>
  <ng-container *ngFor="let item of items" >
    <mat-expansion-panel item  [item]="item"></mat-expansion-panel>
  </ng-container>
</mat-accordion>

With ItemComponent selector as '[item]'

Why not apply ngFor to the expansion panel directly?

<mat-accordion>
  <mat-expansion-panel *ngFor="let item of items">
    <mat-expansion-panel-header>
      <mat-panel-title>
        Header Content
      </mat-panel-title>
    </mat-expansion-panel-header>
    <ng-template matExpansionPanelContent>
      Expandable Content
    </ng-template>
  </mat-expansion-panel>
</mat-accordion>

I was trying to share a link from Stackblitz, but unfortunately it seems we have a bug there with sharing, so I'm going to share a screenshot of what I mean:

enter image description here

Edit: it seems Stackblitz decided to cooperate now. Here is the link: https://stackblitz.com/edit/angular-ae7cfr

Related