Components: [Table] Support sorting / filtering table with pipes

Created on 14 Jun 2018  路  15Comments  路  Source: angular/components

Bug, feature request, or proposal:

Request

What is the expected behavior?

I use the MatTable feature.

My data source contains a bunch of ids, so most of my columns is bound to an id property and use a pipe to display the name.

For example:
<ng-container matColumnDef="type"> <th mat-header-cell *matHeaderCellDef> Type </th> <td mat-cell *matCellDef="let device"> {{product.TypeId | productTypePipe}} </td> </ng-container>

And it works great!

However when I wanted to implement sort or filter I stumble some difficulties.

Apparently the table uses the model to sort / filter data.

However I wish to sort the table using the piped value and not the id value.

Likewise, I would like to filter by the piped value (which are strings) and not the ids (which the client is not aware of).

For the sorting issue I tried to use the sortingDataAccessor however I couldn't use the services that translate the ids into names. (I guess because the context is different)

What is the current behavior?

the current behavior uses the model given to the DataSource with no regard to what actually displayed in the table.

What are the steps to reproduce?

https://angular-material2-issue-m3sdhq.stackblitz.io
(Try to sort the symbol column)

Manual reproduce

  1. Create a pipe that translates a number to a string.
  2. create table that the cell uses this pipe.
  3. try to sort / filter by the value name.

What is the use-case or motivation for changing an existing behavior?

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Angular: Angular 6.
OS: Windows 7
TypeScript: 2.7.2.
Browsers: Chrome (But I believe every browser is affected)

Is there anything else we should know?

My pipes uses DI so I can just create instance of them (I wish I could)

Thanks

Most helpful comment

Modifying the data model to transform the data in the component comes with its own set of problems when using a Material Table.

For example, piping a Date using a DatePipe, shortDate format in the component code outputs a string in M/d/yy format. If the user then clicks the column header above the date field to sort, it will sort _alphabetically_ rather than _chronologically_ because it's a string and not a date.

Letting the true date exist in the data model, but still transforming it HTML-side using a DatePipe allows the data to display in a human-friendly format, and the sort to work as one would expect.

Unfortunately, the filter being tied to that same data model does not create an expected filtering behavior, as the user is filtering data that they may not be able to see (a true date rather than a formatted string).

All 15 comments

I guess you are fighting against the design here. All changes (filtering, sorting) must be applied to the data model and are then reflected in the ui.

So you should transform your data-model. You can inject your pipe in your component code and use it there to transform each item in the dataset. After that, you can use it as data source. Depending on your use case, you might even consider doing such transformations inside your services.

If I'll create a ViewModel for the table, then for every action originated from the table I'll have to search for the real model to execute the action.

There is one thing I don't understand, you already supply a method to override the sort logic with my own sort logic.

Why can't I use services inside the sortingDataAccessor method. Is it something that might be worked around using bind() ?
Maybe the right solution is to support using the class services inside this method?

What do you think?
Thanks,

If you use the view-model approach, then you simply include your model as a property in the view-model. This way, when there is an action and you receive the view-model, you can get the model by simply accessing the model property.

Alternatively, you can also skip the whole view-model part and simply add / set properties to your model when transforming your data.

This approach will also benefit performance, since angular has not to reevaluate your pipe expressions all the time.

Modifying the data model to transform the data in the component comes with its own set of problems when using a Material Table.

For example, piping a Date using a DatePipe, shortDate format in the component code outputs a string in M/d/yy format. If the user then clicks the column header above the date field to sort, it will sort _alphabetically_ rather than _chronologically_ because it's a string and not a date.

Letting the true date exist in the data model, but still transforming it HTML-side using a DatePipe allows the data to display in a human-friendly format, and the sort to work as one would expect.

Unfortunately, the filter being tied to that same data model does not create an expected filtering behavior, as the user is filtering data that they may not be able to see (a true date rather than a formatted string).

Modifying the data model to transform the data in the component comes with its own set of problems when using a Material Table.

For example, piping a Date using a DatePipe, shortDate format in the component code outputs a string in M/d/yy format. If the user then clicks the column header above the date field to sort, it will sort _alphabetically_ rather than _chronologically_ because it's a string and not a date.

Letting the true date exist in the data model, but still transforming it HTML-side using a DatePipe allows the data to display in a human-friendly format, and the sort to work as one would expect.

Unfortunately, the filter being tied to that same data model does not create an expected filtering behavior, as the user is filtering data that they may not be able to see (a true date rather than a formatted string).

Same "problem" here. Maybe the code behind the table could generate a model with the piped data and try to work with both piped data and the model data.

I also can't find a way to sort and filter my columns that uses pipe.

I would love to know more about how to implement the recommendation to => transform your data-model and inject your pipe in your component code and use it there to transform each item in the dataset.

@leongrin I'm not sure if this is what you're looking for, but here is an example of one approach that could be used to make the pipe's output accessible on a model, so that it can be used for sorting - hope it helps:

@Pipe({name: 'myExamplePipe'})
export class MyExamplePipe implements PipeTransform {
  transform(model: MyModel) {
    const transformedValue = ...;  // do the pipe transform here
    // Cache the transformed value on the model object so it can be used for sorting
    model.cachedTransformedValue = transformedValue;
    return transformedValue;
  }
}

EDIT: This does not work correctly if the table is paginated and there is more than one page of results - see https://github.com/angular/components/issues/11782#issuecomment-545647745.

@pshields I don't think this solution applies to my use case. Let me share part o my code so you can better understand where I am stuck.

table.component.html

<div class="table-responsive">
          <table mat-table class="full-width-table" [dataSource]="dataSource" matSort aria-label="Elements">

 <!-- Interactions Column -->
            <ng-container matColumnDef="interactions">
              <th mat-header-cell *matHeaderCellDef mat-sort-header>interactions</th>
              <td mat-cell *matCellDef="let row">{{ ((row.aliasId |
                getInteractions:startDate:
                endDate: row.profileType:
                row.userId | async)?.interactionsArray.length) | number : '1.2-2' }}</td>
            </ng-container>

GetInteractionsPipe

import { Pipe, PipeTransform } from '@angular/core';
import {ProfilePerformanceService} from '../services/profile-performance.service';

@Pipe({
  name: 'getInteractions',
  pure: true
})
export class GetInteractionsPipe implements PipeTransform {

  constructor(private profilePerformanceServ: ProfilePerformanceService) {
  }

  transform(
    value: any,
    startDate: Date,
    endDate: Date,
    profileType: string,  // 'practice' OR 'clinic'
    userId: string
  ): any {
    return this.profilePerformanceServ.getInteractions(
      startDate,
      endDate,
      profileType,
      userId
    );
  }
}

@leongrin Your pipe currently takes in an "aliasId" as input. If you change the pipe to take the whole table row as its input, you can stash the transformed value on the row object somewhere, and use it for sorting. It's not the cleanest approach, but it works, at least for my use case.

Once the pipe's transformed value is cached on the table row object, assuming you are using a MatTableDataSource, you can configure it to be used for sorting by setting

dataSource.sortingDataAccessor = (rowObject, columnName) => {
  switch(columnName) {
    case COLUMN_I_WANT_TO_SORT_BY_PIPED_VALUE:
      return rowObject['thePropertyNameWhereThePipedValueIsCached'];
    default:
      return rowObject[columnName];
  }
};

@pshields Thank you a lot for helping me get this sorting working. But I couldn't implement your solution. Probably because I don't know how to execute this recommendation: 'you can stash the transformed value on the row object somewhere'.

@leongrin I'll try one more time to illustrate what I mean. In your code, you would change the transform method in your pipe as follows:

transform(
  rowObject: any,  // this is a reference to the row object
  ...  // leave other params the same
): any {
  const transformedValue = this.profilePerformanceServ.getInteractions(
    startDate,
    endDate,
    profileType,
    userId
  );
  // Save the transformed value onto the row object for future access by the sort data accessor
  rowObject.cachedInteractions = transformedValue;
  // Also return it from the pipe
  return transformedValue;
}

Then in your template, you would pass in the whole row object to the pipe:

<td mat-cell *matCellDef="let row">{{ ((row |
      getInteractions:startDate:
      endDate: row.profileType:
      row.userId | async)?.interactionsArray.length) | number : '1.2-2' }}</td>

Then somewhere in your component, you would set the sorting data accessor:

dataSource.sortingDataAccessor = (rowObject, columnName) => {
  switch(columnName) {
    case 'interactions':
      return rowObject.cachedInteractions;
    default:
      return rowObject[columnName];
  }
};

EDIT: This does not work correctly if the table is paginated and there is more than one page of results - see https://github.com/angular/components/issues/11782#issuecomment-545647745.

@pshields Thank you very much. Your final explanation solved my problem. Have a great day.

FYI: I just realized that the approach I mentioned above doesn't correctly handle the case where the results are paginated, since only pages that have been viewed will have cached results available to be used for sorting. Because of that limitation, I plan to switch my code to a different approach which does not require a pipe's output for sorting.

That's true. Thank you for the update.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings