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?
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();
}
}
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();
}
}
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()
.
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.
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.
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()">
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);
}
}
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()" />
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.
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>
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>
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.
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.
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
, 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.
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...