57

Is there a way to debounce the template directive (ngModelChange)?

Or, alternatively, what is the least-painful way to do it a different way?

The closest answer I see is this: How to watch for form changes in Angular 2?

So, for example, I have a text input, I want to get onChange updates, but I want to debounce it down from every keystroke:

<input type="text" class="form-control" placeholder="Enter a value" name="foo" [(ngModel)]="input.event.value" (ngModelChange)="onFieldChange($event, input)">

Debounce onFieldChange()

Community
  • 1
  • 1
mtyson
  • 8,196
  • 16
  • 66
  • 106
  • 1
    https://angular.io/docs/ts/latest/tutorial/toh-pt6.html#!#search-by-name may be this link will help you – Devansh Dec 24 '16 at 02:36
  • You could use a debounce decorator, checkout: https://stackoverflow.com/a/61177846/1691423 – vlio20 Sep 17 '20 at 13:20

5 Answers5

78

EDIT

In new version of Angular you can use updateOn in ngModelOption to set 'blur' for example. Link to angular.io documentation.

Code example :

<input [(ngModel)]="value"
  [ngModelOptions]="{ updateOn: 'blur' }"
  (ngModelChange)="updateOnlyOnBlur($event)"> 

LEGACY

Here's the less painful way of debouncing keystrokes if you don't want to use the formcontrol approach.

search.component.html

<input type="text" placeholder="Enter a value" name="foo" [(ngModel)]="txtQuery" (ngModelChange)="onFieldChange($event)">

search.component.ts

    export class SearchComponent {
    
         txtQuery: string; // bind this to input with ngModel
         txtQueryChanged: Subject<string> = new Subject<string>();
    
         constructor() {
          this.txtQueryChanged
            .debounceTime(1000) // wait 1 sec after the last event before emitting last event
            .distinctUntilChanged() // only emit if value is different from previous value
            .subscribe(model => {
              this.txtQuery = model;
    
              // Call your function which calls API or do anything you would like do after a lag of 1 sec
              this.getDataFromAPI(this.txtQuery);
             });
        }
    
    onFieldChange(query:string){
      this.txtQueryChanged.next(query);
    }
}
ibenjelloun
  • 7,425
  • 2
  • 29
  • 53
Sangram Nandkhile
  • 17,634
  • 19
  • 82
  • 116
  • 3
    See @Willi Mentzel's update to this answer: https://stackoverflow.com/a/52977862/467240 – mtyson Oct 25 '18 at 20:09
  • 1
    Is it necessary to set the model back to the txtQuery inside the subscription? – Augusto Barreto Sep 11 '19 at 01:46
  • @AugustoBarreto Because he is using two-way binding (a.k.a. "banana in a box" [()] ), it is not necessary. At the point the subscription is setting `this.txtQuery` it has already been updated by the ngModelChange shorthand via the two-way binding – claudekennilol May 19 '21 at 20:20
47

For RxJs 6+

The chosen answer won't work for RxJs 6+. Here is what you have to change:

The imports have to look like this:

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

You need to call pipe:

// ...
this.txtQueryChanged
   .pipe(debounceTime(1000), distinctUntilChanged())
   .subscribe(model => {
       this.txtQuery = model;
       // api call
   });
 // ...

Take a look at this article for further reading.

Community
  • 1
  • 1
Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
36

I've written a little directive to solve this.

How to use it:

<input [ngModel]="someValue" (ngModelChangeDebounced)="someValue = $event">

You can optionally set a debounce time (default is 500):

[ngModelChangeDebounceTime]="200"

The directive itself:

@Directive({
  selector: '[ngModelChangeDebounced]',
})
export class NgModelChangeDebouncedDirective implements OnDestroy {
  @Output()
  ngModelChangeDebounced = new EventEmitter<any>();
  @Input()
  ngModelChangeDebounceTime = 500; // optional, 500 default

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

  constructor(private ngModel: NgModel) {
    this.subscription = this.ngModel.control.valueChanges.pipe(
      skip(1), // skip initial value
      distinctUntilChanged(),
      debounceTime(this.ngModelChangeDebounceTime)
    ).subscribe((value) => this.ngModelChangeDebounced.emit(value));
  }
}

Stackblitz: https://stackblitz.com/edit/angular-9-0-0-rc-1-y2q2ss

Martin Cremer
  • 5,191
  • 2
  • 32
  • 38
4

If you wanted to add debounceTime while doing http call you can use Subject which is very easy to use . Which is also explained in angular2 tutorial - HTTP.

Devansh
  • 1,277
  • 1
  • 13
  • 19
3

I've accomplished that using a Subject.

public onModelChangeSubject = new Subject<string>();  

Then on the ngOnInit of the component, I added:

ngOnInit() {
  this.onModelChangeSubject.pipe(debounceTime(500)).subscribe(_ => {
    this.doSomething();
  });
}

And finally, in the template, I just made a small change in the ngModelChange event:

<input [(ngModel)]="myValue" (ngModelChange)="onModelChangeSubject.next()">
Tiago Ávila
  • 2,737
  • 1
  • 31
  • 34