55

How would you make rows expandable in angular material tables? One requirement is that I need to be using the angular material table. I would also prefer to use the material accordion to the information provided here.

I want to click on row and show different information for each column. Im looking for something like below. If you click on row 1, rows 2 and 3 appear with different data.

enter image description here

Neuron
  • 5,141
  • 5
  • 38
  • 59
Simon
  • 1,681
  • 1
  • 21
  • 34

5 Answers5

116

As mentioned here by Andrew Seguin this is already feasible out of the box: using the when predicate.

See this example: https://stackblitz.com/edit/angular-material-expandable-table-rows (thx to Lakston)

demo

Inside of the mat-table tag you have to use the mat-row component with a matRipple directive. When you click on a row the row element will be assigned to the expandedElement variable:

<mat-row *matRowDef="let row; columns: displayedColumns;"
        matRipple 
        class="element-row" 
        [class.expanded]="expandedElement == row"
        (click)="expandedElement = row">
</mat-row>

But now we have to add our expanded row, that is hidden by default and will be shown if the user clicks on the row above:

<mat-row *matRowDef="let row; columns: ['expandedDetail']; when: isExpansionDetailRow"
        [@detailExpand]="row.element == expandedElement ? 'expanded' : 'collapsed'"
        style="overflow: hidden"> 
</mat-row>

Important is here the already mentioned when predicate. This calls a isExpansionDetailRow function that is defined in the component itself and checks if the row has a detailRow property:

isExpansionDetailRow = (row: any) => row.hasOwnProperty('detailRow');

Since RC0 the first param is the index:

isExpansionDetailRow = (i: number, row: any) => row.hasOwnProperty('detailRow');

If you want to have an expanded view for every row, you have to add an "ExpansionDetailRow" identified by the detailRow property for every row like this:

connect(): Observable<Element[]> {
    const rows = [];
    data.forEach(element => rows.push(element, { detailRow: true, element }));
    return Observable.of(rows);
}

If you would log the rows variable to the console output, it will look like this:

console output

EDIT: COMPLETE EXAMPLE USING DIRECTIVE

Mat Table expandable rows (sorting, pagination and filtering)

Umair M
  • 10,298
  • 6
  • 42
  • 74
Philipp Kief
  • 8,172
  • 5
  • 33
  • 43
  • Is there a way to grab the index in an individual cell so that only that cell is clickable? Also, this `when` solution is very nice! I'm injecting a details component that retrieves additional data from the API, so I'm not sure that would make sense here. – wolfhoundjesse Jan 12 '18 at 17:38
  • Nevermind. I see that you can do `let row; let index = index` right in the cell definition to receive the row properties. – wolfhoundjesse Jan 12 '18 at 18:20
  • This is amazing, I'm wondering if someone might be able to give me a hand with how to implement it in a material table that has pagination, sorting and filtering as in the last example found here https://material.angular.io/components/table/examples? – David Jan 28 '18 at 17:43
  • 6
    @David Here's an example with pagination, sorting and filtering: https://stackblitz.com/edit/angular-material2-expandable-rows-filter-pagination-sorting (thx to [shlomiassaf](https://github.com/angular/material2/issues/6095#issuecomment-348007549)). It uses another approach that is better for this case than in the example above. – Philipp Kief Jan 28 '18 at 22:52
  • @PhilippKief Thanks a lot for all this! Very helpful! Any idea on how to trigger the expand row without click? Like an icon inside the the row itself? I've been trying a few things but it seems hard to refer it without `@Host` annotations.. – jbarradas Mar 29 '18 at 11:52
  • I can't get this working, should isExpansionDetailRow fire every time I click a row? Also, if I set it to true, it will override the original row instead of adding a new one. – Miklós Kosárkár Apr 18 '18 at 15:21
  • How does `isExpansionDetailRow` work? When I use the following example from the demo page, `whenFn = (_i: number, d: DemoDataObject) => d.value;` it just replaces the regular rows with the expansion rows – DevEng May 07 '18 at 14:17
  • 4
    This solution unfortunately does not support pagination and sorting! – non4me May 24 '18 at 07:32
  • This could work with sort and pagination, it just need to do the double line in the observable at last: `this.getDetailRow(this.getPagedData(this.getSortedData([...this.data])))`. where getDetailRow returns an array with `element, { detailRow: true, element: element }` – ElBarto May 31 '18 at 15:54
  • @PhilippKief .. Great answer. Is it possible to add another table inside the row?? Is it feasible in Material Table? – Ganesh Babu Jun 10 '18 at 13:45
  • @David I added a solution at the bottom with pagination and column sorting if you're still interested – Braden Brown Jul 02 '18 at 21:37
  • Awesome!! Just what I needed and such a simple solution. – Jason Foglia Jul 02 '18 at 23:40
  • 3
    I'm getting the following error: `TypeError: Cannot read property 'sticky' of undefined` – Knight Jul 26 '18 at 13:37
  • Hi, thank you for above answer. i need to add column filters to above collapsible table. can you help me to do it? – kumara Jan 24 '19 at 07:38
  • @PhilippKief Is there a way to know if a row is expanded or not. I want to use this to render different icons in a certain column. I am using the directive cdkDetailRowDirective. – Umair M Jan 27 '19 at 17:22
  • For some reason, `connect()` is not getting called in my case, can anyone help ? Thanks – curious_debugger Mar 04 '20 at 16:35
  • Here's another example that uses multiTemplateDataRows so you can define multiple rows without having to duplicate the data: https://stackblitz.com/angular/ygdrrokyvkv?file=app%2Ftable-expandable-rows-example.html – Berend Nov 27 '20 at 08:20
  • anyone have any idea how to get this working with paginator and sorting – m.yuki Apr 09 '21 at 15:21
7

It is not possible out of the box, but you can solve it with a little custom code. Take a look at this discussion and this solution (not from me, but the basis for this answer).

In short: Use the material table and add a click-method to the rows:

<md-row *mdRowDef="let row; columns: displayedColumns; let index=index" (click)="expandRow(index, row)" #myRow></md-row>

Add a component for the expanded area. The row_detail.html contains the html which is in the expanded area.

@Component({
  selector: 'app-inline-message',
  templateUrl: 'row_detail.html',
  styles: [`
    :host {
      display: block;
      padding: 24px;
      background: rgba(0,0,0,0.03);
    }
  `]
})
export class InlineMessageComponent {
  @Input() content1: string;
  @Input() content2: string;
}

In your component where the table lives you need the method to expand the row. First, add this to your component...

expandedRow: number;
@ViewChildren('myRow', { read: ViewContainerRef }) containers;

... and then add the method:

/**
   * Shows the detail view of the row
   * @param {number} index
   */
expandRow(index: number, row: DataFromRowFormat) {

    if (this.expandedRow != null) {
      // clear old message
      this.containers.toArray()[this.expandedRow].clear();
    }

    if (this.expandedRow === index) {
      this.expandedRow = null;
    } else {
      const container = this.containers.toArray()[index];
      const factory: ComponentFactory<InlineMessageComponent> = this.resolver.resolveComponentFactory(InlineMessageComponent);
      const messageComponent = container.createComponent(factory);

      messageComponent.instance.content1= "some text";
      messageComponent.instance.content2 = "some more text";
    }
}
Neuron
  • 5,141
  • 5
  • 38
  • 59
Tobii
  • 81
  • 2
  • This is the one that worked for me, I think because I'm working with a custom datasource, thanks very much – RasMason Apr 26 '18 at 16:07
  • this is the thing i was searching and this suits my needs . Thanks :) – CruelEngine Nov 29 '18 at 08:41
  • I like this solution, creating a dynamic component better than the solution shown by @Philipp Kief above (let's call that one dynamic row). The dynamic row solution starts to cause some grief around pagination and sorting as it is messing with the rows of the table. This solution, dynamic component, also gives you the ability to easily do everything else with the component that you would normally do. – Matt Jan 11 '19 at 18:51
  • 1
    add `this.expandedRow = index` at the end of the `else` block in the `expandRow` method – andreisrob Feb 14 '19 at 05:00
  • Check out this [stackblitz](https://stackblitz.com/edit/angular-dynamically-creating-components-animations-ng?file=app%2Falert.component.ts) to implement animation when the dynamic component is created and destroyed with each click – andreisrob Feb 14 '19 at 15:35
  • in angular 7 replace this.resolver.resolveComponentFactory with this.componentFactoryResolver.resolveComponentFactory and add a constructor to ComponentFactoryResolver – shasho Mar 10 '19 at 16:48
2

When CDK was the only way to get something close to a Material Table, using md-row's in a regular table was a viable alternative, but since @angular/material 2.0.0-beta.12 ditched CDK tables and now have their own Data Tables that might fit your needs. See documentation below:

https://material.angular.io/components/table/overview

1

Here is how you can do it with Angular Material. This example includes pagination and one example with mat-sort-header on the 'name' column. I had to override mat paginator and do a custom sort to make sure the expandable row was still by its parent when it was sorted. This example also allows you to open multiple rows at a time and to close all rows

https://stackblitz.com/edit/angular-material2-issue-zs6rfz

Braden Brown
  • 3,111
  • 1
  • 20
  • 18
0

This answer is for only using Angular, we can do the Table row expand and collapse option.

I have given StackBlitz

In the TS file, we have created a variable to store table data.

data = [
    {
      id: 1,
      name: 'Abc',
      email: 'abc@mail.com',
      isExpand: false,
      address: [
        {
          add1: 'Delhi',
          add2: 'Bangalore',
        }
      ]
    },
    {
      id: 2,
      name: 'Xyz',
      email: 'xyz@mail.com',
      isExpand: false,
      address: [
        {
          add1: 'Mumbai',
          add2: 'Pune',
        }
      ]
    },
    {
      id: 3,
      name: 'ijk',
      email: 'ijk@mail.com',
      isExpand: false,
      address: [
        {
          add1: 'Chennai',
          add2: 'Bangalore',
        }
      ]
    },
    {
      id: 4,
      name: 'def',
      email: 'def@mail.com',
      isExpand: false,
      address: [
        {
          add1: 'Kolkata',
          add2: 'Hyderabad',
        }
      ]
    }
  ]

In the HTML file, we have a table.

<table>
  <thead>
    <tr>
      <th></th>
      <th>SL</th>
      <th>Name</th>
      <th>Email</th>
    </tr>
  </thead>
  <tbody>
    <ng-container *ngFor="let row of data">
      <tr>
        <td (click)="row.isExpand = !row.isExpand">
          <span *ngIf="!row.isExpand">+</span>
          <span *ngIf="row.isExpand">-</span>
        </td>
        <td>{{ row.id }}</td>
        <td>{{ row.name }}</td>
        <td>{{ row.email }}</td>
      </tr>
      <tr *ngIf="row.isExpand">
        <td colspan="4">
          <table>
            <thead>
              <th>Address1</th>
              <th>Address2</th>
            </thead>
            <tbody>
              <tr *ngFor="let row2 of row.address">
                <td>{{ row2.add1 }}</td>
                <td>{{ row2.add2 }}</td>
              </tr>
            </tbody>
          </table>
        </td>
      </tr>
    </ng-container>
  </tbody>
</table>
Nashir
  • 254
  • 1
  • 5
  • 9