Angulaire et debounce

Dans AngularJS, je peux rebondir un modèle en utilisant les options ng-model.

ng-model-options="{ debounce: 1000 }"

Comment puis-je rebondir un modèle en angulaire? J'ai essayé de chercher debounce dans les docs mais je n'ai rien trouvé.

Https://angular.io/search/#stq=debounce&stp=1

Une solution serait d'écrire ma propre fonction debounce, par exemple:

import {Component, Template, bootstrap} from 'angular2/angular2';

// Annotation section
@Component({
  selector: 'my-app'
})
@Template({
  url: 'app.html'
})
// Component controller
class MyAppComponent {
  constructor() {
    this.firstName = 'Name';
  }

  changed($event, el){
    console.log("changes", this.name, el.value);
    this.name = el.value;
  }

  firstNameChanged($event, first){
    if (this.timeoutId) window.clearTimeout(this.timeoutID);
    this.timeoutID = window.setTimeout(() => {
        this.firstName = first.value;
    }, 250)
  }

}
bootstrap(MyAppComponent);

Et mon html

<input type=text [value]="firstName" #first (keyup)="firstNameChanged($event, first)">

Mais je cherche une fonction de construction, y en a-t-il une dans Angular?

105
demandé sur ishandutta2007 2015-08-17 16:09:01

13 réponses

Mise à jour pour RC.5

Avec Angular 2, nous pouvons rebondir en utilisant l'opérateur RxJS debounceTime() sur un contrôle de formulaire valueChanges observable:

import {Component}   from '@angular/core';
import {FormControl} from '@angular/forms';
import {Observable}  from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input type=text [value]="firstName" [formControl]="firstNameControl">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName        = 'Name';
  firstNameControl = new FormControl();
  formCtrlSub: Subscription;
  resizeSub:   Subscription;
  ngOnInit() {
    // debounce keystroke events
    this.formCtrlSub = this.firstNameControl.valueChanges
      .debounceTime(1000)
      .subscribe(newValue => this.firstName = newValue);
    // throttle resize events
    this.resizeSub = Observable.fromEvent(window, 'resize')
      .throttleTime(200)
      .subscribe(e => {
        console.log('resize event', e);
        this.firstName += '*';  // change something to show it worked
      });
  }
  ngDoCheck() { console.log('change detection'); }
  ngOnDestroy() {
    this.formCtrlSub.unsubscribe();
    this.resizeSub  .unsubscribe();
  }
} 

Plunker

Le code ci-dessus comprend également un exemple de la façon de réduire les événements de redimensionnement de la fenêtre, comme demandé par @albanx dans un commentaire ci-dessous.


Bien que le code ci-dessus soit probablement la manière angulaire de le faire, il n'est pas efficace. Chaque frappe et chaque événement de redimensionnement, même s'ils sont rebounced et throttled, résultats dans la détection de changement en cours d'exécution. En d'autres termes, debouncing et throttling n'affectent pas la fréquence à laquelle change detection runs. (J'ai trouvé un commentaire GitHub de Tobias Bosch qui le confirme.) Vous pouvez le voir lorsque vous exécutez le plunker et vous voyez combien de fois ngDoCheck() est appelé lorsque vous tapez dans la zone de saisie ou redimensionnez la fenêtre. (Utilisez le bouton bleu " x " pour exécuter le plunker dans une fenêtre séparée pour voir les événements de redimensionnement.)

Un une technique plus efficace consiste à créer vous-même des Observables RxJS à partir des événements, en dehors de la "zone"D'Angular. De cette façon, la détection de changement n'est pas appelée chaque fois qu'un événement se déclenche. Ensuite, dans vos méthodes de rappel subscribe, déclenchez manuellement la détection de changement-c'est-à-dire que vous contrôlez lorsque la détection de changement est appelée:

import {Component, NgZone, ChangeDetectorRef, ApplicationRef, 
        ViewChild, ElementRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input #input type=text [value]="firstName">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName = 'Name';
  keyupSub:  Subscription;
  resizeSub: Subscription;
  @ViewChild('input') inputElRef: ElementRef;
  constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef,
    private appref: ApplicationRef) {}
  ngAfterViewInit() {
    this.ngzone.runOutsideAngular( () => {
      this.keyupSub = Observable.fromEvent(this.inputElRef.nativeElement, 'keyup')
        .debounceTime(1000)
        .subscribe(keyboardEvent => {
          this.firstName = keyboardEvent.target.value;
          this.cdref.detectChanges();
        });
      this.resizeSub = Observable.fromEvent(window, 'resize')
        .throttleTime(200)
        .subscribe(e => {
          console.log('resize event', e);
          this.firstName += '*';  // change something to show it worked
          this.cdref.detectChanges();
        });
    });
  }
  ngDoCheck() { console.log('cd'); }
  ngOnDestroy() {
    this.keyupSub .unsubscribe();
    this.resizeSub.unsubscribe();
  }
} 

Plunker

J'utilise ngAfterViewInit() au lieu de ngOnInit() pour s'assurer que les inputElRef est défini.

detectChanges() exécutera la détection des changements sur ce composant et de ses enfants. Si vous préférez exécuter la détection des modifications à partir du composant racine (c'est-à-dire exécuter une vérification complète de la détection des modifications), utilisez ApplicationRef.tick() au lieu de cela. (Je mets un appel à ApplicationRef.tick() dans les commentaires dans le plunker.) Notez que l'appel de {[11] } entraînera l'appel de ngDoCheck().

157
répondu Mark Rajcok 2018-03-17 19:02:30

Si vous ne voulez pas traiter avec @angular/forms, Vous pouvez simplement utiliser un RxJS Subject avec des fixations de changement.

Vue.composant.html

<input [ngModel]='model' (ngModelChange)='changed($event)' />

Vue.composant.ts

import { Subject } from 'rxjs/Subject';
import { Component }   from '@angular/core';
import 'rxjs/add/operator/debounceTime';

export class ViewComponent {
    model: string;
    modelChanged: Subject<string> = new Subject<string>();

    constructor() {
        this.modelChanged
            .debounceTime(300) // wait 300ms after the last event before emitting last event
            .distinctUntilChanged() // only emit if value is different from previous value
            .subscribe(model => this.model = model);
    }

    changed(text: string) {
        this.modelChanged.next(text);
    }
}

Cela déclenche la détection des changements. pour un moyen qui ne déclenche pas la détection de changement, consultez la réponse de Mark.

84
répondu 0xcaff 2017-05-23 12:34:39

Pas directement accessible comme dans angular1 mais vous pouvez facilement jouer avec NgFormControl et RxJS observables:

<input type="text" [ngFormControl]="term"/>

this.items = this.term.valueChanges
  .debounceTime(400)
  .distinctUntilChanged()
  .switchMap(term => this.wikipediaService.search(term));

Ce billet de blog l'explique clairement: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Ici c'est pour une saisie semi-automatique mais cela fonctionne dans tous les scénarios.

27
répondu bertrandg 2016-01-07 13:42:06

Il pourrait être mis en œuvre en tant que Directive

import { Directive, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { NgControl } from '@angular/forms';
import 'rxjs/add/operator/debounceTime'; 
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/takeUntil'; 

@Directive({
    selector: '[ngModel][debounce]',
})
export class DebounceDirective implements OnInit, OnDestroy {
    @Output()
    public onDebounce = new EventEmitter<any>();

    @Input('debounce')
    public debounceTime: number = 500;

    private isFirstChange: boolean = true;
    private ngUnsubscribe: Subject<void> = new Subject<void>();

    constructor(public model: NgControl) {
    }

    ngOnInit() {
        this.model.valueChanges
            .takeUntil(this.ngUnsubscribe)
            .debounceTime(this.debounceTime)
            .distinctUntilChanged()
            .subscribe(modelValue => {
                if (this.isFirstChange) {
                    this.isFirstChange = false;
                } else {
                    this.onDebounce.emit(modelValue);
                }
            });
    }

    ngOnDestroy() {
        this.ngUnsubscribe.next();
        this.ngUnsubscribe.complete();
    }

}

Utilisez-le comme

<input [(ngModel)]="model" [debounce]="500" (onDebounce)="doSomethingWhenModelIsChanged()">
24
répondu Oleg Polezky 2018-09-03 05:30:12

Vous pouvez créer un RxJS (v. 6) Observable qui fait tout ce que vous voulez.

Vue.composant.html

<input type="text" (input)="onSearchChange($event.target.value)" />

Vue.composant.ts

import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class ViewComponent {
    searchChangeObserver;

  onSearchChange(searchValue: string) {

    if (!this.searchChangeObserver) {
      Observable.create(observer => {
        this.searchChangeObserver = observer;
      }).pipe(debounceTime(300)) // wait 300ms after the last event before emitting last event
        .pipe(distinctUntilChanged()) // only emit if value is different from previous value
        .subscribe(console.log);
    }

    this.searchChangeObserver.next(searchValue);
  }  


}
11
répondu Matthias 2018-07-05 00:06:52

Pour toute personne utilisant les lodash, il est extrêmement facile de antirebond n'importe quelle fonction:

changed = _.debounce(function() {
    console.log("name changed!");
}, 400);

Ensuite, il suffit de lancer quelque chose comme ceci dans votre modèle:

<input [ngModel]="firstName" (ngModelChange)="changed()" />
6
répondu br4d 2017-03-16 22:27:52

J'ai résolu cela en écrivant un décorateur debounce. Le problème décrit peut être résolu en appliquant le @ debounceAccessor à l'accesseur set de la propriété.

J'ai également fourni un décorateur de debounce supplémentaire pour les méthodes, ce qui peut être utile pour d'autres occasions.

Cela rend très facile de rebondir une propriété ou une méthode. Le paramètre est le nombre de millisecondes que le rebounce devrait durer, 100 ms dans l'exemple ci-dessous.

@debounceAccessor(100)
set myProperty(value) {
  this._myProperty = value;
}


@debounceMethod(100)
myMethod (a, b, c) {
  let d = a + b + c;
  return d;
}

Et voici le code pour le décorateurs:

function debounceMethod(ms: number, applyAfterDebounceDelay = false) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        if (applyAfterDebounceDelay) {
          originalMethod.apply(this, args);
        }
        timeoutId = null;
      }, ms);

      if (!applyAfterDebounceDelay) {
        return originalMethod.apply(this, args);
      }
    }
  }
}

function debounceAccessor (ms: number) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalSetter = descriptor.set;
    descriptor.set = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        timeoutId = null;
      }, ms);
      return originalSetter.apply(this, args);
    }
  }
}

J'ai ajouté un paramètre supplémentaire pour le décorateur de méthode qui vous permet de déclencher la méthode après le délai de debounce. Je l'ai fait pour que je puisse par exemple l'utiliser lorsqu'il est couplé avec mouseover ou redimensionner des événements, où je voulais que la capture se produise à la fin du flux d'événements. Dans ce cas cependant, la méthode ne retournera pas de valeur.

3
répondu Fredrik_Macrobond 2017-02-16 16:40:22

Nous pouvons créer une directive [debounce] qui écrase la fonction viewtomodelupdate par défaut de ngModel par une directive vide.

Code De La Directive

@Directive({ selector: '[debounce]' })
export class MyDebounce implements OnInit {
    @Input() delay: number = 300;

    constructor(private elementRef: ElementRef, private model: NgModel) {
    }

    ngOnInit(): void {
        const eventStream = Observable.fromEvent(this.elementRef.nativeElement, 'keyup')
            .map(() => {
                return this.model.value;
            })
            .debounceTime(this.delay);

        this.model.viewToModelUpdate = () => {};

        eventStream.subscribe(input => {
            this.model.viewModel = input;
            this.model.update.emit(input);
        });
    }
}

Comment l'utiliser

<div class="ui input">
  <input debounce [delay]=500 [(ngModel)]="myData" type="text">
</div>
3
répondu BebbaPig 2017-12-15 00:01:57

Une solution Simple serait de créer une directive que vous pouvez appliquer à n'importe quel contrôle.

import { Directive, ElementRef, Input, Renderer, HostListener, Output, EventEmitter } from '@angular/core';
import { NgControl } from '@angular/forms';

@Directive({
    selector: '[ngModel][debounce]',
})
export class Debounce 
{
    @Output()
    public onDebounce = new EventEmitter<any>();

    @Input('debounce')
    public debounceTime: number = 500;


    private modelValue = null;

    constructor(public model: NgControl, el: ElementRef, renderer: Renderer)
    {

    }


    ngOnInit()
    {
         this.modelValue = this.model.value;

         if (!this.modelValue)
         {
            var firstChangeSubs = this.model.valueChanges.subscribe(v =>
            {
               this.modelValue = v;
               firstChangeSubs.unsubscribe()
            });
        }



        this.model.valueChanges
            .debounceTime(this.debounceTime)
            .distinctUntilChanged()
            .subscribe(mv =>
            {
                if (this.modelValue != mv)
                {
                    this.modelValue = mv;
                    this.onDebounce.emit(mv);
                }


            });
    }
}

L'utilisation serait

<textarea [ngModel]="somevalue"   
          [debounce]="2000"
          (onDebounce)="somevalue = $event"                               
          rows="3">
</textarea>
2
répondu Ashg 2017-02-03 02:39:08

J'ai passé des heures là-dessus, j'espère pouvoir gagner du temps à quelqu'un d'autre. Pour moi, l'approche suivante pour utiliser debounce sur un contrôle est plus intuitive et plus facile à comprendre pour moi. Il est construit sur le angular.io solution docs pour la saisie semi-automatique mais avec la possibilité pour moi d'intercepter les appels sans avoir à dépendre de lier les données au DOM.

Plunker

Un scénario d'utilisation pour cela peut être de vérifier un nom d'utilisateur après qu'il soit tapé pour voir si quelqu'un a déjà pris, puis d'avertissement à l'utilisateur.

Remarque: n'oubliez pas, (blur)="function(something.value) pourrait faire plus de sens pour vous en fonction de vos besoins.

1
répondu Helzgate 2016-09-23 18:50:33

C'est la meilleure solution que j'ai trouvée jusqu'à présent. Met à jour le ngModel sur blur et debounce

import { Directive, Input, Output, EventEmitter,ElementRef } from '@angular/core';
import { NgControl, NgModel } from '@angular/forms';
import 'rxjs/add/operator/debounceTime'; 
import 'rxjs/add/operator/distinctUntilChanged';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';

@Directive({
    selector: '[ngModel][debounce]',
})
export class DebounceDirective {
    @Output()
    public onDebounce = new EventEmitter<any>();

    @Input('debounce')
    public debounceTime: number = 500;

    private isFirstChange: boolean = true;

    constructor(private elementRef: ElementRef, private model: NgModel) {
    }

    ngOnInit() {
        const eventStream = Observable.fromEvent(this.elementRef.nativeElement, 'keyup')
            .map(() => {
                return this.model.value;
            })
            .debounceTime(this.debounceTime);

        this.model.viewToModelUpdate = () => {};

        eventStream.subscribe(input => {
            this.model.viewModel = input;
            this.model.update.emit(input);
        });
    }
}

, Comme emprunté à partir de https://stackoverflow.com/a/47823960/3955513

Puis en HTML:

<input [(ngModel)]="hero.name" 
        [debounce]="3000" 
        (blur)="hero.name = $event.target.value"
        (ngModelChange)="onChange()"
        placeholder="name">

Sur blur le modèle est explicitement mis à jour en utilisant JavaScript.

Exemple ici: https://stackblitz.com/edit/ng2-debounce-working

1
répondu Shyamal Parikh 2018-02-21 10:59:57

, Puisque le sujet est vieux, la plupart des réponses ne fonctionne pas sur Angulaire 6.
Voici donc une solution courte et simple pour Angular 6 avec RxJS.

Importez d'abord les éléments nécessaires:

import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

Initialiser sur ngOnInit:

export class MyComponent implements OnInit {
  notesText: string;
  notesModelChanged: Subject<string> = new Subject<string>();

  constructor() { }

  ngOnInit() {
    this.notesModelChanged
      .pipe(
        debounceTime(2000),
        distinctUntilChanged()
      )
      .subscribe(newText => {
        this.notesText = newText;
        console.log(newText);
      });
  }
}

Utilisez cette façon:

<input [ngModel]='notesText' (ngModelChange)='notesModelChanged.next($event)' />

P.S.: pour des solutions plus complexes et efficaces, vous pouvez toujours vérifier d'autres réponses.

0
répondu Just Shadow 2018-09-09 12:07:13

Pour les formes réactives et la manipulation sous Angular v2 (latest) plus v4 regardez:

Https://github.com/angular/angular/issues/6895#issuecomment-290892514

Espérons qu'il y aura un support natif pour ce genre de choses bientôt...

-1
répondu Matthew Erwin 2017-04-01 04:29:52