¿Cómo retrasar la búsqueda instantánea de AngularJS?

147

Tengo un problema de rendimiento que parece que no puedo solucionar. Tengo una búsqueda instantánea pero es algo lenta, ya que comienza a buscar en cada una 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>

Los datos JSON ni siquiera son tan grandes, solo 300 KB, creo que lo que debo lograr es poner un retraso de ~ 1 segundo en la búsqueda para esperar a que el usuario termine de escribir, en lugar de realizar la acción en cada pulsación de tecla. AngularJS hace esto internamente, y después de leer documentos y otros temas aquí no pude encontrar una respuesta específica.

Agradecería cualquier sugerencia sobre cómo puedo retrasar la búsqueda instantánea.

cerebro
fuente
1
Obtiene todos los json en la aplicación init ... y luego su filtro de búsqueda no obtiene los datos por segunda vez al escribir ... está filtrando el modelo ya existente. ¿Estoy en lo correcto?
Maksym
¿La respuesta a continuación funcionó? Si es así, acepta la respuesta. Si no, avíseme y lo aclararé más.
Jason Aden
Hola Jason, gracias por la respuesta. Estaba tratando de jugar con tu código, pero no tuve suerte, la búsqueda deja de funcionar para mí.
braincomb
No importa, fue mi mal que pasé por alto algo. Su solución funciona de hecho. Gracias :)
braincomb
Eche un vistazo a esta respuesta aquí, que proporciona una directiva que le permite retrasar ng-change: stackoverflow.com/questions/21121460/…
Doug

Respuestas:

121

(Consulte la respuesta a continuación para obtener una solución angular 1.3).

El problema aquí es que la búsqueda se ejecutará cada vez que cambie el modelo, que es cada acción de keyup en una entrada.

Habría formas más limpias de hacer esto, pero probablemente la forma más fácil sería cambiar el enlace para que tenga una propiedad $ scope definida dentro de su controlador en el que opera su filtro. De esa manera puede controlar con qué frecuencia se actualiza esa variable $ scope. Algo como esto:

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
fuente
Tenga en cuenta que $ scope. $ Watch en un ng-modelno funcionará dentro del modal de bootstrap angular-ui
Hendy Irawan
1
Creo que también funcionará sin la variable tempFilterText: $ scope. $ Watch ('searchText', function (val) {if (filterTextTimeout) $ timeout.cancel (filterTextTimeout); filterTextTimeout = $ timeout (function () {$ scope. filterText = val;}, 250); // retraso 250 ms})
Jos Theeuwen
@JosTheeuwen es simplemente una variable global que se considera una mala práctica y no se permite en modo estricto .
mb21
301

ACTUALIZAR

Ahora es más fácil que nunca (Angular 1.3), solo agregue una opción antirrebote en el modelo.

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

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

Documentación sobre ngModelOptions:
https://docs.angularjs.org/api/ng/directive/ngModelOptions

Método antiguo:

Aquí hay otro método sin dependencias más allá del propio ángulo.

Debe establecer un tiempo de espera y comparar su cadena actual con la versión anterior, si ambas son iguales, entonces realiza la búsqueda.

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

y esto entra en tu opinión:

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

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

El saqueador obligatorio: http://plnkr.co/dAPmwf

Josue Alexander Ibarra
fuente
2
Para mí es una respuesta mucho más comprensible de lo aceptado :) ¡Gracias!
OZ_
3
¿No hay un problema en el que se puedan acumular múltiples cambios de modelo, causando solicitudes duplicadas? En la respuesta de @ JasonAden, se encarga de eso cancelando eventos previamente en cola.
Blaskovicz
En teoría, si el modelo experimenta un cambio, pero los datos siguen siendo los mismos, causaría múltiples solicitudes. En la práctica, nunca lo he visto suceder. Puede agregar una bandera para verificar ese caso límite si está preocupado.
Josue Alexander Ibarra
Esta es, con mucho, la mejor opción para angular 1.3
Marcus W
Advertencia aquí: si tiene un evento de pulsación de tecla que se envía o se dispara, lo hará sin el último valor del modelo ya que el enlace de valor se ha eliminado. por ejemplo, escriba 'foo' y, al presionar una tecla inmediatamente, el valor seguirá siendo una cadena vacía.
jbodily
34

En Angular 1.3 haría esto:

HTML:

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

Controlador:

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

Básicamente le estás diciendo a angular que corra myDebouncedFunction(), cuando msgcambia la variable de alcance. El atributo ng-model-options="{debounce: 1000}"asegura que msgsolo se pueda actualizar una vez por segundo.

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

Ahora podemos configurar las opciones de ng-model-rebote con el tiempo y, cuando se difumina, el modelo debe cambiarse inmediatamente; de ​​lo contrario, al guardarlo tendrá un valor anterior si no se completa el retraso.

Ali Adravi
fuente
9

Para aquellos que usan keyup / keydown en el marcado HTML. Esto no usa reloj.

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
fuente
6

Actualizaciones de modelos rebotados / estrangulados para angularjs: http://jsfiddle.net/lgersman/vPsGb/3/

En su caso, no hay nada más que hacer que usar la directiva en el código jsfiddle de esta manera:

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

Básicamente es un pequeño fragmento de código que consiste en una única directiva angular llamada "ng-ampere-debounce" que utiliza http://benalman.com/projects/jquery-throttle-debounce-plugin/ que se puede conectar a cualquier elemento dom. La directiva reordena los controladores de eventos adjuntos para que pueda controlar cuándo regular los eventos.

Puede usarlo para limitar / eliminar rebotes * actualizaciones angulares del modelo * controlador de eventos angulares ng- [evento] * controladores de eventos jquery

Echa un vistazo: http://jsfiddle.net/lgersman/vPsGb/3/

La directiva será parte del marco de Orangevolt Ampere ( https://github.com/lgersman/jquery.orangevolt-ampere ).

Lgersman
fuente
6

Solo para usuarios redirigidos aquí:

Como se introdujo en Angular 1.3, puede usar el atributo ng-model-options :

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

Creo que la mejor manera de resolver este problema es usar el complemento de Ben Alman jQuery throttle / debounce . En mi opinión, no hay necesidad de retrasar los eventos de cada campo en su formulario.

Simplemente envuelva su $ scope. $ Watch función de manejo en $ .debounce de esta manera:

$scope.$watch("searchText", $.debounce(1000, function() {
    console.log($scope.searchText);
}), true);
Daniel Popov
fuente
Tendrá que envolver esto en un $ alcance. $ Aplicar
Aakil Fernandes
3

Otra solución es agregar una funcionalidad de retraso a la actualización del modelo. La directiva simple parece hacer un truco:

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..." />

Así que solo usa delayed-modelen lugar de ng-modely define el deseado data-delay.

Demostración: http://plnkr.co/edit/OmB4C3jtUD2Wjq5kzTSU?p=preview

dfsq
fuente
¡Oye! ¿Puedes explicar cómo model: '=delayedModel'está funcionando? ¿O puedes señalarme un enlace donde pueda encontrarlo?
Akash Agrawal
@AkashAgrawal Es un enlace de datos bidireccional. Lea sobre esto aquí docs.angularjs.org/api/ng.$compile
dfsq
1
@dfsq Estaba usando ng-change y solía activarse cada vez que hay un cambio en el texto. Pero no puedo usarlo cuando se define una directiva. element.on('change')se activa solo en el desenfoque (1) ¿Hay alguna solución? (2) ¿cómo llamar a una función del controlador en el cambio de texto?
Vyas Rao
0

Resolví este problema con una directiva que básicamente lo que hace es vincular el modelo ng real en un atributo especial que miro en la directiva, luego, usando un servicio antirrebote, actualizo mi atributo de directiva, por lo que el usuario observa la variable que se une a debounce-model en lugar 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);
  }
};
});

Uso:

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

Y en el controlador:

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

Demostración en jsfiddle: http://jsfiddle.net/6K7Kd/37/

El servicio $ debounce se puede encontrar aquí: http://jsfiddle.net/Warspawn/6K7Kd/

Inspirado por la directiva eventBind http://jsfiddle.net/fctZH/12/

Ofir D
fuente
0

Angular 1.3 tendrá un rebote ng-model-options, pero hasta entonces, debe usar un temporizador como dijo Josue Ibarra. Sin embargo, en su código, lanza un temporizador cada vez que se presiona una tecla. Además, está usando setTimeout, cuando en Angular uno tiene que usar $ timeout o usar $ apply al final de setTimeout.

FA
fuente
0

¿Por qué todos quieren usar el reloj? También podría usar una función:

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
fuente
0

Creo que la forma más fácil aquí es precargar el json o cargarlo una vez $dirtyy luego la búsqueda de filtro se encargará del resto. Esto le ahorrará las llamadas http adicionales y es mucho más rápido con datos precargados. La memoria dolerá, pero vale la pena.

NateNjugush
fuente