esvit / ng-table

Simple table with sorting and filtering on AngularJS
http://esvit.github.io/ng-table
BSD 3-Clause "New" or "Revised" License
2.77k stars 851 forks source link

Filtering a complex property is broken when using AngularJS v1.2.21 #388

Closed craggsi closed 9 years ago

craggsi commented 10 years ago

Here's how to reproduce:-

Edit the Demo 4 example (Table with filters). Add an additional 'Status' property to each data row. Note this is a complex object property:-

var data = [{name: "Moroni", age: 50, Status: { Id:1, Description: 'AVAILABLE' }}, {name: "Tiancum", age: 43, Status: { Id:1, Description: 'AVAILABLE' }}, {name: "Jacob", age: 27, Status: { Id:1, Description: 'AVAILABLE' }}, {name: "Nephi", age: 29, Status: { Id:1, Description: 'AVAILABLE' }}];

Finally, edit the HTML table to include an additional column as shown below:-

"< td data-title="'Status'" filter="{ 'Status.Description' : 'text' }" > {{user.Status.Description}} < /td >"

When using AngularJS v1.2.0-rc.2, the filter works for both the simple property (name) and the complex property, however when you reference AngularJS v1.2.21, the filter dismisses valid Status Description entries and so all rows are filtered out.

For example, the filter { "Status.Description": "av" } should return all data rows, but instead all rows are filtered out.

No errors are shown in the browser console.

monad98 commented 10 years ago

I have a same problem. Filter doesn't work for nested property.

assisrafael commented 10 years ago

I'm also having the same problem. This is the workaround (it only handles the case 'a.b') that I'm using:

getData: function($defer, params) {
    var filters = {};

    angular.forEach(params.filter(), function(value, key) {
        var splitedKey = key.match(/^([a-zA-Z]+)\.([a-zA-Z]+)$/);

        if(!splitedKey) {
            filters[key] = value;
            return;
        }

        splitedKey = splitedKey.splice(1);

        var father = splitedKey[0],
        son = splitedKey[1];
        filters[father] = {};
        filters[father][son] = value;
    });

    // use build-in angular filter
    var filteredData = params.filter() ? $filter('filter')(data, filters) : data;
}
gfreeau commented 10 years ago

@assisrafael thanks for the idea. I extended it to go multiple levels deep.

getData: function($defer, params) {
    var filters = {};

    angular.forEach(params.filter(), function(value, key) {
        if (key.indexOf('.') === -1) {
            filters[key] = value;
            return;
        }

        var createObjectTree = function (tree, properties, value) {
            if (!properties.length) {
                return value;
            }

            var prop = properties.shift();

            if (!prop || !/^[a-zA-Z]/.test(prop)) {
                throw new Error('invalid nested property name for filter');
            }

            tree[prop] = createObjectTree({}, properties, value);

            return tree;
        };

        var filter = createObjectTree({}, key.split('.'), value);

        angular.extend(filters, filter);
    });

    var filteredData = params.filter() ? $filter('filter')(data, filters) : data;
}
assisrafael commented 10 years ago

Thanks for sharing.

barroudjo commented 10 years ago

Just to let you guys know: if you tried to filter on two nested properties at the same time it does not work. I explained the problem here: https://github.com/angular/angular.js/issues/9698.

The solution is to both use a custom comparator function and redefine the filters as explained here (by the way I don't think going deeper than one level is any use when redefining the filter, at least in my solution as I use eval) :

compareForNestedFiltering = function (actual, expected) {
    function contains (actualVal, expectedVal) {
        return actualVal.toString().toLowerCase().indexOf(expectedVal.toString().trim().toLowerCase()) !== -1;
    }
    if(typeof expected !== 'object') return contains(actual, expected);
    var result = Object.keys(expected).every(function (key) {
        return contains(eval('actual.'+key), eval('expected.'+key));
    });
    return result;
};

formatNestedFilters = function (filters) {
    var reformattedFilters = {};
    angular.forEach(filters, function(value, key) {
        var firstDotPos = key.indexOf('.');
        if(firstDotPos == -1) return reformattedFilters[key] = value;
        var firstKey = key.slice(0, firstDotPos);
        var remainingKey = key.slice(firstDotPos+1);
        if (!reformattedFilters.hasOwnProperty(firstKey)) reformattedFilters[firstKey] = {};
        reformattedFilters[firstKey][remainingKey] = value;
    });
    return reformattedFilters;
};

var filteredData = $filter('filter')(data, formatNestedFilters(params.filter()), compareForNestedFiltering )
craggsi commented 10 years ago

Thanks for all your posts! Back when I posted the issue, time was against me, so I created a flat view model and mapped my nested object hierarchy to it. I suppose it all depends on your situation whether this or one of the above approaches is needed.

Regards Simon

appagg commented 10 years ago

Thank you, craggsi and barroudjo for bringing up this problem and sharing the workaround.

hendricius commented 9 years ago

@barroudjo thanks that worked.

hendricius commented 9 years ago

Another workaround that is a bit of hack but I like it. Only works in newer browsers that support javascript setters though:

Object.defineProperties(this.prototype, {
  vesselCode: {
    get: function() {
      if (!this.vessel) {
        return;
      }
      return this.vessel.code;
    }
  }
});

In this case in the UI you can just use something like this:

<td data-title="'Vessel'" sortable="'vesselCode'" filter="{ 'vesselCode': 'text' }">
  {{project.vessel.code }}
</td>
hendricius commented 9 years ago

@iyel fixed this in master?

iyeldinov commented 9 years ago

@hendricius I was closing issues just to understand which of them are still relevant. Please read https://github.com/esvit/ng-table#submitting-an-issue