Como colocar um atraso na pesquisa instantânea do AngularJS?

147

Tenho um problema de desempenho que não consigo resolver. Eu tenho uma pesquisa instantânea, mas é um pouco lenta, pois começa a pesquisar em cada uma keyup().

JS:

var App = angular.module('App', []);

App.controller('DisplayController', function($scope, $http) {
$http.get('data.json').then(function(result){
    $scope.entries = result.data;
});
});

HTML:

<input id="searchText" type="search" placeholder="live search..." ng-model="searchText" />
<div class="entry" ng-repeat="entry in entries | filter:searchText">
<span>{{entry.content}}</span>
</div>

Os dados JSON não são tão grandes, apenas 300 KB, acho que o que preciso fazer é colocar um atraso de ~ 1 segundo na pesquisa para aguardar que o usuário termine de digitar, em vez de executar a ação em cada pressionamento de tecla. O AngularJS faz isso internamente e, depois de ler documentos e outros tópicos aqui, não consegui encontrar uma resposta específica.

Agradeço qualquer indicação de como posso atrasar a pesquisa instantânea.

braincomb
fonte
1
Você está obtendo todo o json no aplicativo init ... e seu filtro de pesquisa não está obtendo os dados pela segunda vez ao digitar ... está filtrando o modelo já existente. Estou correcto?
Maksym
A resposta abaixo deu certo? Nesse caso, aceite a resposta. Caso contrário, informe-me e esclarecerei mais.
Jason Aden
Ei Jason, obrigado pela resposta. Eu estava tentando brincar com seu código, mas sem sorte, a pesquisa para de funcionar completamente para mim.
braincomb
Deixa pra lá, foi ruim eu ter esquecido alguma coisa. Sua solução funciona mesmo. Obrigado :)
braincomb
Dê uma olhada nesta resposta aqui, que fornece uma diretiva que permite que você coloque um atraso no ng-change: stackoverflow.com/questions/21121460/…
Doug

Respostas:

121

(Veja a resposta abaixo para obter uma solução Angular 1.3.)

O problema aqui é que a pesquisa será executada toda vez que o modelo for alterado, que é toda ação de keyup em uma entrada.

Haveria maneiras mais limpas de fazer isso, mas provavelmente a maneira mais fácil seria alternar a ligação para que você tenha uma propriedade $ scope definida dentro do seu Controller na qual seu filtro opera. Dessa forma, você pode controlar com que frequência a variável $ scope é atualizada. Algo assim:

JS:

var App = angular.module('App', []);

App.controller('DisplayController', function($scope, $http, $timeout) {
    $http.get('data.json').then(function(result){
        $scope.entries = result.data;
    });

    // This is what you will bind the filter to
    $scope.filterText = '';

    // Instantiate these variables outside the watch
    var tempFilterText = '',
        filterTextTimeout;
    $scope.$watch('searchText', function (val) {
        if (filterTextTimeout) $timeout.cancel(filterTextTimeout);

        tempFilterText = val;
        filterTextTimeout = $timeout(function() {
            $scope.filterText = tempFilterText;
        }, 250); // delay 250 ms
    })
});

HTML:

<input id="searchText" type="search" placeholder="live search..." ng-model="searchText" />
<div class="entry" ng-repeat="entry in entries | filter:filterText">
    <span>{{entry.content}}</span>
</div>
Jason Aden
fonte
. Note que $ âmbito $ assistir em um ng-modelnão vai funcionar dentro de inicialização angular-ui modal
Hendy Irawan
1
Eu acho que ele também funcionará sem a variável tempFilterText: $ scope. $ Watch ('searchText', função (val) {if (filterTextTimeout) $ timeout.cancel (filterTextTimeout); filterTextTimeout = $ timeout (function () {$ scope. filterText = val;}, 250); // demora 250 ms})
Jos Theeuwen
@JosTheeuwen, então é simplesmente uma variável global que é considerada má prática e não é permitida no modo estrito .
MB21
301

ATUALIZAR

Agora está mais fácil do que nunca (Angular 1.3), basta adicionar uma opção de rejeição ao modelo.

<input type="text" ng-model="searchStr" ng-model-options="{debounce: 1000}">

Plunker atualizado:
http://plnkr.co/edit/4V13gK

Documentação sobre ngModelOptions:
https://docs.angularjs.org/api/ng/directive/ngModelOptions

Método antigo:

Aqui está outro método sem dependências além do próprio angular.

Você precisa definir um tempo limite e comparar sua cadeia atual com a versão anterior, se as duas forem iguais, então ela realiza a pesquisa.

$scope.$watch('searchStr', function (tmpStr)
{
  if (!tmpStr || tmpStr.length == 0)
    return 0;
   $timeout(function() {

    // if searchStr is still the same..
    // go ahead and retrieve the data
    if (tmpStr === $scope.searchStr)
    {
      $http.get('//echo.jsontest.com/res/'+ tmpStr).success(function(data) {
        // update the textarea
        $scope.responseData = data.res; 
      });
    }
  }, 1000);
});

e isso entra na sua visão:

<input type="text" data-ng-model="searchStr">

<textarea> {{responseData}} </textarea>

O desentupidor obrigatório: http://plnkr.co/dAPmwf

Josue Alexander Ibarra
fonte
2
Para mim, é uma resposta muito mais compreensível do que aceita :) Obrigado!
OZ_
3
Não há um problema em que várias alterações de modelo possam se acumular, causando solicitações duplicadas? Na resposta de @ JasonAden, ele cuida disso cancelando eventos na fila anterior.
Blaskovicz 01/04/19
Em teoria, se o modelo sofrer uma alteração, mas os dados permanecerem os mesmos, isso causaria várias solicitações. Na prática, nunca vi isso acontecer. Você pode adicionar uma bandeira para verificar esse caso extremo, se estiver preocupado.
Josue Alexander Ibarra
Esta é de longe a escolha superior para angular 1.3
Marcus W
Aviso aqui: Se você tiver um evento de pressionamento de tecla que enviar ou disparar, ele o fará sem o valor mais recente do modelo, pois a ligação do valor foi devolvida. por exemplo, digite 'foo' e, ao pressionar imediatamente a tecla, o valor ainda será uma string vazia.
jbodily
34

No Angular 1.3, eu faria o seguinte:

HTML:

<input ng-model="msg" ng-model-options="{debounce: 1000}">

Controlador:

$scope.$watch('variableName', function(nVal, oVal) {
    if (nVal !== oVal) {
        myDebouncedFunction();
    }
});

Basicamente, você está dizendo ao angular para executar myDebouncedFunction(), quando a msgvariável do escopo muda. O atributo ng-model-options="{debounce: 1000}"garante que msgsomente seja possível atualizar uma vez por segundo.

Michael Falck Wedelgård
fonte
10
 <input type="text"
    ng-model ="criteria.searchtext""  
    ng-model-options="{debounce: {'default': 1000, 'blur': 0}}"
    class="form-control" 
    placeholder="Search" >

Agora podemos definir o debounce de ng-model-options com o tempo e, quando desfocado, o modelo precisa ser alterado imediatamente, caso contrário, ao salvar, ele terá um valor mais antigo se o atraso não for concluído.

Ali Adravi
fonte
9

Para quem usa keyup / keydown na marcação HTML. Isso não usa relógio.

JS

app.controller('SearchCtrl', function ($scope, $http, $timeout) {
  var promise = '';
  $scope.search = function() {
    if(promise){
      $timeout.cancel(promise);
    }
    promise = $timeout(function() {
    //ajax call goes here..
    },2000);
  };
});

HTML

<input type="search" autocomplete="off" ng-model="keywords" ng-keyup="search()" placeholder="Search...">
Vinoth
fonte
6

Atualizações de modelo reprovadas / limitadas para angularjs: http://jsfiddle.net/lgersman/vPsGb/3/

No seu caso, não há mais nada a fazer do que usar a diretiva no código jsfiddle assim:

<input 
    id="searchText" 
    type="search" 
    placeholder="live search..." 
    ng-model="searchText" 
    ng-ampere-debounce
/>

É basicamente um pequeno pedaço de código que consiste em uma única diretiva angular chamada "ng-ampere-debounce" utilizando http://benalman.com/projects/jquery-throttle-debounce-plugin/ que pode ser anexada a qualquer elemento dom. A diretiva reordena os manipuladores de eventos anexados para que possa controlar quando controlar os eventos.

Você pode usá-lo para estrangular / depurar * atualizações angulares do modelo * manipulador de eventos angular ng- [event] * manipuladores de eventos jquery

Dê uma olhada: http://jsfiddle.net/lgersman/vPsGb/3/

A diretiva fará parte da estrutura do Orangevolt Ampere ( https://github.com/lgersman/jquery.orangevolt-ampere ).

lgersman
fonte
6

Apenas para usuários redirecionados aqui:

Conforme introduzido, Angular 1.3você pode usar o atributo ng-model-options :

<input 
       id="searchText" 
       type="search" 
       placeholder="live search..." 
       ng-model="searchText"
       ng-model-options="{ debounce: 250 }"
/>
Morteza Tourani
fonte
5

Acredito que a melhor maneira de resolver esse problema é usar o plugin jQuery throttle / debounce de Ben Alman . Na minha opinião, não há necessidade de adiar os eventos de todos os campos em seu formulário.

Apenas envolva sua função de manipulação $ scope. $ Watch em $ .debounce da seguinte maneira:

$scope.$watch("searchText", $.debounce(1000, function() {
    console.log($scope.searchText);
}), true);
Daniel Popov
fonte
Você precisará
agrupar
3

Outra solução é adicionar uma funcionalidade de atraso ao modelo de atualização. A diretiva simples parece fazer um truque:

app.directive('delayedModel', function() {
    return {
        scope: {
            model: '=delayedModel'
        },
        link: function(scope, element, attrs) {

            element.val(scope.model);

            scope.$watch('model', function(newVal, oldVal) {
                if (newVal !== oldVal) {
                    element.val(scope.model);        
                }
            });

            var timeout;
            element.on('keyup paste search', function() {
                clearTimeout(timeout);
                timeout = setTimeout(function() {
                    scope.model = element[0].value;
                    element.val(scope.model);
                    scope.$apply();
                }, attrs.delay || 500);
            });
        }
    };
});

Uso:

<input delayed-model="searchText" data-delay="500" id="searchText" type="search" placeholder="live search..." />

Então você apenas usa delayed-modelno lugar ng-modele define o desejado data-delay.

Demonstração: http://plnkr.co/edit/OmB4C3jtUD2Wjq5kzTSU?p=preview

dfsq
fonte
Ei! você pode explicar como model: '=delayedModel'está funcionando? Ou você pode me indicar um link para encontrá-lo?
precisa saber é o seguinte
@AkashAgrawal É uma ligação de dados bidirecional. Leia sobre isso aqui docs.angularjs.org/api/ng.$compile
dfsq
1
@dfsq Eu estava usando ng-change e era acionado sempre que havia uma alteração no texto. Mas não posso usá-lo quando uma diretiva é definida. element.on('change')dispara apenas no desfoque. (1) Existe uma solução alternativa? (2) como chamar uma função do controlador na alteração de texto?
Vyas Rao
0

Eu resolvi esse problema com uma diretiva que, basicamente, o que ele faz é vincular o modelo ng real em um atributo especial que assisto na diretiva; em seguida, usando um serviço de debounce, atualizo meu atributo de diretiva, para que o usuário observe a variável que ele vincula ao modelo de rejeição em vez do modelo ng.

.directive('debounceDelay', function ($compile, $debounce) {
return {
  replace: false,
  scope: {
    debounceModel: '='
  },
  link: function (scope, element, attr) {
    var delay= attr.debounceDelay;
    var applyFunc = function () {
      scope.debounceModel = scope.model;
    }
    scope.model = scope.debounceModel;
    scope.$watch('model', function(){
      $debounce(applyFunc, delay);
    });
    attr.$set('ngModel', 'model');
    element.removeAttr('debounce-delay'); // so the next $compile won't run it again!

   $compile(element)(scope);
  }
};
});

Uso:

<input type="text" debounce-delay="1000" debounce-model="search"></input>

E no controlador:

    $scope.search = "";
    $scope.$watch('search', function (newVal, oldVal) {
      if(newVal === oldVal){
        return;
      }else{ //do something meaningful }

Demonstração em jsfiddle: http://jsfiddle.net/6K7Kd/37/

o serviço $ debounce pode ser encontrado aqui: http://jsfiddle.net/Warspawn/6K7Kd/

Inspirado pela diretiva eventualmenteBind http://jsfiddle.net/fctZH/12/

Ofir D
fonte
0

O Angular 1.3 terá debounce de ng-model-options, mas até então, você precisará usar um timer como Josue Ibarra disse. No entanto, em seu código, ele lança um cronômetro a cada pressionamento de tecla. Além disso, ele está usando setTimeout, quando em Angular é necessário usar $ timeout ou $ apply no final de setTimeout.

FA
fonte
0

Por que todo mundo quer usar o relógio? Você também pode usar uma função:

var tempArticleSearchTerm;
$scope.lookupArticle = function (val) {
    tempArticleSearchTerm = val;

    $timeout(function () {
        if (val == tempArticleSearchTerm) {
            //function you want to execute after 250ms, if the value as changed

        }
    }, 250);
}; 
NicoJuicy
fonte
0

Eu acho que a maneira mais fácil aqui é pré-carregar o json ou carregá-lo uma vez $dirtye, em seguida, a pesquisa de filtro cuidará do resto. Isso economiza as chamadas http extras e é muito mais rápido com os dados pré-carregados. A memória vai doer, mas vale a pena.

NateNjugush
fonte