Roman
Roman

Reputation: 3149

Angular Material 2 DataTable Sorting with nested objects

I have a normal Angular Material 2 DataTable with sort headers. All sort are headers work fine. Except for the one with an object as value. These doesn't sort at all.

For example:

 <!-- Project Column - This should sort!-->
    <ng-container matColumnDef="project.name">
      <mat-header-cell *matHeaderCellDef mat-sort-header> Project Name </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.project.name}} </mat-cell>
    </ng-container>

note the element.project.name

Here's the displayColumn config:

 displayedColumns = ['project.name', 'position', 'name', 'test', 'symbol'];

Changing 'project.name' to 'project' doesn't work nor "project['name']"

What am I missing? Is this even possible?

Here's a Stackblitz: Angular Material2 DataTable sort objects

Edit: Thanks for all your answers. I've already got it working with dynamic data. So I don't have to add a switch statement for every new nested property.

Here's my solution: (Creating a new DataSource which extends MatTableDataSource is not necessary)

export class NestedObjectsDataSource extends MatTableDataSource<MyObjectType> {

  sortingDataAccessor: ((data: WorkingHours, sortHeaderId: string) => string | number) =
    (data: WorkingHours, sortHeaderId: string): string | number => {
      let value = null;
      if (sortHeaderId.indexOf('.') !== -1) {
        const ids = sortHeaderId.split('.');
        value = data[ids[0]][ids[1]];
      } else {
        value = data[sortHeaderId];
      }
      return _isNumberValue(value) ? Number(value) : value;
    }

  constructor() {
    super();
  }
}

Upvotes: 131

Views: 69730

Answers (16)

Sylvin J. Rodrigues
Sylvin J. Rodrigues

Reputation: 11

this.dataSource.data.map((res)=>{
    res.name = res.project.name; 
  })

If you want to sort by name then you can manipulate the original array using the map

Upvotes: 0

Deepu Reghunath
Deepu Reghunath

Reputation: 9663

If your data contain inner object like below example

myData = [ {service: 'SRS',
      team: {id: 'T1', name: 'Blasters'},
      rank: 23,
      .....
     },
     { service: 'COC',
      team: {id: 'T2', name: 'Strikers'},
      rank: 7,
      .....
     },
     ......
]

then you can use a custom sorting function

<table mat-table [dataSource]="myData" #outersort="matSort" matSort>
      <ng-container matColumnDef="service">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>SERVICE</th>
        <td mat-cell *matCellDef="let element">
          {{ element.service }}
        </td>
      </ng-container>
      <ng-container matColumnDef="team.name">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>TEAM</th>
        <td mat-cell *matCellDef="let element">
          {{ element.team.name }}
        </td>
      </ng-container>
     .....

     <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
     <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
   </table>

displayed column should represent nested object as follows

displayedColumns = ['service','team.name', 'rank']

and in ts file

@ViewChild(MatSort) MySort: MatSort;

 .....
  this.dataSource = new MatTableDataSource(myData);
  this.dataSource.sortingDataAccessor = (item, property) => {
    return eval('item.'+property);
  };
  this.dataSource.sort = MySort;
.....

Upvotes: 0

Botond
Botond

Reputation: 680

I find a clear solution and works for any input data.

    this.dataSource.sortingDataAccessor = (obj: any, property: string) => property.split('.').reduce((o, p) => o && o[p], obj);
    this.dataSource.sort = this.sort;

In the HTML file you need to set the nested object to the mat-sort-header. EX: mat-sort-header="User.Name"

      <ng-container matColumnDef="UserName">
        <th mat-header-cell *matHeaderCellDef mat-sort-header="User.Name">User</th>
        <td mat-cell *matCellDef="let row">{{ row.User.Name}}</td>
      </ng-container>

Explanation: First step: set your data source. Sec.: set the generic sorting data accelerator. Third: call the sort to the datasource.

  this.dataSource = new MatTableDataSource(project.ActivityLog.items);
  setTimeout(() => {
  
    this.dataSource.sortingDataAccessor = (obj: any, property: string) => property.split('.').reduce((o, p) => o && o[p], obj);
    this.dataSource.sort = this.sort;

  })
}

Upvotes: 0

Suhail Akhtar
Suhail Akhtar

Reputation: 2023

My table columns were not ordering correctly, so I modified one of the answers to work with my data.

function pathDataAccessor(item: any, path: string): any {
  return (item: any, path: string): any => {
    return path.split(".").reduce((accumulator: any, key: string) => {
      let returnValue;
      if (accumulator) {
        returnValue = accumulator[key];
      } else {
        returnValue = undefined;
      }
      if (typeof returnValue === "string") {
        returnValue = returnValue.trim().toLocaleLowerCase();
      }
      return returnValue;
    }, item);
  };
}

Upvotes: 0

Mikelgo
Mikelgo

Reputation: 575

If you want to have an Angular material table with some extended features, like sorting for nested objects have a look at https://github.com/mikelgo/ngx-mat-table-extensions/blob/master/libs/ngx-mat-table/README.md .

I created this lib because I was missing some features of mat-table out of the box.

The advanced sorting is similar to @Hieu Nguyen suggested answer but a bit extended to also have proper sorting by upper and smaller case letters.

Upvotes: 1

Chan Jing Hong
Chan Jing Hong

Reputation: 2461

Just add this to your data source and you will be able to access the nested object

this.dataSource.sortingDataAccessor = (item, property) => {
    // Split '.' to allow accessing property of nested object
    if (property.includes('.')) {
        const accessor = property.split('.');
        let value: any = item;
        accessor.forEach((a) => {
            value = value[a];
        });
        return value;
    }
    // Access as normal
    return item[property];
};

Upvotes: 2

Priti jha
Priti jha

Reputation: 143

Use MatTableDataSource Check complete MatSort issue solution

in HTML

    <ng-container matColumnDef="createdDate" @bounceInLeft>
      <th mat-header-cell *matHeaderCellDef mat-sort-header class="date"> Created date
      </th>
          <td mat-cell *matCellDef="let element" class="date"> {{element.createdDate
           | date :'mediumDate'}} </td>
   </ng-container>

  <ng-container matColumnDef="group.name">
    <th mat-header-cell *matHeaderCellDef mat-sort-header class="type"> Group </th>
    <td mat-cell *matCellDef="let element" class="type"> {{element.group.name}} </td>
  </ng-container>

@ViewChild(MatSort, { static: true }) sort: MatSort;

    ngOnInit() {
      this.dataSource = new MatTableDataSource(yourData);
      this.dataSource.sortingDataAccessor = (item, property) => {
    switch(property) {
      case 'project.name': return item.project.name;
      default: return item[property];
    }
  };
  this.dataSource.sort = sort;
}

Upvotes: 0

Andy
Andy

Reputation: 303

I like @Hieu_Nguyen solutions. I'll just add that if you use lodash in you project as I do then the solution translates to this:

import * as _ from 'lodash';

this.dataSource.sortingDataAccessor = _.get; 

No need to reinvent the deep property access.

Upvotes: 13

Tim Harker
Tim Harker

Reputation: 2407

Another alternative, that no one threw out here, flatten the column first...

yourData.map((d) => 
   d.flattenedName = d.project && d.project.name ? 
                     d.project.name : 
                     'Not Specified');

this.dataSource = new MatTableDataSource(yourData);

Just another alternative, pros and cons for each!

Upvotes: 1

Steve Sanders
Steve Sanders

Reputation: 8651

It was hard to find documentation on this, but it is possible by using sortingDataAccessor and a switch statement. For example:

@ViewChild(MatSort) sort: MatSort;

ngOnInit() {
  this.dataSource = new MatTableDataSource(yourData);
  this.dataSource.sortingDataAccessor = (item, property) => {
    switch(property) {
      case 'project.name': return item.project.name;
      default: return item[property];
    }
  };
  this.dataSource.sort = sort;
}

Upvotes: 251

Erik Schaareman
Erik Schaareman

Reputation: 191

The answer as given can even be shortened, no switch required, as long as you use the dot notation for the fields.

ngOnInit() {
  this.dataSource = new MatTableDataSource(yourData);

  this.dataSource.sortingDataAccessor = (item, property) => {
     if (property.includes('.')) return property.split('.').reduce((o,i)=>o[i], item)
     return item[property];
  };

  this.dataSource.sort = sort;
}

Upvotes: 19

Toby Harris
Toby Harris

Reputation: 141

I use a generic method which allows you to use a dot.seperated.path with mat-sort-header or matColumnDef. This fails silently returning undefined if it cannot find the property dictated by the path.

function pathDataAccessor(item: any, path: string): any {
  return path.split('.')
    .reduce((accumulator: any, key: string) => {
      return accumulator ? accumulator[key] : undefined;
    }, item);
}

You just need to set the data accessor

this.dataSource.sortingDataAccessor = pathDataAccessor;

Upvotes: 14

E.Sarawut
E.Sarawut

Reputation: 11

I customized for multiple nested object level.

this.dataSource.sortingDataAccessor =
  (data: any, sortHeaderId: string): string | number => {
    let value = null;
    if (sortHeaderId.includes('.')) {
      const ids = sortHeaderId.split('.');
      value = data;
      ids.forEach(function (x) {
        value = value? value[x]: null;
      });
    } else {
      value = data[sortHeaderId];
    }
    return _isNumberValue(value) ? Number(value) : value;
  };

Upvotes: 1

Hieu Nguyen
Hieu Nguyen

Reputation: 451

You can write a function in component to get deeply property from object. Then use it in dataSource.sortingDataAccessor like below

getProperty = (obj, path) => (
  path.split('.').reduce((o, p) => o && o[p], obj)
)

ngOnInit() {
  this.dataSource = new MatTableDataSource(yourData);
  this.dataSource.sortingDataAccessor = (obj, property) => this.getProperty(obj, property);
  this.dataSource.sort = sort;
}

columnDefs = [
  {name: 'project.name', title: 'Project Name'},
  {name: 'position', title: 'Position'},
  {name: 'name', title: 'Name'},
  {name: 'test', title: 'Test'},
  {name: 'symbol', title: 'Symbol'}
];

And in html

<ng-container *ngFor="let col of columnDefs" [matColumnDef]="col.name">
      <mat-header-cell *matHeaderCellDef>{{ col.title }}</mat-header-cell>
      <mat-cell *matCellDef="let row">
        {{ getProperty(row, col.name) }}
      </mat-cell>
  </ng-container>

Upvotes: 45

kawthar
kawthar

Reputation: 119

I had the same issue, by testing the first proposition I had some errors, I could fixe it by adding "switch (property)"

this.dataSource.sortingDataAccessor =(item, property) => {
    switch (property) {
    case 'project.name': return item.project.name;

    default: return item[property];
    }
  };

Upvotes: 0

funkizer
funkizer

Reputation: 4897

It's trying to sort by element['project.name']. Obviously element doesn't have such a property.

It should be easy to create a custom datasource that extends MatTableDatasource and supports sorting by nested object properties. Check out the examples in material.angular.io docs on using a custom source.

Upvotes: 0

Related Questions