Custom filter with AngularJS ngTable

David M. Karr picture David M. Karr · Mar 17, 2014 · Viewed 32.6k times · Source

I'm trying to construct a table using ngTable, but with different custom filtering than described in the example from the ngTable page.

I want filtering in place, but I don't want ngTable to render the filter selectors. I want to render them myself (above the table), and then reference them in my "getData()" method.

The example referred to earlier doesn't explain how any of that machinery works. I have no idea exactly what needs to be specified in the "filter" property in each "td" element. I understand the basic syntax of the AngularJS $filter function, but I'm not clear on what ngTable is doing with this. From the one example, it looks like I can only do "equals" checking, which would only select rows where the associated column value is equal to the filter value. That isn't quite what I need.

My table has several columns. Two of them are called "key" and "failed", being string and boolean respectively. When I'm rendering these filter fields above the table, I need a custom label for the "failed" filter. Filtering for the "key" column should match the filter value with any substring of the "key" values. For instance, if I have key values of "abc", "abac", and "def", a filter value of "a" will result in the first two entries showing, and not showing the "def" entry.

Update:

Related to this, I wish I could figure out how to do something like this:

Let's say I have a ngRepeat expression in my table element like this, using "standard" angularjs filters:

"item in $data | customfilter:param | anothercustomfilter:param"

We know that this doesn't quite work, as those filters will only apply to one page slice obtained from the "getData()" method. What I'd really like to be able to do in my "getData()" method is simply access the entire filter chain, including the parameter expressions, and simply pass a different array into it, being the entire original data list, not just the page slice.

At the same time, I'd need to be able to "turn off" the filtering angularjs is doing by itself, by executing that filter chain in its normal processing.

This sounds difficult, but I find the current API requires a lot of coupling between the html and the javascript. It would be nice if the html could specify the desired filtering, and the javascript would just use that entire filter chain, but use it on the entire data list, not just the page slice.

Update:

Here's a relevant excerpt from my HTML:

<label for="keysFilter">Filter Keys:</label>
<input id="keysFilter" type="text" ng-model="keysFilter"/>
<label for="showOnlyFailed">Show only queries that failed?</label>
<input id="showOnlyFailed" type="checkbox" ng-model="showOnlyFailed"/>
<table ng-table="tableParams" table-pagination="custom/pages" class="table">
<tr ng-repeat="queryInfo in $data"> <!--  | filterFailed:showOnlyFailed | filterMatchingKeys:keysFilter -->

Here's my tableParams code:

$scope.tableParams  = new ngTableParams({
    page: 1,
    count: 10,
    sorting: {
        lastRun: 'desc'
    }
},
{
    debugMode: true,
    total:  $scope.completedQueries.length,
    getData:    function($defer, params) {
        var orderedData = params.sorting() ?
                $filter('orderBy')($scope.completedQueries, params.orderBy()) :
                data;
        orderedData = $filter('filterFailed')(orderedData, $scope.showOnlyFailed);
        orderedData = $filter('filterMatchingKeys')(orderedData, $scope.keysFilter);

        params.total(orderedData.length);
        $defer.resolve(orderedData.slice((params.page() - 1) * params.count(),
                                                     params.page() * params.count()));
    }
});

Note that I used have this ngTable not using the "$data" list, and just iterating through my "completedQueries" list. When it worked like that, the list would immediately change when I clicked on the "Show only queries that failed" checkbox, or entered text in the "keysFilter" input field.

However, now that I'm using the "$data" list, nothing happens when I change either of those fields. In fact, I even added $watch-es for both of those fields, and neither of them fire. However, when I make changes to either of those fields, I know the table data is being reevaluated, because two of the columns have data that are expected to be millis value, and I have a custom filter on those columns that translate the value to a "time ago" english expression, like "30 seconds ago", or "2 minutes ago", and each time I change one of those input fields, I see those expressions in the table change, but it still doesn't do the proper filtering.

If it matters, here are the $watch-es that I added to my scope. These never appear to fire:

    $scope.$watch("showOnlyFailed", function() {
    $scope.tableParams.reload();
});

$scope.$watch("keysFilter", function() {
    $scope.tableParams.reload();
});

Note that when I have these commented in, I see the following error after I hit my "getData()" method:

Error: settings.$scope is null
@http://localhost:8000/js/diag/libs/ng-table.src.js:411
qFactory/defer/deferred.promise.then/wrappedCallback@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:11046
qFactory/ref/<.then/<@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:11132
Scope.prototype.$eval@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:12075
Scope.prototype.$digest@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:11903
Scope.prototype.$apply@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:12179
bootstrap/doBootstrap/<@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:1341
invoke@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:3762
bootstrap/doBootstrap@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:1340
bootstrap@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:1353
angularInit@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:1301
@http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js:21048
n.Callbacks/j@http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js:2
n.Callbacks/k.fireWith@http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js:2
.ready@http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js:2
K@http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js:2
http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js
Line 9509

This is the relevant block of code:

            $defer.promise.then(function (data) {
            settings.$loading = false;
            log('ngTable: current scope', settings.$scope);
            if (settings.groupBy) {
                self.data = settings.$scope.$groups = data;
            } else {
                self.data = settings.$scope.$data = data; // line 411
            }
            settings.$scope.pages = self.generatePagesArray(self.page(), self.total(), self.count());
        });

Update:

Here is my plunkr that demonstrates that changing the external filter fields doesn't work. I also have two $watch-es commented out to try to rectify this. When I comment those in, I get an error within ng-table, complaining of a null scope.

Update:

I tried adding the "newvalue, oldvalue" parameters to my $watch-es (I updated the plunkr). Now changes to the fields are causing the table to update. Unfortunately, I still get that stack trace on line 411 of ng-table.

Answer

cleftheris picture cleftheris · Apr 15, 2015

You don't need the watches nor the custom filters you created. Actually angular's 'filter' filter is quite powerful.

You just need to create an object that keeps track of your filter values with members that match your items fields. Something like this.

$scope.filter = {
    key: undefined,
    failed: undefined
}

then you can take it back inside the getData callback using params.filter(). I have updated your plunker here. You can also check the sample below:

var app = angular.module('main', ['ngTable']);

app.controller('MainCtrl', function($scope, $http, $filter, ngTableParams) {

    $scope.completedQueries = [{"key":"abc000","lastRun":123,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc001","lastRun":1234,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc002","lastRun":111111111,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":true},{"key":"abc003","lastRun":123,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc004","lastRun":1234,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":true},{"key":"abc005","lastRun":111111111,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc006","lastRun":123,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc007","lastRun":1234,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc008","lastRun":111111111,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc009","lastRun":123,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc010","lastRun":1234,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc011","lastRun":111111111,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc012","lastRun":123,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":true},{"key":"abc013","lastRun":1234,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc014","lastRun":111111111,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc015","lastRun":123,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":true},{"key":"abc016","lastRun":1234,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false},{"key":"abc017","lastRun":111111111,"lastSuccessfulTime":9999,"elapsedTime":456,"rows":10,"failed":false}];
    $scope.filter = {
        key: undefined,
        failed: undefined
    };
    $scope.tableParams =  new ngTableParams({
        page: 1,
        count: 10,
        filter: $scope.filter
    }, {
        debugMode: true,
        total: $scope.completedQueries.length,
        getData: function($defer, params) {
            var orderedData = params.sorting() ? $filter('orderBy')($scope.completedQueries, params.orderBy()) : data;
            orderedData	= $filter('filter')(orderedData, params.filter());
            params.total(orderedData.length);
            $defer.resolve(orderedData.slice((params.page() - 1) * params.count(), params.page() * params.count()));
        }
    });
});
<link href="https://cdnjs.cloudflare.com/ajax/libs/ng-table/0.3.3/ng-table.min.css" rel="stylesheet"/>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ng-table/0.3.3/ng-table.min.js"></script>
<div  ng-app="main"  ng-controller="MainCtrl">
  <label for="keysFilter">Filter Keys:</label>
  <input id="keysFilter" type="text" ng-model="filter.key"/>
  <label for="showOnlyFailed">Show only queries that failed?</label>
  <input id="showOnlyFailed" type="checkbox" ng-model="filter.failed"/>
  <br/>
  <table ng-table="tableParams" class="table">
    <tr ng-repeat="queryInfo in $data">
        <td data-title="'Key'" sortable="'key'">{{queryInfo.key}}</td>
        <td data-title="'Last Run'" sortable="'lastRun'">{{queryInfo.lastRun}}</td>
        <td data-title="'Last Successful Run'" sortable="'lastSuccessfulRun'">{{queryInfo.lastSuccessfulRun}}</td>
        <td data-title="'Elapsed Time'" sortable="'elapsedTime'">{{queryInfo.elapsedTime}} ms</td>
        <td data-title="'Rows'" sortable="'rows'">{{queryInfo.rows}}</td>
        <td data-title="'Failed'" sortable="'failed'">{{queryInfo.failed}}</td>
        <td data-title="''"><button class="btn">Detail</button></td>
    </tr>
  </table> 
<div>