Angular e debounce

No AngularJS eu posso debitar um modelo usando opções ng-model.

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

Como posso debitar um modelo em Angular? Eu tentei procurar por debounce nos documentos, mas não consegui encontrar nada.

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

Uma solução seria escrever minha própria function de debounce, por exemplo:

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

E meu html

  

Mas estou procurando uma function build in, existe uma em Angular?

Atualizado para RC.5

Com Angular 2 podemos debounce usando o operador RxJS debounceTime debounceTime() em valueChanges observable de um controle de valueChanges :

 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: ` 
{{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

O código acima também inclui um exemplo de como controlar os events de redimensionamento de janelas, conforme solicitado por @albanx em um comentário abaixo.


Embora o código acima seja provavelmente o modo angular de fazê-lo, não é eficiente. Cada pressionamento de tecla e cada evento de redimensionamento, mesmo que sejam debilitados e limitados, resultam na detecção de alterações em execução. Em outras palavras, o debouncing e o throttling não afetam a frequência com que a detecção de alterações é executada . (Eu encontrei um comentário no GitHub de Tobias Bosch que confirma isso.) Você pode ver isso quando você executa o plunker e você vê quantas vezes o ngDoCheck() está sendo chamado quando você digita na checkbox de input ou redimensiona a janela. (Use o botão azul “x” para executar o plunker em uma janela separada para ver os events de redimensionamento.)

Uma técnica mais eficiente é criar RxJS Observables a partir dos events, fora da “zona” de Angular. Dessa forma, a detecção de alterações não é chamada toda vez que um evento é triggersdo. Em seguida, em seus methods de retorno de chamada de assinatura, acione manualmente a detecção de alterações, ou seja, você controla quando a detecção de alterações é chamada:

 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: ` 
{{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

Eu uso ngAfterViewInit() vez de ngOnInit() para garantir que inputElRef seja definido.

detectChanges() executará a detecção de alterações neste componente e seus filhos. Se você preferir executar a detecção de alterações a partir do componente raiz (ou seja, executar uma verificação de detecção de alteração completa), use ApplicationRef.tick() . (Eu coloquei uma chamada para ApplicationRef.tick() nos comentários no plunker.) Note que chamar tick() fará com que ngDoCheck() seja chamado.

Se você não quer lidar com @angular/forms , você pode simplesmente usar um RxJS Subject com bindings de mudança.

view.component.html

  

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 = new Subject(); 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); } } 

Isso triggers a detecção de alterações. Para uma maneira que não acione a detecção de alterações, confira a resposta de Mark.

Não é diretamente acessível como em angular1, mas você pode facilmente brincar com os observáveis ​​NgFormControl e RxJS:

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

Este post do blog explica isso claramente: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Aqui é para um autocomplete mas funciona todos os cenários.

Poderia ser implementado como Directiva

 import { Directive, Input, Output, EventEmitter, 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 OnDestroy { @Output() public onDebounce = new EventEmitter(); @Input('debounce') public debounceTime: number = 500; private isFirstChange: boolean = true; private ngUnsubscribe: Subject = new Subject(); 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(); } } 

usá-lo como

  

Você pode criar um RxJS (v.6) Observable que faça o que quiser.

view.component.html

  

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

Para quem usa o lodash, é extremamente fácil de desfazer qualquer function:

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

em seguida, basta jogar algo assim em seu modelo:

  

Eu resolvi isso escrevendo um decorador de debounce. O problema descrito pode ser resolvido aplicando o @debounceAccessor ao acessador set da propriedade.

Eu também forneci um decorador de debounce adicional para methods, que podem ser úteis para outras ocasiões.

Isto torna muito fácil de debater uma propriedade ou um método. O parâmetro é o número de milissegundos que o debounce deve durar, 100 ms no exemplo abaixo.

 @debounceAccessor(100) set myProperty(value) { this._myProperty = value; } @debounceMethod(100) myMethod (a, b, c) { let d = a + b + c; return d; } 

E aqui está o código para os decoradores:

 function debounceMethod(ms: number, applyAfterDebounceDelay = false) { let timeoutId; return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor) { 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) { let originalSetter = descriptor.set; descriptor.set = function (...args: any[]) { if (timeoutId) return; timeoutId = window.setTimeout(() => { timeoutId = null; }, ms); return originalSetter.apply(this, args); } } } 

Eu adicionei um parâmetro adicional para o decorador de método que permite acionar o método após o atraso de debounce. Eu fiz isso para que eu pudesse, por exemplo, usá-lo quando acoplado com events de mouseover ou resize, onde eu queria que a captura ocorresse no final do stream de events. Neste caso, no entanto, o método não retornará um valor.

Podemos criar uma diretiva [debounce] que sobrescreve a function default viewToModelUpdate do ngModel com uma vazia.

Código da Diretiva

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

Como usá-lo

 

Solução simples seria criar uma diretiva que você pode aplicar a qualquer controle.

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

o uso seria

  

Passei horas nisto, esperançosamente eu posso salvar alguém mais algum tempo. Para mim, a abordagem a seguir para usar o debounce em um controle é mais intuitiva e mais fácil de entender para mim. Ele é construído sobre a solução angular.io docs para autocomplete, mas com a capacidade de interceptar as chamadas sem depender da vinculação dos dados ao DOM.

Plunker

Um cenário de caso para isso pode estar verificando um nome de usuário depois que ele é typescript para ver se alguém já o tomou e avisando o usuário.

Nota: não se esqueça, (blur)="function(something.value) pode fazer mais sentido para você, dependendo de suas necessidades.

Esta é a melhor solução que encontrei até agora. Atualiza o ngModel no blur e 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(); @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); }); } } 

como emprestado de https://stackoverflow.com/a/47823960/3955513

Então em HTML:

  

No blur o modelo é explicitamente atualizado usando o javascript simples.

Exemplo aqui: https://stackblitz.com/edit/ng2-debounce-working

Para Formulários Reativos e manuseio sob Angular v2 (mais recente) mais v4, observe:

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

Espero que haja suporte nativo para esses tipos de coisas em breve …