Homan
Homan

Reputation: 26718

angular directive encapsulating a delay for ng-change

I have a search input field with a requery function bound to the ng-change.

 <input ng-model="search" ng-change="updateSearch()">

However this fires too quickly on every character. So I end up doing something like this alot:

  $scope.updateSearch = function(){
    $timeout.cancel(searchDelay);
    searchDelay = $timeout(function(){
      $scope.requery($scope.search);
    },300);
  }

So that the request is only made 300ms after the user has stopped typing. Is there any solution to wrap this in a directive?

Upvotes: 25

Views: 16947

Answers (4)

TigerChan
TigerChan

Reputation: 88

I know i'm late to the game but,hopefully this will help anyone still using 1.2. Pre ng-model-options i found this worked for me, as ngchange will not fire when the value is invalid.

this is a slight variation on @doug's answer as it uses ngKeypress which doesn't care what state the model is in.

function delayChangeDirective($timeout) {
    var directive = {
        restrict: 'A',
        priority: 10,
        controller: delayChangeController,
        controllerAs: "$ctrl",
        scope: true,
        compile: function compileHandler(element, attributes) {
            var expression = attributes['ngKeypress'];
            if (!expression)
                return;

            var ngModel = attributes['ngModel'];
            if (ngModel) {
                attributes['ngModel'] = '$parent.' + ngModel;
            }
            attributes['ngKeypress'] = '$$delay.execute()';

            return {
                post: postHandler,
            };

            function postHandler(scope, element, attributes) {
                scope.$$delay = {
                    expression: expression,
                    delay: scope.$eval(attributes['ngKeypressDelay']),
                    execute: function () {
                        var state = scope.$$delay;
                        state.then = Date.now();
                        if (scope.promise) {
                            $timeout.cancel(scope.promise);
                        }

                        scope.promise = $timeout(function() {
                            delayedActionHandler(scope, state, expression);
                            scope.promise = null;
                        }, state.delay);
                    }
                };
            }
        }
    };

    function delayedActionHandler(scope, state, expression) {
        var now = Date.now();
        if (now - state.then >= state.delay) {
            scope.$parent.$eval(expression);
        }
    };

    return directive;
};

Upvotes: 0

schellmax
schellmax

Reputation: 6094

As of angular 1.3 this is way easier to accomplish, using ngModelOptions:

<input ng-model="search" ng-change="updateSearch()" ng-model-options="{debounce:3000}">

Syntax:  {debounce: Miliseconds}

Upvotes: 50

user799755
user799755

Reputation:

To solve this problem, I created a directive called ngDelay.

ngDelay augments the behavior of ngChange to support the desired delayed behavior, which provides updates whenever the user is inactive, rather than on every keystroke. The trick was to use a child scope, and replace the value of ngChange to a function call that includes the timeout logic and executes the original expression on the parent scope. The second trick was to move any ngModel bindings to the parent scope, if present. These changes are all performed in the compile phase of the ngDelay directive.

Here's a fiddle which contains an example using ngDelay: http://jsfiddle.net/ZfrTX/7/ (Written and edited by me, with help from mainguy and Ryan Q)

You can find this code on GitHub thanks to brentvatne. Thanks Brent!

For quick reference, here's the JavaScript for the ngDelay directive:

app.directive('ngDelay', ['$timeout', function ($timeout) {
    return {
        restrict: 'A',
        scope: true,
        compile: function (element, attributes) {
            var expression = attributes['ngChange'];
            if (!expression)
                return;

            var ngModel = attributes['ngModel'];
            if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
            attributes['ngChange'] = '$$delay.execute()';

            return {
                post: function (scope, element, attributes) {
                    scope.$$delay = {
                        expression: expression,
                        delay: scope.$eval(attributes['ngDelay']),
                        execute: function () {
                            var state = scope.$$delay;
                            state.then = Date.now();
                            $timeout(function () {
                                if (Date.now() - state.then >= state.delay)
                                    scope.$parent.$eval(expression);
                            }, state.delay);
                        }
                    };
                }
            }
        }
    };
}]);

And if there are any TypeScript wonks, here's the TypeScript using the angular definitions from DefinitelyTyped:

components.directive('ngDelay', ['$timeout', ($timeout: ng.ITimeoutService) => {
    var directive: ng.IDirective = {
        restrict: 'A',
        scope: true,
        compile: (element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
            var expression = attributes['ngChange'];
            if (!expression)
                return;

            var ngModel = attributes['ngModel'];
            if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
            attributes['ngChange'] = '$$delay.execute()';
            return {
                post: (scope: IDelayScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
                    scope.$$delay = {
                        expression: <string>expression,
                        delay: <number>scope.$eval(attributes['ngDelay']),
                        execute: function () {
                            var state = scope.$$delay;
                            state.then = Date.now();
                            $timeout(function () {
                                if (Date.now() - state.then >= state.delay)
                                    scope.$parent.$eval(expression);
                            }, state.delay);
                        }
                    };
                }
            }
        }
    };

    return directive;
}]);

interface IDelayScope extends ng.IScope {
    $$delay: IDelayState;
}

interface IDelayState {
    delay: number;
    expression: string;
    execute(): void;
    then?: number;
    action?: ng.IPromise<any>;
}

Upvotes: 25

Alborz
Alborz

Reputation: 6903

This works perfectly for me: JSFiddle

  var app = angular.module('app', []);
    app.directive('delaySearch', function ($timeout) {
        return {
            restrict: 'EA',
            template: ' <input ng-model="search" ng-change="modelChanged()">',
            link: function ($scope, element, attrs) {
                $scope.modelChanged = function () {
                    $timeout(function () {
                        if ($scope.lastSearch != $scope.search) {
                            if ($scope.delayedMethod) {
                                $scope.lastSearch = $scope.search;
                                $scope.delayedMethod({ search: $scope.search });
                            }
                        }
                    }, 300);
                }
            },
            scope: {
                delayedMethod:'&'
            }
        }
    });

Using the directive

In your controller:

app.controller('ctrl', function ($scope,$timeout) {
    $scope.requery = function (search) {
        console.log(search);
    }
});

In your view:

<div ng-app="app">
    <div ng-controller="ctrl">
        <delay-search delayed-method="requery(search)"></delay-search>
    </div>
</div>

Upvotes: 0

Related Questions