122

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,620
  • 2
  • 23
  • 32
Roman
  • 3,011
  • 2
  • 20
  • 30

16 Answers16

240

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
  • 8,444
  • 2
  • 30
  • 32
  • 1
    where did you get `sort` from in `this.dataSource.sort = sort;` – Joey Gough Oct 23 '18 at 14:49
  • 9
    I had to place this in `ngAfterViewInit` for it to work – Mel Nov 15 '18 at 15:48
  • placed this next to my table declaration and it worked instantly. Saved me a ton of debugging. thanks! – JamieT Apr 03 '19 at 17:58
  • 1
    Need to be done each time the `MatTableDataSource` is changed (seems logic cause it encapsulate the `sortingDataAccessor` but anyway). Thank you ! – Quentin Klein May 03 '19 at 13:41
  • Shouldn't it be `case 'project.name': return item['project']['name'];`? –  Jul 17 '19 at 09:40
  • @JoeyGough from the ViewChild – Mawcel Jul 25 '19 at 09:10
  • 1
    When using "strict" TypeScript, `item[property]` will cause errors (assuming `item` is some typed object). For those situations I found this answer useful: https://stackoverflow.com/a/55108590/53538 which is about forcing a typed object to be "indexable". – Guss Aug 26 '20 at 12:36
43

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
  • 431
  • 3
  • 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
  • 4
    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
  • 3
    @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
19

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
  • 191
  • 1
  • 2
13

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
  • 131
  • 1
  • 5
  • 3
    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
12

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
  • 193
  • 1
  • 8
  • 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
2

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
  • 2,251
  • 4
  • 22
  • 41
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,367
  • 1
  • 15
  • 28
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
  • 483
  • 4
  • 15
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,626
  • 1
  • 18
  • 20
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
  • 145
  • 1
  • 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,718
  • 15
  • 29
0

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;

  })
}
Botond
  • 630
  • 6
  • 9
0

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;
.....
Deepu Reghunath
  • 8,132
  • 2
  • 38
  • 47
0
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