130

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();
  }
}
1
  • 3
    Could you please update the stackblitz with the fix Commented Jan 22, 2020 at 5:10

15 Answers 15

252

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;
}
Sign up to request clarification or add additional context in comments.

7 Comments

where did you get sort from in this.dataSource.sort = sort;
I had to place this in ngAfterViewInit for it to work
placed this next to my table declaration and it worked instantly. Saved me a ton of debugging. thanks!
Need to be done each time the MatTableDataSource is changed (seems logic cause it encapsulate the sortingDataAccessor but anyway). Thank you !
When using "strict" TypeScript, item[property] will cause errors (assuming item is some typed object). For those situations I found this answer useful: stackoverflow.com/a/55108590/53538 which is about forcing a typed object to be "indexable".
|
46

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>

4 Comments

This seems to be the best solution, small and concise, and it isn't as limited as the switch.
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.
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 you should make this a separate answer. it sounds too simple to be true in a comment.. Is that all I have to do?
18

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

Comments

14

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;

2 Comments

1000% should be the accepted solution. This was the only solution that didn't throw typeErrors for me.
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.
14

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.

2 Comments

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 <ng-container matColumnDef="user.name"> in your template.
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"
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];
};

Comments

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

3 Comments

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.
I got Cannot find name '_isNumbervalue, and assuming this is a lodash method, I can't find the method in the node module. isNumberexists. I'm not previously familiar with lodash if that's what this is. How do I use this?
import {_isNumberValue} from "@angular/cdk/coercion";
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!

Comments

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.

Comments

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.

Comments

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

1 Comment

This answer was already given: stackoverflow.com/a/49057493/5468463
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);
  };
}

Comments

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;

  })
}

Comments

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;
.....

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.