206

In AngularJS I was able to debounce a model by using ng-model options.

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

How can I debounce a model in Angular?
I tried to search for debounce in the docs but I couldn't find anything.

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

A solution would be to write my own debounce function, for example:

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

And my html

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

But I'm looking for a built in function, is there one in Angular?

Just Shadow
  • 10,860
  • 6
  • 57
  • 75
koningdavid
  • 7,903
  • 7
  • 35
  • 46

16 Answers16

236

Updated for RC.5

With Angular 2 we can debounce using RxJS operator debounceTime() on a form control's 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

The code above also includes an example of how to throttle window resize events, as asked by @albanx in a comment below.


Although the above code is probably the Angular-way of doing it, it is not efficient. Every keystroke and every resize event, even though they are debounced and throttled, results in change detection running. In other words, debouncing and throttling do not affect how often change detection runs. (I found a GitHub comment by Tobias Bosch that confirms this.) You can see this when you run the plunker and you see how many times ngDoCheck() is being called when you type into the input box or resize the window. (Use the blue "x" button to run the plunker in a separate window to see the resize events.)

A more efficient technique is to create RxJS Observables yourself from the events, outside of Angular's "zone". This way, change detection is not called each time an event fires. Then, in your subscribe callback methods, manually trigger change detection – i.e., you control when change detection is called:

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

I use ngAfterViewInit() instead of ngOnInit() to ensure that inputElRef is defined.

detectChanges() will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick() instead. (I put a call to ApplicationRef.tick() in comments in the plunker.) Note that calling tick() will cause ngDoCheck() to be called.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 2
    @Mark Rajcok I think instead of [value] , you should use [ngModel] , because [value] doesn't update the input value . – Milad Jun 19 '16 at 14:04
  • 1
    is there any generic debounce method (for example to apply on window resize event)? – albanx Jul 15 '16 at 19:56
  • I would add this two lines after `.debounceTime(1000)` just to improve that (later in the subscribe you'll already have the value): `.map((event: any) => event.target.value)` `.distinctUntilChanged()` – Dunos Sep 22 '17 at 12:14
  • 1
    @MarkRajcok I believe the CD issue you described in your answer is resolved by https://github.com/angular/zone.js/pull/843 – Jefftopia Nov 10 '17 at 16:29
  • @MarkRajcok I used `this.ngzone.run(() => {});` to render response from `subscribe` method of a `httpRequest` invoked from `this.ngzone.runOutsideAngular` since `this.cdref.detectChanges()` was not aware of returned results. Don't know is it the correct way of doing this! – Midhun KM Dec 29 '17 at 09:13
  • 2
    When would we need to unsubscribe to prevent memory leaks? – slanden Jan 24 '18 at 17:28
  • 1
    @slanden Yes, acccording to https://netbasal.com/when-to-unsubscribe-in-angular-d61c6b21bad3, we should unsubscribe from `.fromEvent()` subscriptions – Jon Onstott Jan 26 '18 at 19:07
199

If you don't want to deal with @angular/forms, you can just use an RxJS Subject with change bindings.

view.component.html

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

view.component.ts

import { Subject } from 'rxjs';
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);
    }
}

This does trigger change detection. For a way that doesn't trigger change detection, check out Mark's answer.


Update

.pipe(debounceTime(300), distinctUntilChanged()) is needed for rxjs 6.

Example:

   constructor() {
        this.modelChanged.pipe(
            debounceTime(300), 
            distinctUntilChanged())
            .subscribe(model => this.model = model);
    }
Ignacio Ara
  • 2,476
  • 2
  • 26
  • 37
0xcaff
  • 13,085
  • 5
  • 47
  • 55
  • 7
    I prefer this solution! Worked with angular 2.0.0, rxjs 5.0.0-beta 12 –  Nov 24 '16 at 11:21
  • 2
    Worked perfectly, simple and clear, no form involved. I'm on Angular 4.1.3, rxjs 5.1.1 – fifth Jun 12 '17 at 01:12
  • I think this is superior solution since it has the option to work with forms if needed, but removes that dependency making implementation that much simpler. Thanks. – max Jan 12 '18 at 21:37
  • 2
    `.pipe(debounceTime(300), distinctUntilChanged())` is needed for rxjs 6 – Icycool Nov 14 '18 at 06:59
  • The solution saved me. I was using `keyUp` event on `input.nativeElement` in a `mat-table`, that stopped working when the number of columns was changed – igorepst Dec 27 '18 at 09:34
  • Is there anyway to prevent change detection with this method? – Ankur Akvaliya Feb 06 '19 at 11:28
  • Certainly it is possible to prevent change detection with this method, as I just recently adapted both this answer and the selected one, and worked just fine. I even added an extra conditional check to maximize api rest call efficiency. Working on ng v. 7.x – j4v1 Jun 13 '19 at 23:49
  • distinctUntilChanged() is a life saver, otherwise your form values will refresh all the time and interrupt i.e. sending queryParams! – Marcin Wojtach Oct 17 '19 at 17:56
  • `import { Subject } from 'rxjs/Subject';` gives error with angular 8. Use `import { Subject } from 'rxjs';` – canbax Nov 09 '19 at 09:58
  • 1
    Do you think we need to do unsubscribe or another thing on OnDestroy? – Murat Can OĞUZHAN May 12 '21 at 08:23
65

Since the topic is old, most of the answers don't work on Angular 6-13 and/or use other libs.
So here is a short and simple solution for Angular 6+ with RxJS.

Import the necessary stuff first:

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

Implement the ngOnInit and ngOnDestroy:

export class MyComponent implements OnInit, OnDestroy {
  public notesText: string;
  public 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 this way:

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

P.S. For more complex and efficient solutions you might still want to check other answers.

Just Shadow
  • 10,860
  • 6
  • 57
  • 75
  • 2
    @JustShadow Thank you! It was really helpful. – Niral Munjariya Jul 16 '19 at 12:38
  • This works perfect on the first try. But when I delete the searched text somehow the next request takes too long to respond. – Sadiksha Gautam Nov 12 '19 at 09:33
  • That's strange. It still works fine on my side. Could you please share more info or maybe open a new question for that? – Just Shadow Nov 12 '19 at 10:03
  • hats off... thank you @JustShadow! @SadikshaGautam must have already got the solution by now but for new ones... you might need to just lower the debouceTime(2000) from 2000 milliseconds to something lower, perhaps the usual delay which is 300milliseconds. – Dynamic Remo Jan 30 '22 at 19:58
  • In Angular14 I'm getting: ```error TS2345: Argument of type 'Event' is not assignable to parameter of type 'string'.``` – jessewolfe Aug 22 '22 at 19:59
  • 1
    I found the issue - you have to have: ````import { FormsModule } from '@angular/forms';```` and also put FormsModule in the imports: attribute of your @NgModule that needs to use ngModel. You also must have a "name" attribute on your element. – jessewolfe Aug 22 '22 at 21:48
37

It could be implemented as 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();
  }

}

use it like

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

component sample

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
  • 1,006
  • 14
  • 13
  • 1
    with more imports, that worked for me: import "rxjs/add/operator/debounceTime"; import "rxjs/add/operator/distinctUntilChanged"; – Sbl May 09 '17 at 09:08
  • 2
    This by far makes it the simplest to implement application wide – joshcomley Nov 09 '17 at 01:49
  • Isn't working checkout: https://stackblitz.com/edit/ng2-debounce-example?file=src/app/app.module.ts – Shyamal Parikh Feb 21 '18 at 10:14
  • @Shyamal Parikh, it is working. You need to add (onDebounce)="doSomethingWhenModelIsChanged()" and the implementation to the component – Oleg Polezky Feb 22 '18 at 16:57
  • 1
    isFirstChange is used not to emit on initialize – Oleg Polezky Sep 03 '18 at 05:26
  • A logger shows me my event `this.debounceEvent.emit(modelValue);` is emitted, but my `search(searchTerm: string): void` method is not called. Why do we need the `[(ngModel)]="searchTerm"` in ``? – Stephane Dec 06 '18 at 09:00
  • I posted my question https://stackoverflow.com/questions/53648388/directive-event-not-being-received-by-the-component – Stephane Dec 06 '18 at 10:28
  • It would be nice if you can update your answer and use the new [pipe syntax](https://rxjs-dev.firebaseapp.com/guide/v6/migration#operator-pipe-syntax) – Augusto Barreto Nov 17 '19 at 15:56
  • 3
    Works in Angular 8 and rxjs 6.5.2 with the following changes. If you want to use the pipe syntax, change the following: `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()` to `this.model.valueChanges .pipe( debounceTime(this.debounceTime), distinctUntilChanged() )` – kumaheiyama Nov 24 '19 at 16:02
  • 1
    Works in Angular 9 and rxjs 6.5.4 with changes @kumaheiyama stated in his comment. Just don't forget to export the directive in the module where you're creating it. And don't forget to include the module you're creating this directive in, into the module where you are using it. – Filip Savic Apr 09 '20 at 02:08
  • Also, a minor improvement. Instead of doing the `isFirstChange` check, you can just add a filter `filter(value => value != null)` as first in the `pipe()`. Another one would be to not do a search for less than, say, 2 characters. That would be another filter like this: `filter(value => value.length > 2)`. Imports: `import { debounceTime, distinctUntilChanged, map, filter } from 'rxjs/operators';`. – Filip Savic Apr 09 '20 at 12:24
31

Not directly accessible like in angular1 but you can easily play with NgFormControl and RxJS observables:

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

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

This blog post explains it clearly: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Here it is for an autocomplete but it works all scenarios.

sdgluck
  • 24,894
  • 8
  • 75
  • 90
bertrandg
  • 3,147
  • 2
  • 27
  • 35
  • but there is an error from service, this is not running again – Arun Tyagi Nov 18 '16 at 02:05
  • I dont understand the example. `[...]` is one-way target binding. Why can the container be notified of `valueChanges`? shouldn't it have to be sth. like `(ngFormControl)="..."`? – phil294 Dec 15 '16 at 22:22
27

You can create an RxJS (v.6) Observable that does whatever you like.

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


}
Matthias
  • 1,150
  • 20
  • 38
  • Thanks that helped, however I think the import should be from `rsjs/Rx`, I had errors when using the import the way you wrote it... so in my case it's now: `import { Observable } from 'rxjs/Rx';` – ghiscoding Apr 05 '18 at 21:52
  • 2
    @ghiscoding It depends on the rxjs version. In version 6 it is: ```import { Observable } from 'rxjs';```. – Matthias Jul 05 '18 at 00:07
  • Thanks! As an aside, you can just use one `pipe` call `pipe(debounceTime(300), distinctUntilChanged())` – al. Sep 12 '18 at 20:46
  • 1
    searchChangeObserver is a Subscriber, so searchChangeSubscriber will be a better name. – Khonsort Sep 13 '18 at 12:27
16

For anyone using lodash, it is extremely easy to debounce any function:

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

then just throw something like this into your template:

<(input)="changed($event.target.value)" />
Brad C
  • 2,868
  • 22
  • 33
8

Solution with initialization subscriber directly in event function:

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

And html:

<input type="text" (input)="onFind($event.target.value)">
Serhii Vasko
  • 383
  • 4
  • 11
4

I solved this by writing a debounce decorator. The problem described could be solved by applying the @debounceAccessor to the property's set accessor.

I've also supplied an additional debounce decorator for methods, which can be useful for other occasions.

This makes it very easy to debounce a property or a method. The parameter is the number of milliseconds the debounce should last, 100 ms in the example below.

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


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

And here's the code for the decorators:

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

I added an additional parameter for the method decorator which let's you trigger the method AFTER the debounce delay. I did that so I could for instance use it when coupled with mouseover or resize events, where I wanted the capturing to occur at the end of the event stream. In this case however, the method won't return a value.

Fredrik_Borgstrom
  • 2,504
  • 25
  • 32
4

We can create a [debounce] directive which overwrites ngModel's default viewToModelUpdate function with an empty one.

Directive Code

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

How to use it

<div class="ui input">
  <input debounce [delay]=500 [(ngModel)]="myData" type="text">
</div>
BebbaPig
  • 118
  • 1
  • 6
3

HTML file:

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

TS file:

timer = null;
time = 250;
  search(searchStr : string) : void {
    clearTimeout(this.timer);
    this.timer = setTimeout(()=>{
      console.log(searchStr);
    }, time)
  }
Yoav Schniederman
  • 5,253
  • 3
  • 28
  • 32
3

DebounceTime in Angular 7 with RxJS v6

Source Link

Demo Link

enter image description here

In HTML Template

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

In component

    ....
    ....
    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
  • 9,626
  • 4
  • 66
  • 46
3

You could also solve this by using a decorators, For an instance by using the debounce decorator from utils-decorator lib (npm install utils-decorators):

import {debounce} from 'utils-decorators';

class MyAppComponent {

  @debounce(500)
  firstNameChanged($event, first) {
   ...
  }
}
vlio20
  • 8,955
  • 18
  • 95
  • 180
2

Simple solution would be to create a directive which you can apply to any 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);
                }
            });
    }
}

usage would be

<textarea [ngModel]="somevalue"   
          [debounce]="2000"
          (onDebounce)="somevalue = $event"                               
          rows="3">
</textarea>
rhavelka
  • 2,283
  • 3
  • 22
  • 36
Ashg
  • 2,526
  • 1
  • 10
  • 5
1

Spent hours on this, hopefully I can save someone else some time. To me the following approach to using debounce on a control is more intuitive and easier to understand for me. It's built on the angular.io docs solution for autocomplete but with the ability for me to intercept the calls without having to depend on tying the data to the DOM.

Plunker

A use case scenario for this might be checking a username after it's typed to see if someone has already taken it, then warning the user.

Note: don't forget, (blur)="function(something.value) might make more sense for you depending on your needs.

Post Impatica
  • 14,999
  • 9
  • 67
  • 78
0

This is the best solution I have found till now. Updates the ngModelon blur and 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);
        });
    }
}

as borrowed from https://stackoverflow.com/a/47823960/3955513

Then in HTML:

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

On blur the model is explicitly updated using plain javascript.

Example here: https://stackblitz.com/edit/ng2-debounce-working

Shyamal Parikh
  • 2,988
  • 4
  • 37
  • 78