102

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();
  }
}
prograde
  • 2,322
  • 2
  • 22
  • 31
Roman
  • 2,719
  • 2
  • 19
  • 29

13 Answers13

216

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;
}
Steve Sanders
  • 7,952
  • 2
  • 29
  • 32
39

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>
Hieu Nguyen
  • 391
  • 2
  • 2
  • 1
    This seems to be the best solution, small and concise, and it isn't as limited as the switch. – Ivar Kallejärv Apr 19 '18 at 07:19
  • I really really like this implementation. Cuts down on the code that has to be used/generated. I ran into a problem with the last implementation of the mat tables with this before, refreshes were causing issues. This is clean though. – L.P. Jun 13 '18 at 14:42
  • 3
    I like this solutions too. I use `lodash` in my project so if you use `lodash`, this solution translates to this: `this.dataSource.sortingDataAccessor = _.get;` No need to reinvent the deep property access. – Andy Apr 11 '19 at 18:48
  • 2
    @andy you should make this a separate answer. it sounds too simple to be true in a comment.. Is that all I have to do? – Simon_Weaver Jun 27 '19 at 18:29
14

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;
}
Erik Schaareman
  • 141
  • 1
  • 2
11

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;
Toby Harris
  • 111
  • 1
  • 5
  • 1
    1000% should be the accepted solution. This was the only solution that didn't throw typeErrors for me. – Matt Westlake Feb 07 '21 at 03:28
  • Just don't forget to mention that matColumnDef needs to match displayedColumns as for path.property like "Address.CompanyName" for both. This answer saved me. – Marc Roussel Nov 30 '21 at 18:52
9

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.

Andy
  • 151
  • 1
  • 5
  • 1
    Works wonderfull, but for anyone struggling: you should name `displayedColumns`'s as the path to the values, i.e. `['title', 'value', 'user.name'];` and then use `` in your template. – Jeffrey Roosendaal Feb 11 '20 at 14:50
  • 1
    Alternatively, you can leave the column names as-is and override the sortHeaderId independently via `mat-sort-header` e.g. `mat-sort-header="user.name"` – p4m Dec 22 '20 at 12:45
1

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;
  };
E.Sarawut
  • 11
  • 4
  • Your solution helped me the most as I realized I could return number or string. My table has both types and needed to be sorted where numbers were sorted numerically and not like strings. Using the ternary operator that checks for typing was the key to the solution. – TYMG Jan 28 '19 at 17:23
  • I got `Cannot find name '_isNumbervalue`, and assuming this is a lodash method, I can't find the method in the node module. `isNumber`exists. I'm not previously familiar with lodash if that's what this is. How do I use this? – Rin and Len May 10 '19 at 10:57
  • 1
    import {_isNumberValue} from "@angular/cdk/coercion"; – E.Sarawut May 15 '19 at 10:26
1

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!

Tim Harker
  • 2,307
  • 1
  • 14
  • 27
1

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];
};
Chan Jing Hong
  • 1,843
  • 3
  • 18
  • 37
1

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.

Mikelgo
  • 399
  • 1
  • 13
0

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.

funkizer
  • 4,220
  • 1
  • 15
  • 19
0

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];
    }
  };
kawthar
  • 119
  • 1
  • 3
0

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;
}
Priti jha
  • 85
  • 4
0

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);
  };
}

Suhail AKhtar
  • 1,287
  • 11
  • 25