Angular директива, инкапсулирующая задержку для ng-change

У меня есть поле ввода поиска с функцией requery, связанной с ng-change.

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

Однако это срабатывает слишком быстро для каждого персонажа. Поэтому я в конечном итоге делаю что-то вроде этого alot:

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

Таким образом, запрос будет выполнен только через 300 мс после того, как пользователь перестанет печатать. Есть ли какое-либо решение, чтобы обернуть это в директиве?

Ответ 1

Как и в случае с angular 1.3, это проще выполнить, используя ngModelOptions:

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

Syntax:  {debounce: Miliseconds}

Ответ 2

Чтобы решить эту проблему, я создал директиву ngDelay.

ngDelay увеличивает поведение ngChange для поддержки желаемого отложенного поведения, которое предоставляет обновления всякий раз, когда пользователь неактивен, а не на каждом нажатии клавиши. Фокус в том, чтобы использовать дочернюю область и заменить значение ngChange на вызов функции, который включает логику тайм-аута и выполняет исходное выражение в родительской области. Второй трюк состоял в том, чтобы перенести любые привязки ngModel в родительскую область, если они есть. Все эти изменения выполняются на этапе компиляции директивы ngDelay.

Здесь сценарий, содержащий пример с использованием ngDelay: http://jsfiddle.net/ZfrTX/7/ (Написано и отредактировано мной, с помощью mainguy и Ryan Q)

Этот код можно найти на GitHub благодаря brentvatne. Спасибо Брент!

Для быстрой справки, здесь JavaScript для директивы ngDelay:

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);
                        }
                    };
                }
            }
        }
    };
}]);

И если есть TypeScript wonks, здесь TypeScript с использованием определений angular от 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>;
}

Ответ 3

Это отлично работает для меня: 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:'&'
            }
        }
    });

Использование директивы

В вашем контроллере:

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

На ваш взгляд:

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

Ответ 4

Я знаю, что опаздываю в игру, но, надеюсь, это поможет любому, кто все еще использует 1.2. Pre-n-model-options Я нашел, что это сработало для меня, поскольку ngchange не будет срабатывать, когда значение недействительно.

это небольшое отклонение от ответа @doug, поскольку оно использует ngKeypress, которому не важно, в каком состоянии находится модель.

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;
};