Angular y antirrebote

160

En AngularJS puedo eliminar el rebote de un modelo usando las opciones ng-model.

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

¿Cómo puedo eliminar el rebote de un modelo en Angular? Traté de buscar el rebote en los documentos pero no pude encontrar nada.

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

Una solución sería escribir mi propia función antirrebote, por ejemplo:

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

Y mi html

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

Pero estoy buscando una función integrada, ¿hay alguna en Angular?

koningdavid
fuente
3
Esto podría ser relevante github.com/angular/angular/issues/1773 , aparentemente no implementado todavía.
Eric Martinez
Mira esta publicación para Angular 7 con RxJS v6 freakyjolly.com/…
Code Spy el

Respuestas:

202

Actualizado para RC.5

Con Angular 2 podemos eliminar el rebote usando el operador RxJS debounceTime()en un valueChangesobservable de control de formulario :

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

El código anterior también incluye un ejemplo de cómo regular los eventos de cambio de tamaño de la ventana, como lo solicitó @albanx en un comentario a continuación.


Aunque el código anterior es probablemente la forma angular de hacerlo, no es eficiente. Cada pulsación de tecla y cada evento de cambio de tamaño, aunque se eliminen y se limiten, da como resultado la detección de cambios en ejecución. En otras palabras, la eliminación de rebotes y la limitación no afectan la frecuencia con la que se ejecuta la detección de cambios . (Encontré un comentario de GitHub de Tobias Bosch que confirma esto). Puedes ver esto cuando ejecutas el plunker y ves cuántas veces ngDoCheck()se llama cuando escribes en el cuadro de entrada o cambias el tamaño de la ventana. (Use el botón azul "x" para ejecutar el plunker en una ventana separada para ver los eventos de cambio de tamaño).

Una técnica más eficiente es crear RxJS Observables usted mismo a partir de los eventos, fuera de la "zona" de Angular. De esta manera, la detección de cambios no se llama cada vez que se dispara un evento. Luego, en sus métodos de devolución de llamada de suscripción, active manualmente la detección de cambios, es decir, usted controla cuándo se llama a la detección de cambios:

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

Lo uso en ngAfterViewInit()lugar de ngOnInit()asegurarme de que inputElRefesté definido.

detectChanges()ejecutará la detección de cambios en este componente y sus hijos. Si prefiere ejecutar la detección de cambios desde el componente raíz (es decir, ejecutar una verificación de detección de cambios completa), utilice ApplicationRef.tick()en su lugar. (Puse una llamada ApplicationRef.tick()en los comentarios en el plunker). Tenga en cuenta que llamar tick()hará ngDoCheck()que se llame.

Mark Rajcok
fuente
2
@ Mark Rajcok, creo que en lugar de [valor], debería usar [ngModel], porque [valor] no actualiza el valor de entrada.
Milad
1
¿Existe algún método genérico de eliminación de rebotes (por ejemplo, para aplicar en el evento de cambio de tamaño de ventana)?
albanx
1
@MarkRajcok Creo que el problema de CD que describiste en tu respuesta se resuelve en github.com/angular/zone.js/pull/843
Jefftopia
2
¿Cuándo necesitaríamos cancelar la suscripción para evitar pérdidas de memoria?
calumniado el
1
@slanden Sí, según netbasal.com/when-to-unsubscribe-in-angular-d61c6b21bad3 , deberíamos darnos de baja de las .fromEvent()suscripciones
Jon Onstott el
153

Si no desea tratar @angular/forms, puede usar un RxJS Subjectcon enlaces de cambio.

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

Esto activa la detección de cambios. Para una forma que no active la detección de cambios, consulte la respuesta de Mark.


Actualizar

.pipe(debounceTime(300), distinctUntilChanged()) es necesario para rxjs 6.

Ejemplo:

   constructor() {
        this.modelChanged.pipe(
            debounceTime(300), 
            distinctUntilChanged())
            .subscribe(model => this.model = model);
    }
0xcaff
fuente
55
¡Prefiero esta solución! Trabajó con angular 2.0.0, rxjs 5.0.0-beta 12
alsco77
2
Funcionó perfectamente, simple y claro, sin forma involucrada. Estoy en Angular 4.1.3, rxjs 5.1.1
quinto
Creo que esta es una solución superior, ya que tiene la opción de trabajar con formularios si es necesario, pero elimina esa dependencia y hace que la implementación sea mucho más simple. Gracias.
Max
2
.pipe(debounceTime(300), distinctUntilChanged())es necesario para rxjs 6
Icycool
La solución me salvó. Estaba usando keyUpevent on input.nativeElementen mat-table, que dejó de funcionar cuando se cambió el número de columnas
igorepst
35

Podría implementarse como Directiva

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

}

úsalo como

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

muestra componente

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);
    });
  }
} 
Oleg Polezky
fuente
1
con más importaciones, eso funcionó para mí: importar "rxjs / add / operator / debounceTime"; importar "rxjs / add / operator / distinctUntilChanged";
Sbl
2
Esto, con mucho, lo convierte en el más sencillo de implementar en toda la aplicación
joshcomley, el
1
isFirstChange se usa para no emitir al inicializar
Oleg Polezky
2
Funciona en Angular 8 y rxjs 6.5.2 con los siguientes cambios. Si desea utilizar la sintaxis de la tubería, cambie lo siguiente: import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged';to import { debounceTime, distinctUntilChanged } from 'rxjs/operators';and this.model.valueChanges .debounceTime(this.debounceTime) .distinctUntilChanged()tothis.model.valueChanges .pipe( debounceTime(this.debounceTime), distinctUntilChanged() )
kumaheiyama
1
Funciona en Angular 9 y rxjs 6.5.4 con los cambios que @kumaheiyama declaró en su comentario. Simplemente no olvides exportar la directiva en el módulo donde la estás creando. Y no olvide incluir el módulo en el que está creando esta directiva, en el módulo donde lo está utilizando.
Filip Savic
29

Como el tema es antiguo, la mayoría de las respuestas no funcionan en Angular 6/7/8/9 y / o usan otras librerías.
Así que aquí hay una solución corta y simple para Angular 6+ con RxJS.

Importa las cosas necesarias primero:

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

Inicializar en ngOnInit:

export class MyComponent implements OnInit, OnDestroy {
  public notesText: string;
  private notesModelChanged: Subject<string> = new Subject<string>();
  private notesModelChangeSubscription: Subscription

  constructor() { }

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

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

Use de esta manera:

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

PD: para soluciones más complejas y eficientes, es posible que aún desee consultar otras respuestas.

Solo sombra
fuente
1
¿Por qué no cancelas esto en destruir?
Virendra Singh Rathore
Actualizado. ¡Gracias por notarlo!
Solo Shadow
1
@ JustShadow ¡Gracias! Fue de mucha ayuda.
Niral Munjariya
Esto funciona perfecto en el primer intento. Pero cuando elimino el texto buscado de alguna manera, la siguiente solicitud tarda demasiado en responder.
Sadiksha Gautam
Eso es extraño. Todavía funciona bien de mi lado. ¿Podría por favor compartir más información o tal vez abrir una nueva pregunta para eso?
Solo Shadow
28

No es accesible directamente como en angular1 pero puedes jugar fácilmente con NgFormControl y observables RxJS:

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

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

Esta publicación de blog lo explica claramente: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Aquí es para un autocompletado pero funciona en todos los escenarios.

Bertrandg
fuente
pero hay un error del servicio, esto no se está ejecutando de nuevo
Arun Tyagi
No entiendo el ejemplo. [...] es un enlace de destino unidireccional. ¿Por qué se puede notificar al contenedor valueChanges? ¿No debería ser algo? como (ngFormControl)="..."?
phil294
20

Puede crear una RxJS (v.6) observable que haga lo que quiera.

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


}
Matías
fuente
Gracias que ayudaron, sin embargo creo que la importación debe ser de rsjs/Rx, tuve errores cuando se utiliza la importación de la manera que lo has escrito ... por lo que en mi caso es ahora:import { Observable } from 'rxjs/Rx';
ghiscoding
2
@ghiscoding Depende de la versión de rxjs. En la versión 6 es: import { Observable } from 'rxjs';.
Matthias
¡Gracias! Como acotación al margen, sólo puede utilizar una pipellamadapipe(debounceTime(300), distinctUntilChanged())
al.
1
searchChangeObserver es un suscriptor, por lo que searchChangeSubscriber será un mejor nombre.
Khonsort el
12

Para cualquiera que use lodash, es extremadamente fácil eliminar cualquier función:

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

entonces simplemente arroje algo como esto en su plantilla:

<(input)="changed($event.target.value)" />
Brad C
fuente
3
o simplemente (input) = "cambiado ($ event.target.value)"
Jamie Kudla
1
Gracias por responder con lodash :)
Vamsi
Creo que esto aún activará la detección de cambio angular en cada cambio, independientemente del rebote.
AsGoodAsItGets
5

Solución con suscriptor de inicialización directamente en función de evento:

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

Y html:

<input type="text" (input)="onFind($event.target.value)">
Serhii Vasko
fuente
Funciona totalmente bien para el cuadro de texto autocompletado angular 8 prime ng. Muchas gracias.
Jasmin Akther Suma
4

Resolví esto escribiendo un decorador antirrebote. El problema descrito podría resolverse aplicando @debounceAccessor al conjunto de acceso de la propiedad.

También he suministrado un decorador antirrebote adicional para métodos, que puede ser útil para otras ocasiones.

Esto hace que sea muy fácil eliminar el rebote de una propiedad o un método. El parámetro es el número de milisegundos que debe durar el rebote, 100 ms en el siguiente ejemplo.

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


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

Y aquí está el código para los decoradores:

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

Agregué un parámetro adicional para el decorador de métodos que le permite activar el método DESPUÉS del retraso de rebote. Lo hice para poder usarlo, por ejemplo, cuando se combina con mouseover o cambiar el tamaño de los eventos, donde quería que la captura ocurriera al final de la secuencia de eventos. En este caso, sin embargo, el método no devolverá un valor.

Fredrik_Macrobond
fuente
3

Podemos crear una directiva [debounce] que sobrescribe la función viewToModelUpdate predeterminada de ngModel con una vacía.

Código de directiva

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

Cómo usarlo

<div class="ui input">
  <input debounce [delay]=500 [(ngModel)]="myData" type="text">
</div>
BebbaPig
fuente
2

Archivo HTML:

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

Archivo TS:

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

Una solución simple sería crear una directiva que pueda aplicar a cualquier control.

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

el uso sería

<textarea [ngModel]="somevalue"   
          [debounce]="2000"
          (onDebounce)="somevalue = $event"                               
          rows="3">
</textarea>
Ashg
fuente
Esta clase está lejos de compilarse Angular 7.
Stephane
1

Pasé horas en esto, espero poder salvar a alguien más en algún momento. Para mí, el siguiente enfoque para usar debounceen un control es más intuitivo y más fácil de entender para mí. Se basa en la solución angular.io docs para autocompletar, pero con la capacidad de interceptar las llamadas sin tener que depender de vincular los datos al DOM.

Saqueador

Un escenario de caso de uso para esto podría ser verificar un nombre de usuario después de escribirlo para ver si alguien ya lo ha tomado, y luego advertir al usuario.

Nota: no lo olvide, (blur)="function(something.value)podría tener más sentido para usted según sus necesidades.

Helzgate
fuente
1

DebounceTime en Angular 7 con RxJS v6

Enlace fuente

Enlace de demostración

ingrese la descripción de la imagen aquí

En plantilla HTML

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

En componente

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

    }
Code Spy
fuente
1

También podría resolver esto usando un decorador, por ejemplo, usando el decorador debounce de utils-decorator lib ( npm install utils-decorators):

import {debounce} from 'utils-decorators';

class MyAppComponent {

  @debounce(500)
  firstNameChanged($event, first) {
   ...
  }
}
vlio20
fuente
0

Esta es la mejor solución que he encontrado hasta ahora. Actualiza la ngModelsobre blurydebounce

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

prestado de https://stackoverflow.com/a/47823960/3955513

Luego en HTML:

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

En blurel modelo se actualiza explícitamente usando JavaScript simple.

Ejemplo aquí: https://stackblitz.com/edit/ng2-debounce-working

Shyamal Parikh
fuente