web-dev-qa-db-fra.com

Angulaire et rebond

Dans AngularJS, je peux lancer un modèle en utilisant les options de modèle ng.

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

Comment puis-je faire rebondir un modèle dans Angular? J'ai essayé de rechercher des rebonds dans la documentation mais je n'ai rien trouvé.

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

Une solution serait d'écrire ma propre fonction anti-rebond, 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 dans la construction, y-a-t-il une version dans Angular?

115
koningdavid

Mis à jour pour RC.5

Avec Angular 2, nous pouvons utiliser l’opérateur debounceTime() de RxJS sur l’observable valueChanges d’un contrôle de formulaire:

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 limitation des événements de redimensionnement de 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 de touche et chaque événement de redimensionnement, même s'ils sont réduits au minimum et réduits, entraînent la détection des modifications. En d'autres termes, le rebond et la régulation n'affectent pas la fréquence de détection des modifications . (J'ai trouvé un commentaire GitHub de Tobias Bosch qui le confirme.) Vous pouvez le voir lorsque vous exécutez le plongeur et combien de fois que ngDoCheck() est appelé lorsque vous tapez dans la zone de saisie ou redimensionnez le la fenêtre. (Utilisez le bouton bleu "x" pour exécuter le plongeur dans une fenêtre séparée pour voir les événements de redimensionnement.)

Une technique plus efficace consiste à créer vous-même des observables RxJS à partir d'é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 est déclenché. Ensuite, dans vos méthodes de rappel d’abonnement, déclenchez manuellement la détection de changement - c’est-à-dire que vous contrôlez quand 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 m'assurer que inputElRef est défini.

detectChanges() exécutera la détection des modifications sur ce composant et 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 plutôt ApplicationRef.tick() . (Je mets un appel à ApplicationRef.tick() dans les commentaires du périphérique.) Notez que l'appel de tick() provoquera l'appel de ngDoCheck().

180
Mark Rajcok

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

view.component.html

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

view.component.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 de changement. Pour un moyen qui ne déclenche pas la détection de changement, consultez la réponse de Mark.


Mettre à jour

.pipe(debounceTime(300), distinctUntilChanged()) est nécessaire pour rxjs 6.

Exemple:

   constructor() {
        this.modelChanged.pipe(
            debounceTime(300), 
            distinctUntilChanged())
            .subscribe(model => this.model = model);
    }
103
0xcaff

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

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

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

Cet article de blog l'explique clairement: http://blog.edlytram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

La voici pour une saisie semi-automatique mais cela fonctionne dans tous les scénarios.

28
bertrandg

Elle pourrait être mise 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 { Subscription } from 'rxjs';

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

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

  private isFirstChange: boolean = true;
  private subscription: Subscription;

  constructor(public model: NgControl) {
  }

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

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

l'utiliser comme

<input [(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">

échantillon de composant

import { Component } from "@angular/core";

@Component({
  selector: 'app-sample',
  template: `
<input[(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">
<input[(ngModel)]="value" (onDebounce)="asyncDoSomethingWhenModelIsChanged($event)">
`
})
export class SampleComponent {
  value: string;

  doSomethingWhenModelIsChanged(value: string): void {
    console.log({ value });
  }

  async asyncDoSomethingWhenModelIsChanged(value: string): Promise<void> {
    return new Promise<void>(resolve => {
      setTimeout(() => {
        console.log('async', { value });
        resolve();
      }, 1000);
    });
  }
} 
26
Oleg Polezky

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

view.component.html

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

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


}
14
Matthias

Pour ceux qui utilisent lodash, il est extrêmement facile de rebounce n’importe quelle fonction:

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

puis lancez quelque chose comme ceci dans votre modèle:

<input [ngModel]="firstName" (ngModelChange)="changed()" />
8
br4d

J'ai résolu ce problème en écrivant un décorateur anti-rebond. Le problème décrit pourrait être résolu en appliquant le @debounceAccessor à l'accesseur set de la propriété.

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

Cela facilite grandement le rebond d'une propriété ou d'une méthode. Le paramètre est le nombre de millisecondes que le rebond doit 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 les 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 anti-rebond. Je l'ai fait pour pouvoir, par exemple, l'utiliser lorsqu'il est associé à des événements de survol ou de redimensionnement, où je voulais que la capture ait lieu à la fin du flux d'événements. Dans ce cas toutefois, la méthode ne renverra pas de valeur.

3
Fredrik_Macrobond

Nous pouvons créer une directive [debounce] qui remplace la fonction viewToModelUpdate par défaut de ngModel par une fonction 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
BebbaPig

Une solution simple consisterait à 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
Ashg

Comme le sujet est ancien, la plupart des réponses ne fonctionnent pas sur angulaire 6 .
Voici donc une solution simple et rapide 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 manière:

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

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

2
Just Shadow

J'y ai passé des heures, j'espère pouvoir sauver 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. Il est construit sur la solution angular.io docs pour la saisie semi-automatique, mais avec la possibilité pour moi d'intercepter les appels sans avoir à dépendre de la liaison des données au DOM.

Plunker

Un scénario d'utilisation pourrait consister à vérifier un nom d'utilisateur après sa saisie pour voir si quelqu'un l'a déjà pris, puis à en avertir l'utilisateur.

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

1
Helzgate

Fichier HTML:

<input [ngModel]="filterValue"
       (ngModelChange)="filterValue = $event ; search($event)"
        placeholder="Search..."/>

Fichier TS:

timer = null;
time = 250;
  search(searchStr : string) : void {
    clearTimeout(this.timer);
    this.timer = setTimeout(()=>{
      console.log(searchStr);
    }, time)
  }
1
Yoav Schniederman

DebounceTime in Angular 7 avec RxJS v6

Source Lien

Démo Lien

 enter image description here 

Dans le modèle HTML

<input type="text" #movieSearchInput class="form-control"
            placeholder="Type any movie name" [(ngModel)]="searchTermModel" />

En composant

    ....
    ....
    export class AppComponent implements OnInit {

    @ViewChild('movieSearchInput') movieSearchInput: ElementRef;
    apiResponse:any;
    isSearching:boolean;

        constructor(
        private httpClient: HttpClient
        ) {
        this.isSearching = false;
        this.apiResponse = [];
        }

    ngOnInit() {
        fromEvent(this.movieSearchInput.nativeElement, 'keyup').pipe(
        // get value
        map((event: any) => {
            return event.target.value;
        })
        // if character length greater then 2
        ,filter(res => res.length > 2)
        // Time in milliseconds between key events
        ,debounceTime(1000)        
        // If previous query is diffent from current   
        ,distinctUntilChanged()
        // subscription for response
        ).subscribe((text: string) => {
            this.isSearching = true;
            this.searchGetCall(text).subscribe((res)=>{
            console.log('res',res);
            this.isSearching = false;
            this.apiResponse = res;
            },(err)=>{
            this.isSearching = false;
            console.log('error',err);
            });
        });
    }

    searchGetCall(term: string) {
        if (term === '') {
        return of([]);
        }
        return this.httpClient.get('http://www.omdbapi.com/?s=' + term + '&apikey=' + APIKEY,{params: PARAMS.set('search', term)});
    }

    }
1
Code Spy

C'est la meilleure solution que j'ai trouvée jusqu'à présent. Met à jour les ngModelsur 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);
        });
    }
}

emprunté à 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 à l'aide de javascript simple.

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

1
Shyamal Parikh

Solution avec initialisation abonné directement en fonction événement:

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

class MyAppComponent {
    searchTermChanged: Subject<string> = new Subject<string>();

    constructor() {
    }

    onFind(event: any) {
        if (this.searchTermChanged.observers.length === 0) {
            this.searchTermChanged.pipe(debounceTime(1000), distinctUntilChanged())
                .subscribe(term => {
                    // your code here
                    console.log(term);
                });
        }
        this.searchTermChanged.next(event);
    }
}

Et html:

<input type="text" (input)="onFind($event.target.value)">
0
Serhii Vasko