angular ui-grid/ng-grid filterChanged firing too frequently
Asked Answered
B

4

6

I'm trying to implement a grid in angular, using server-side sorting, server-side pagination and server-side filtering. Using ui-grid (unstable), I added ui.grid.paging and got everything up and running. Very nice, kudos to the developers.

However.... $scope.gridApi.core.on.filterChanged is firing for every key pressed, so when I search for "Patrick" in the givenname column, seven events are fired and seven get-requests hit my server. Even worse, since it is a large set I'm filtering, this operation is pretty expensive and the results my even overtake each other, like the most specific filter getting the fastest result, triggering success before a less specific result is processed.

I'd like to slow it down, like "fire after entry has stopped". I'm pretty much new to javascript and REST, this is my first real-world-project. I would really appreciate any ideas how to handle this issue. It feels like a common scenario, so there might be some standard solutions or best practices I'm missing.

Yours, Patrick.

Blakey answered 4/12, 2014 at 18:50 Comment(0)
H
16

If you wanna go "all angular" I would suggest to use a $timeout inside the on.filterChanged event handler:

if (angular.isDefined($scope.filterTimeout)) {
    $timeout.cancel($scope.filterTimeout);
}
$scope.filterTimeout = $timeout(function () {
    getPage();
}, 500);

where 500 is the time (in milliseconds) you would like to wait between each on.filterChanged event before going to the server and getPage() is the function that actually goes to the server and retrieves the data.

Don't forget to cancel your $timeout on the controller's 'destroy' event:

$scope.$on("$destroy", function (event) {
    if (angular.isDefined($scope.filterTimeout)) {
        $timeout.cancel($scope.filterTimeout);
    }
});
Halftrack answered 4/12, 2014 at 22:32 Comment(0)
P
9

I was dealing with the same problem and I came up with another solution which IMHO is more "Angular-friendly". I used the ngModelOptions directive introduced in Angular 1.3. I replaced uiGrid's default filter template ("ui-grid/ui-grid-filter") by a custom one, and configured the ngModelOptions directive on the input with a default debounce value of 300 ms and 0 ms for blur.

This is a sample template based on ui-grid 3.0.5 original template where I also changed default CSS classes by Bootstrap classes:

$templateCache.put('ui-grid/ui-grid-filter-custom',
  "<div class=\"ui-grid-filter-container\" ng-repeat=\"colFilter in col.filters\" ng-class=\"{'ui-grid-filter-cancel-button-hidden' : colFilter.disableCancelFilterButton === true }\">" +
  "<div ng-if=\"colFilter.type !== 'select'\"><input type=\"text\" class=\"input-sm form-control\" ng-model=\"colFilter.term\" ng-model-options=\"{ debounce : { 'default' : 300, 'blur' : 0 }}\" ng-attr-placeholder=\"{{colFilter.placeholder || ''}}\" aria-label=\"{{colFilter.ariaLabel || aria.defaultFilterLabel}}\"><div role=\"button\" class=\"ui-grid-filter-button\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term !== null && colFilter.term !== ''\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\">&nbsp;</i></div></div>" +
  "<div ng-if=\"colFilter.type === 'select'\"><select class=\"form-control input-sm\" ng-model=\"colFilter.term\" ng-attr-placeholder=\"{{colFilter.placeholder || aria.defaultFilterLabel}}\" aria-label=\"{{colFilter.ariaLabel || ''}}\" ng-options=\"option.value as option.label for option in colFilter.selectOptions\"><option value=\"\"></option></select><div role=\"button\" class=\"ui-grid-filter-button-select\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term != null\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\">&nbsp;</i></div></div>" +
  "</div>"
);

The final step for this to work is to set this template in every columnDef where you enable filtering:

columnDefs: [{
  name: 'theName',
  displayName: 'Whatever',
  filterHeaderTemplate: 'ui-grid/ui-grid-filter-custom'
}]

Unfortunately, I couldn't find any way to define this template globally, so I had to repeat the filterHeaderTemplate everywhere... That's the only drawback, but on the other hand, you can also add more filters to your custom template if you need to.

Hope it helps!

Psalmist answered 12/9, 2015 at 21:52 Comment(3)
This is actually the best answer.Help
Actually if you put the override in app.run with $templateCache.put('ui-grid/ui-grid-filter', ...); then this should override the default definition from ui-grid.Intolerant
Hi @anre, you are right, I ended up doing exactly that. My use case was actually more complex than my example here, I had other custom filters but decided to override the default one from ui-grid with just these basic filters and then move others separately to other templates. Thanks!Psalmist
G
1

I'd recommend that you take a look at reactive-extensions for Javascript. Reactive is built for precisely these kinds of scenarios. There is a method called .Throttle(milliseconds) which you can attach to an observable that will call a handler (to hit your API) after x milliseconds have passed for the user not entering anything.

Rx-Js and angular play well together

Here is an example from one of my projects of doing something similar:

observeOnScope($scope, 'tag', true)
            .throttle(1000)
            .subscribe(function (data) {
                if (data.newValue) {
                    $http({
                        url: api.endpoint + 'tag/find',
                        method: 'GET',
                        params: {text: data.newValue}
                    }).then(function (result) {
                        $scope.candidateTags = result.data;
                    })
                }
            });

This code takes $scope.tag and turns it into an observable. The .throttle(1000) means that after 1 second of $scope.tag not changing the subscribe function will be called where (data) is the new value. After that you can hit your API.

I used this to do typeahead lookup of values from my API so hitting the backend everytime a letter changed obviously wasn't the way to go :)

Georg answered 4/12, 2014 at 21:16 Comment(1)
I have implemented the "all angular" solution, since it doesn't involve external dependencies. However, your solutions sounds very interesting and I'll have a look at it soon.Blakey
R
0

JavaScript setTimeout function can also be used for giving delay as given below.

$scope.gridApi.core.on.filterChanged( $scope, function() {

    if (angular.isDefined($scope.filterTimeout)) {
        clearTimeout($scope.filterTimeout);
    }
    $scope.filterTimeout = setTimeout(function(){
        getPage();
    },1000);

});
Rhea answered 15/2, 2016 at 7:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.