21
votes

I am trying to filter Array data based on multiple columns using only one Pipe. Right now, It filters first column value. Please check my below code and help me to sort this out.

My Code:

@Pipe({ name: "dataFilter", pure: false })
export class DataFilterPipe implements PipeTransform {
    transform(value: Array<any>, filter: any[]) {
        if (!filter) {
            return value;
        } else if (value) {
            return value.filter(item => {
                for (var i = 0; i < filter.length; i++) {
                    if (filter[i][1] == undefined) {
                        return true;
                    } else if ((typeof item[filter[i][0]] === 'string' || item[filter[i][0]] instanceof String) &&
                        (item[filter[i][0]].toLowerCase().indexOf(filter[i][1]) !== -1)) {
                        return true;
                    }
                    return false;
                }
            });
        }
    }
}

I am passing data like dataFilter : [['column1',value1],['column2',value2],['column3',value3]].

7
Could you provide an example value? Is it on the form: [{col1:"col1",col2:"col2",col3:"col3"},...,{}]? - John

7 Answers

30
votes

Here is a solution using the object passed as multiple columns filter. I found it more convenient then passing a 2D array:

    @Pipe({
        name: 'filter'
    })
    export class FilterPipe implements PipeTransform {
        transform(items: Array<any>, filter: {[key: string]: any }): Array<any> {
            return items.filter(item => {
                const notMatchingField = Object.keys(filter)
                                             .find(key => item[key] !== filter[key]);

                return !notMatchingField; // true if matches all fields
            });
        }
    }

Having an array of objects with multiple columns:

this.people = [
  {name: 'John', age: 27, sex: 'male'},
  {name: 'Lara', age: 21, sex: 'female'},
  {name: 'Rick', age: 29, sex: 'male'},
  {name: 'Eva',  age: 27, sex: 'female'},
  {name: 'Mike', age: 27, sex: 'male'}
];

And a filter:

this.peopleFilter = {age: 27, sex: 'male'};

Use it like:

 <div *ngFor="let person of people | filter: peopleFilter;"></div>

As a result, two people are matching our criteria: John and Mike.

Here is the working plunker: Multiple columns filter pipe demo.

5
votes

Here is what I did with Angular 8:

Goal: Search on multiple properties say "Property1" and "Property2" from a list of items for a given keyword:

app.module.ts:

......
import { MyFilterPipe } from './shared/pipes/my-filter.pipe';

@NgModule({
  declarations: [
    ...
    MyFilterPipe
  ],
  imports: [
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Pipe: content-filter.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'myFilter'
})
export class MyFilterPipe implements PipeTransform {

  transform(items: any[], keyword: any, properties: string[]): any[] {
    if (!items) return [];
    if (!keyword) return items;
    debugger;
    return items.filter(item => {
      var itemFound: Boolean;
      for (let i = 0; i < properties.length; i++) {
        if (item[properties[i]].toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
          itemFound = true;
          break;
        }
      }
      return itemFound;
    });

  }
}

component:

<input type="search" class="form-control filter-list-input" placeholder="Filter"
                  aria-label="Filter" name="search" [(ngModel)]="searchText" >

    <div *ngFor="let itemof myItems | myFilter:searchText:['Property1', 'Property2']; let i = index">...
</div>

component.ts:

export class MyListComponent implements OnInit {

  ...
  searchText: string;
1
votes

Replace Your code as below,

 export class DataFilterPipe implements PipeTransform {
   transform(value: Item[], field: string, args: string): Item[]{
      let filter: string = args ? args.toLocaleLowerCase() : null;
      return filter ? value.filter((item : Item) =>
          Item[field].toLocaleLowerCase().indexOf(filter) != -1) : value;
   }
}

In Html page,

 <tbody *ngFor="let item of items | dataFilter : columnName : value ">
1
votes

I am assuming that you have an array with columns like this:

[{col1:"col1",col2:"col2",col3:"col3"}]

I have also omitted all type-checkin, null-pointers, and error-handling. The current solution is an example I tried with the array:

myData:Array<any> = [{col1:"a",col2:"b",col3:"cadd"},
    {col1:"abba",col2:"bobba",col3:"cadd"},
    {col1:"abba",col2:"bobba",col3:"cool"},
    {col1:"a",col2:"bobba",col3:"cool"}];

and the pipe:

@Pipe({
  name: 'dataFilter'
})
export class DataFilterPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    return value.filter(item =>{
      var matchesAll = true;
      for(var i = 0; i<args.length; i++){
        // check if your data contains the column and the value defined in args.
        if(item.hasOwnProperty(args[i][0]) && item[args[i][0]]==args[i][1]){
          continue;
        }else{ // at least one column did not match,
          matchesAll = false;
        }
      }
      return matchesAll;
    });
  }

}

You can then call

dataFilter.transform(myData,[["col1","abba"],["col2","bobba"],["col3","cool"]]);

in order to get one result, which is row number 3 after transformation: [{col1:"abba",col2:"bobba",col3:"cool"}].

Note: You may have to adjust the names of the columns in my example to make it work with your code.

EDIT: With this solution, you can also pass arbitrary number of columns.

e.g dataFilter.transform(myData,[["col3","cool"]]);

which will result in the two last rows (from my example) after transformation:

[{col1:"abba",col2:"bobba",col3:"cool"},{col1:"a",col2:"bobba",col3:"cool"}]

EDIT: after comment stating that the code is not working, I provided a plunkr of the example above: https://plnkr.co/edit/VdpGJWyzWUVFzYNDSz1g

0
votes

You can see the whole working ArrayFilterPipe.ts @ http://typescript.io/uN29xRdF1Ag or typescriptlang.org
Also below I have attached the compile JS of the typescript for ease of seeing your desired output. Let me know if want the code commented ...

FYI options array is cnverted to object before performing the filter, as it feels a lot comfortable that the array.

/* yours */
var option = [['column1',value1],['column2',value2],['column3',value3]];
/* mine */
var option = { column1: 'value1', column2: 'value2', column3: 'value3' };

static _arrayToObject(value: Array<Array<any>>): any {
    return (ArrayFilterPipe._isUndefined(value) || value == null) 
    ? value 
    : value.reduce((result, current) => {   
                    result[current[0]] = current[1]; 
                    return result; 
                } , {});
}

You can also build the array/object filter similar to angular 1.x by getting some inspiration from https://github.com/angular/angular.js/blob/master/src/ng/filter/filter.js

var ArrayFilterPipe = (function () {
    function ArrayFilterPipe() {
    }
    ArrayFilterPipe._isOfType = function (value, type) {
        return typeof (value) === type;
    };
    ArrayFilterPipe._isUndefined = function (value) {
        return ArrayFilterPipe._isOfType(value, ArrayFilterPipe.TYPES.UNDEFINED);
    };
    ArrayFilterPipe._isObject = function (value) {
        return ArrayFilterPipe._isOfType(value, ArrayFilterPipe.TYPES.OBJECT);
    };
    ArrayFilterPipe._isOrHasMatch = function (value, target) {
        return ArrayFilterPipe._isOfType(value, ArrayFilterPipe.TYPES.STRING) || ArrayFilterPipe._isOfType(target, ArrayFilterPipe.TYPES.STRING)
            ? value.toString().toLowerCase().indexOf(target.toString().toLowerCase()) >= 0
            : value == target;
    };
    ArrayFilterPipe._hasOptions = function (value, options) {
        return (ArrayFilterPipe._isUndefined(value) || ArrayFilterPipe._isUndefined(options) ? (ArrayFilterPipe._isUndefined(value) && ArrayFilterPipe._isUndefined(options))
            : (value === null || options == null) ? (value === null && options == null)
                : (Array.isArray(value) || Array.isArray(options)) ? false
                    : ArrayFilterPipe._isObject(value) ?
                        (ArrayFilterPipe._isObject(options)
                            ? Object.keys(options).every(function (key) { return value.hasOwnProperty(key) && ArrayFilterPipe._isOrHasMatch(value[key], options[key]); })
                            : Object.values(value).some(function (val) { return ArrayFilterPipe._isOrHasMatch(val, options); }))
                        : !ArrayFilterPipe._isObject(value) ?
                            (!ArrayFilterPipe._isObject(options)
                                ? ArrayFilterPipe._isOrHasMatch(value, options)
                                : false)
                            : false);
    };
    ArrayFilterPipe._arrayToObject = function (value) {
        return (ArrayFilterPipe._isUndefined(value) || value == null) ? value : value.reduce(function (result, current) {
            result[current[0]] = current[1];
            return result;
        }, {});
    };
    ArrayFilterPipe.prototype.transform = function (value, options) {
        if (!value || !Array.isArray(value) || ArrayFilterPipe._isUndefined(options) || options === null) {
            return value;
        }
        options = Array.isArray(options) ? ArrayFilterPipe._arrayToObject(options) : options;
        return value.filter(function (item) { return ArrayFilterPipe._hasOptions(item, options); });
    };
    return ArrayFilterPipe;
}());
ArrayFilterPipe.TYPES = {
    OBJECT: 'object',
    STRING: 'string',
    UNDEFINED: 'undefined'
};
/*
    TESTING
    --------------------------------------------------
*/
var pipe = new ArrayFilterPipe();
var array = [null, undefined, , true, 123123, 'Jeke HeNry', 'joe', 'joe hen', {}, [], { fake: 'hen' }, { name: 'hen' }, { name: 'johenrik', country: 'hen' }, { name: 'joe dick', city: 'hen' }, { name: 'Jeke HeNry', country: 'zxy' }];
var options = null;
/* REF:  http://stackoverflow.com/questions/11403107/capturing-javascript-console-log */
var oldLog = console.log;
console.log = function (message) {
    var _arguments = arguments;
    var div = Object.keys(arguments).map(function (key) { return Number(key); }).reduce(function (result, key) {
        result = result || document.createElement('div');
        var isJSON = (_arguments[key] != null && (typeof (_arguments[key]) === 'object' || Array.isArray(_arguments[key])));
        var span = document.createElement(isJSON ? 'pre' : 'span');
        span.innerHTML = isJSON ? JSON.stringify(_arguments[key], undefined) : _arguments[key].replace('\n', '</br></br>');
        result.appendChild(span);
        return result;
    }, null);
    document.body.appendChild(div);
    oldLog.apply(console, arguments);
};
function test() {
    console.log('options', options);
    console.log('result', pipe.transform(array, options));
}
console.log('case : 01');
console.log('---------------------------------------------------');
options = 'hen';
test();
console.log('\ncase : 02');
console.log('---------------------------------------------------');
options = { name: 'hen' };
test();
options = [['name', 'hen']];
test();
console.log('\ncase : 03');
console.log('---------------------------------------------------');
options = { name: 'hen', country: 'hen' };
test();
options = [['name', 'hen'], ['country', 'hen']];
test();
console.log('\ncase : 04');
console.log('---------------------------------------------------');
options = { name: 'hen', city: 'hen', fake: true };
test();
options = [['name', 'hen'], ['country', 'hen'], ['fake', true]];
test();
0
votes

I had to have a Pipe that could filter by both a specific property value AND a generic value called "All". A user could adjust these filters dynamically by selecting them from a dropdown (thus the use of the more inefficient pure: false config).

In the component (had to default to 'All')

public filters = { status: "All", condition: "All", other: "All" };

In the template

<div *ngFor="let item of items | filter:filters">
@Pipe({
  name: 'filter',
  pure: false
})
export class FilterPipe implements PipeTransform {
  transform(items: Array<any>, filter: {[key: string]: any }): Array<any> {
    return items.filter(item => {
      let matches = Object.keys(filter).every(f => {
        return filter[f] === 'All' || item[f] == filter[f];
      })

      return matches;
    })
  }
}
0
votes

HTML MarkUp:

*ngFor="let item of _items | filtername:{ column1: searchtext, column2: searchtext } : false"

Transform Function:

transform(items: any, filter: any, isAndOperationBwCol: bool): any {
  if (filter && Array.isArray(items)) {
   let filterKeys = Object.keys(filter);
    if (isAndOperationBwCol) {
     return items.filter(item =>
        filterKeys.reduce((memo, keyName) =>
            (memo && new RegExp(filter[keyName], 'gi').test(item[keyName])) || filter[keyName] === "", true));
    } else {
     return items.filter(item => {
      return filterKeys.some((keyName) => {
        console.log(keyName);
        return new RegExp(filter[keyName], 'gi').test(item[keyName]) || filter[keyName] === "";
      });
    });
   }
  } else {
   return items;
  }
}

Ref: https://long2know.com/2017/04/angular-pipes-filtering-on-multiple-keys/