angular directive encapsulating a delay for ng-change

16,500

Solution 1

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}

Solution 2

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>;
}
Share:
16,500

Related videos on Youtube

Homan
Author by

Homan

Web developer / Software Architect and technical enthusiast. CTO/Architect and Lead Engineer that created a Software as a Service for E-commerce merchants (sold to Godaddy). Over 10 years of Ruby on Rails experience. I would like to work more with Elixir as I like its message passing model for concurrently running background jobs. I'm also interested in learning more about machine learning. When I'm not working I like to tinker with VR, Arduino, Raspberry Pi.

Updated on June 26, 2022

Comments

  • Homan
    Homan about 2 years

    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?

    • Fresheyeball
      Fresheyeball over 10 years
      Sure. You can write the code you've already got into a directive.
    • Alborz
      Alborz over 10 years
      I think $timeout.cancel(searchDelay); is useless in your code. you need to compare the old search with new one to avoid repeating the query.
    • Doug
      Doug over 10 years
      Please mark an answer as correct, if you have found one to work.
  • Doug
    Doug over 10 years
    This answer only works for the search value and modelChanged function. What I think the author is looking for is something like ng-delayed-change="<AngularExpression>", which automatically inserts the delay, in all cases.
  • mainguy
    mainguy over 10 years
    I realy love this directive, that was exactly what I needed! Anyhow, since this does not work when there are mutiple inputs on a page, I allowed myself to modify your awseome code by one little line here: restrict:'A',scope:true,compile:... Look at this forked fiddle to see what i mean: jsfiddle.net/ZfrTX. Thanks again, I upvoted your answer! Too bad it was never accepted. Whish I could...
  • Doug
    Doug over 10 years
    @mainguy Thank you! That was an important oversight on my part. The code has been updated to use an isolate scope.
  • Ryan Q
    Ryan Q over 10 years
    @DougR just an fyi "scope: true" is not an isolate scope, but it seems to do the trick. "scope: {}" would produce an isolate scope with no passed parameters from the outer scope.
  • Doug
    Doug over 10 years
    @RyanQ You are absolutely correct, AND changing it to an isolate scope did actually fix a bug. Look at the updated example. The various values no longer are constrained to be on the same object, which had perplexed me. I was confused because I thought I was working with an isolate scope, when in fact I was working with one that prototypically inherits from the elements scope. Thank you.
  • Doug
    Doug over 10 years
    Sorry for the all the updates. Changing to an isolate scope did not fix anything. A child scope is required to preserve the binding on ngModel. Furthermore, the ngModel binding had to be moved to the parent scope to avoid making the caller have to reference their model on $parent explicitly. Due to the properties of prototypal inheritance, properties and functions can be referenced on the scope from angular binding in the usual way. However, if values need to be set on the scope, they will need to be accessed through the $parent scope. Hopefully, this is a corner case.
  • Doug
    Doug over 10 years
    It's noteworthy that this problem is unusually hard to solve, given how simple the desired behavior is. Does anyone know a more concise solution? Or are we running into the limitations of Angular? If a better solution is known, please post a fiddle in the comments. Thanks.
  • brentvatne
    brentvatne about 10 years
    @DougR - I packaged your solution and put it up on bower with credit to you, for the convenience of other readers. Github repo: github.com/brentvatne/angular-delay You can see it on bower: bower.io/search/?q=angular-delay
  • codelearner
    codelearner over 8 years
    @kreepN ,i tried this solution, but i am getting it immediately. can you tell me why? here is the plunker
  • KreepN
    KreepN over 8 years
    @codelearner Just saw your question, but it probably has to do with your plnkr not using a new enough version of angular. It needs 1.3 minimum.
  • Doug
    Doug about 7 years
    It seems like this solution delays the model update, not the change action. It doesn't seem to work when I need the model to update immediately, but also need an action that makes use of the model to be delayed.