Comment retarder la recherche instantanée AngularJS?

147

J'ai un problème de performances que je n'arrive pas à résoudre. J'ai une recherche instantanée mais elle est un peu lente, car elle commence à chercher sur chacun 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>

Les données JSON ne sont même pas si volumineuses, 300 Ko seulement, je pense que ce que je dois accomplir est de mettre un délai d'environ 1 seconde sur la recherche pour attendre que l'utilisateur ait fini de taper, au lieu d'exécuter l'action à chaque frappe. AngularJS fait cela en interne, et après avoir lu des documents et d'autres sujets ici, je n'ai pas trouvé de réponse spécifique.

J'apprécierais tous les conseils sur la façon dont je peux retarder la recherche instantanée.

braincomb
la source
1
Vous obtenez tous les json sur l'application init ... et votre filtre de recherche ne reçoit pas les données une deuxième fois lors de la saisie ... il filtre le modèle déjà existant. Ai-je raison?
Maksym
La réponse ci-dessous a-t-elle fonctionné? Si tel est le cas, veuillez accepter la réponse. Sinon, faites-le moi savoir et je clarifierai davantage.
Jason Aden
Hé Jason, merci pour la réponse. J'essayais de jouer avec votre code mais pas de chance, la recherche cesse de fonctionner pour moi.
braincomb
Tant pis, c'était ma faute, j'ai oublié quelque chose. Votre solution fonctionne en effet. Merci :)
braincomb
Jetez un œil à cette réponse ici, qui fournit une directive qui vous permet de mettre un délai sur ng-change: stackoverflow.com/questions/21121460/…
Doug

Réponses:

121

(Voir la réponse ci-dessous pour une solution angulaire 1.3.)

Le problème ici est que la recherche s'exécutera à chaque fois que le modèle change, ce qui correspond à chaque action keyup sur une entrée.

Il y aurait des moyens plus propres de le faire, mais le moyen le plus simple serait probablement de changer la liaison de sorte que vous ayez une propriété $ scope définie dans votre Controller sur laquelle votre filtre fonctionne. De cette façon, vous pouvez contrôler la fréquence à laquelle cette variable $ scope est mise à jour. Quelque chose comme ça:

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
la source
Notez que $ scope. $ Watch sur un ng-modelne fonctionnera pas dans le modal de bootstrap angular-ui
Hendy Irawan
1
Je pense que cela fonctionnera également sans la variable tempFilterText: $ scope. $ Watch ('searchText', function (val) {if (filterTextTimeout) $ timeout.cancel (filterTextTimeout); filterTextTimeout = $ timeout (function () {$ scope. filterText = val;}, 250); // délai 250 ms})
Jos Theeuwen
@JosTheeuwen c'est alors simplement une variable globale qui est considérée comme une mauvaise pratique et non autorisée en mode strict .
mb21
301

METTRE À JOUR

Maintenant c'est plus facile que jamais (Angular 1.3), il suffit d'ajouter une option anti-rebond sur le modèle.

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

Plunker mis à jour:
http://plnkr.co/edit/4V13gK

Documentation sur ngModelOptions:
https://docs.angularjs.org/api/ng/directive/ngModelOptions

Ancienne méthode:

Voici une autre méthode sans dépendances au-delà de l'angular lui-même.

Vous devez définir un délai d'expiration et comparer votre chaîne actuelle avec la version précédente, si les deux sont identiques, il effectue la recherche.

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

et cela va dans votre avis:

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

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

Le plunker obligatoire: http://plnkr.co/dAPmwf

Josue Alexander Ibarra
la source
2
Pour moi, c'est une réponse beaucoup plus compréhensible qu'acceptée :) Merci!
OZ_
3
N'y a-t-il pas un problème où plusieurs modifications de modèle pourraient s'empiler, provoquant ainsi des demandes en double? Dans la réponse de @ JasonAden, il s'occupe de cela en annulant les événements précédemment mis en file d'attente.
Blaskovicz
En théorie, si le modèle subit un changement, mais que les données restent les mêmes, cela entraînerait plusieurs demandes. En pratique, je n'ai jamais vu cela arriver. Vous pouvez ajouter un indicateur pour vérifier ce cas de bord si vous êtes inquiet.
Josue Alexander Ibarra
C'est de loin le meilleur choix pour angulaire 1.3
Marcus W
Attention ici: Si vous avez un événement de pression de touche qui se soumet ou se déclenche, il le fera sans la dernière valeur de modèle car la liaison de valeur est déboncée. par exemple, tapez «foo» et sur une pression immédiate de la touche, la valeur sera toujours une chaîne vide.
jbodily
34

Dans Angular 1.3, je ferais ceci:

HTML:

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

Manette:

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

Fondamentalement, vous dites à angular de s'exécuter myDebouncedFunction()lorsque la msgvariable de portée change. L'attribut ng-model-options="{debounce: 1000}"garantit que cela msgne peut être mis à jour qu'une fois par seconde.

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

Maintenant, nous pouvons définir les options ng-model-options anti-rebond avec le temps et en cas de flou, le modèle doit être changé immédiatement, sinon, lors de l'enregistrement, il aura une valeur plus ancienne si le délai n'est pas terminé.

Ali Adravi
la source
9

Pour ceux qui utilisent keyup / keydown dans le balisage HTML. Cela n'utilise pas de montre.

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
la source
6

Mises à jour de modèle déboncées / limitées pour angularjs: http://jsfiddle.net/lgersman/vPsGb/3/

Dans votre cas, il n'y a rien de plus à faire que d'utiliser la directive dans le code jsfiddle comme ceci:

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

C'est fondamentalement un petit morceau de code constitué d'une seule directive angulaire nommée "ng-ampere-debounce" utilisant http://benalman.com/projects/jquery-throttle-debounce-plugin/ qui peut être attachée à n'importe quel élément dom. La directive réorganise les gestionnaires d'événements attachés afin de pouvoir contrôler quand limiter les événements.

Vous pouvez l'utiliser pour la limitation / la suppression de * mises à jour angulaires du modèle * gestionnaire d'événements angulaires ng- [événement] * gestionnaires d'événements jquery

Jetez un œil: http://jsfiddle.net/lgersman/vPsGb/3/

La directive fera partie du framework Orangevolt Ampere ( https://github.com/lgersman/jquery.orangevolt-ampere ).

lgersman
la source
6

Juste pour les utilisateurs redirigés ici:

Comme introduit dans, Angular 1.3vous pouvez utiliser l' attribut ng-model-options :

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

Je pense que la meilleure façon de résoudre ce problème est d'utiliser le plugin jQuery throttle / debounce de Ben Alman . À mon avis, il n'est pas nécessaire de retarder les événements de chaque champ de votre formulaire.

Enveloppez simplement votre fonction de gestion $ scope. $ Watch dans $ .debounce comme ceci:

$scope.$watch("searchText", $.debounce(1000, function() {
    console.log($scope.searchText);
}), true);
Daniel Popov
la source
Vous devrez envelopper ceci dans une portée $. $
Apply
3

Une autre solution consiste à ajouter une fonctionnalité de retard à la mise à jour du modèle. La directive simple semble faire un tour:

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

Usage:

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

Donc, vous utilisez simplement delayed-modelà la place ng-modelet définissez souhaité data-delay.

Démo: http://plnkr.co/edit/OmB4C3jtUD2Wjq5kzTSU?p=preview

dfsq
la source
Hey! pouvez-vous expliquer comment ça model: '=delayedModel'marche? Ou pouvez-vous me diriger vers un lien où je peux le trouver?
Akash Agrawal
@AkashAgrawal C'est une liaison de données bidirectionnelle. Lisez à ce sujet ici docs.angularjs.org/api/ng.$compile
dfsq
1
@dfsq J'utilisais ng-change et il se déclenchait chaque fois qu'il y avait un changement dans le texte. Mais je ne peux pas l'utiliser lorsqu'une directive est définie. element.on('change')se déclenche uniquement sur le flou. (1) Y a-t-il une solution? (2) comment appeler une fonction du contrôleur lors d'un changement de texte?
Vyas Rao
0

J'ai résolu ce problème avec une directive qui consiste essentiellement à lier le modèle ng réel à un attribut spécial que je regarde dans la directive, puis en utilisant un service anti-rebond, je mets à jour mon attribut de directive, de sorte que l'utilisateur surveille la variable qui il se lie à debounce-model au lieu de ng-model.

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

Usage:

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

Et dans le contrôleur:

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

Démo dans jsfiddle: http://jsfiddle.net/6K7Kd/37/

le service $ debounce peut être trouvé ici: http://jsfiddle.net/Warspawn/6K7Kd/

Inspiré de la directive éventuellementBind http://jsfiddle.net/fctZH/12/

Ofir D
la source
0

Angular 1.3 aura ng-model-options anti-rebond, mais jusque-là, vous devez utiliser une minuterie comme l'a dit Josue Ibarra. Cependant, dans son code, il lance une minuterie à chaque pression de touche. De plus, il utilise setTimeout, alors que dans Angular, il faut utiliser $ timeout ou utiliser $ apply à la fin de setTimeout.

FA
la source
0

Pourquoi tout le monde veut-il utiliser la montre? Vous pouvez également utiliser une fonction:

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
la source
0

Je pense que le moyen le plus simple ici est de précharger le json ou de le charger une fois $dirty, puis la recherche par filtre se chargera du reste. Cela vous permettra d'économiser les appels http supplémentaires et c'est beaucoup plus rapide avec des données préchargées. La mémoire fera mal, mais ça vaut le coup.

NateNjugush
la source