Components: [Table] Ability to filter complex objects

Created on 7 Feb 2018  路  16Comments  路  Source: angular/components

Bug, feature request, or proposal:

Feature Request

What is the expected behavior?

When a data source contains sub-objects, those fields should also be filterable with the standard filter

What is the current behavior?

Only top level fields on the object are filtered

What are the steps to reproduce?

StackBlitz: https://angular-material2-issue-rcbyat.stackblitz.io

  1. Type Helium in the filter bar
  2. Observe no records displayed

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

To be able to filter complex objects in the data table

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

angular: 5.1.1
material: 5.04
typescript: 2.5.3

Most helpful comment

Sounds like you want a custom filter function, which you can do by overriding the filterPredicate.

The default case performs only a shallow search because it is optimized for simple data objects.

Here's an example of a filter predicate that you can use:

this.dataSource.filterPredicate = (data, filter) => {
  const dataStr = data.position + data.details.name + data.details.symbol + data.details.weight;
  return dataStr.indexOf(filter) != -1; 
}

Live example: https://stackblitz.com/edit/angular-material2-issue-6o5ukn?file=app/app.component.ts

For more info, see https://material.angular.io/components/table/overview#filtering

All 16 comments

Sounds like you want a custom filter function, which you can do by overriding the filterPredicate.

The default case performs only a shallow search because it is optimized for simple data objects.

Here's an example of a filter predicate that you can use:

this.dataSource.filterPredicate = (data, filter) => {
  const dataStr = data.position + data.details.name + data.details.symbol + data.details.weight;
  return dataStr.indexOf(filter) != -1; 
}

Live example: https://stackblitz.com/edit/angular-material2-issue-6o5ukn?file=app/app.component.ts

For more info, see https://material.angular.io/components/table/overview#filtering

Thanks for the prompt, detailed and helpful response, tried this out and it works perfectly

i suggest to use for complex object a filter like:
this.dataSource.filterPredicate = (data, filter) => JSON.stringify(data).includes(filter);

@andrewseguin could be used as default ?

@andrewseguin
I was reviewing the example but it isn't working, as it should. If you type to filter the string 1H then you will get result. The combination of 1H doesn't exist in any column but because the last letter of the first column is "1" and the first letter of the second column is "H" it will filter it. So it looks like the first and second column has been concatenated during the filtering.

The question is if you want your filter work as the example. I would say it is very rare especially if you have many columns. Usually you want to filter a single column or all columns but not as a concatenated string between all columns.

The question is what is a solution to this? I have been trying for several hours now without any success. (I鈥檓 using Angular for the first time.)

@KiarashE
Hi, if you want to check if the filter is contained in at least one of the columns without using a concatenated string, you can use map() to iterate over all the properties of your object and check if the filter is contained in the value of each property. If at least one of the properties contain the filter, the function is going to return true and the record is going to be shown. Additionally, if your object contains sub-objects you can check them as well, as shown in the example below, where I have an Order object and one of its properties is a Client object.

this.dataSource.filterPredicate = (order: Order, filter: string) => {
  let valid = false;

  const transformedFilter = filter.trim().toLowerCase();

  Object.keys(order).map(key => {
    if (
      key === 'client' &&
      (
        order.client.id.toLowerCase().includes(transformedFilter)
        || order.client.idType.toLowerCase().includes(transformedFilter)
        || order.client.name.toLowerCase().includes(transformedFilter)
      )
    ) {
      valid = true;
    } else {
      if (('' + order[key]).toLowerCase().includes(transformedFilter)) {
        valid = true;
      }
    }
  });

  return valid;
};

Hope that helps, I'm also new to Angular.

@xanscale
In my opinion, the downside of using JSON.stringify() here is that the names of the properties are also included in the string, for example, if your object is

{
  "name": "Bob"
}

the string is going to be {"name":"Bob"} and if your filter is "name" that record is going to be shown as well.

@AxiomSword
your solution does not works for nested objects,

PS seams something strange in if clausole

@xanscale
Could you please tell me why it doesn't work?

In the example I'm using nested objects, also, here is a live example where I use the same approach (also with a nested object) and it seems to work correctly: https://stackblitz.com/edit/angular-material2-issue-a9aokb

Try searching "1H", no records will be shown, as @KiarashE requested, and if you search "Hydrogen", one record will appear, which proves that the function is working with nested objects.

I used the if statement to intentionally check only for some properties of the nested object, but of course you also can check all of them:

this.dataSource.filterPredicate = (order: Order, filter: string) => {
  let valid = false;

  const transformedFilter = filter.trim().toLowerCase();

  Object.keys(order).map(key => {
    if (key === 'client' /* || key === 'otherNestedObject'*/) {
      Object.keys(order[key]).map(nestedKey => {
        if (('' + order[key][nestedKey]).toLowerCase().includes(transformedFilter)) {
          valid = true;
        }
      });
    } else {
      if (('' + order[key]).toLowerCase().includes(transformedFilter)) {
        valid = true;
      }
    }
  });

  return valid;
}

Let me know if I am missing something.

@AxiomSword

in first snippet you have just one Object.keys, in second twice. now support only 2 level.

my suggestion support infinite

@xanscale
Yes, I completely agree with you, but as I said before, I think that the downside of your suggestion is that JSON.stringify() will also include the names of the properties in the strings, after I did some researching I come up with the following:

this.dataSource.filterPredicate = (order: Order, filter: string) => {
  const transformedFilter = filter.trim().toLowerCase();

  const listAsFlatString = (obj): string => {
    let returnVal = '';

    Object.values(obj).forEach((val) => {
      if (typeof val !== 'object') {
        returnVal = returnVal + ' ' + val;
      } else if (val !== null) {
        returnVal = returnVal + ' ' + listAsFlatString(val);
      }
    });

    return returnVal.trim().toLowerCase();
  };

  return listAsFlatString(order).includes(transformedFilter);
};

That way each order object will be converted to a string, without including the names of the properties of the object or the nested objects, the string will only contain the values, regardless of the amount of nested objects.

Edit: Changed the code so that everything is inside filterPredicate().

This could be default filterpredicate

@AxiomSword Funciona perfecto, much铆simas gracias.

chef-d'oeuvre, Merci beaucoup

Hi, I have a problem using filterPredicate with values calculated outside the dataSource:

The API returns the messages and the ID of the user who sent it. I cannot edit the API. So what I want to do, is to replace inside the filter the ID with the actual name of the user. I use the same method findUserName to replace the ID with the name in the mat-table. But the filter is a whole different story.

findUserName(id: string) {
    return this.users.find(m => m.id === id).nombre;
}

filterPredicate(persona: Message, filter: string): boolean {
    const filterTokens = filter.trim().toLowerCase().split(' ');
    let tokens = persona.obsMtvMje.trim().toLowerCase().split(' ');
    const user = this.findUserName(persona.codUsrEnv);
    tokens = tokens.concat(user.trim().toLowerCase().split(' '));

    return filterTokens.every(ft => tokens.some(tok => tok.substring(0, ft.length).indexOf(ft) > -1));
}

Because this is the dataSource, it can not find the method findUserName.

Any ideas?

This is just an extension to the above solution which seems to be very generic and works for all types of json structure

this.dataSource.filterPredicate = (data: any, filter) => { const dataStr =JSON.stringify(data).toLowerCase(); return dataStr.indexOf(filter) != -1; }

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