how to wait for the last keystroke before executing a function in angular?

Observables seem to be the way to go, but the good old setTimeout will get you a long way as well. For esthetic reasons let’s first rename your input handler:

the backslash event seems a bit double, because this also triggers (input)

<input type="text" placeholder="Search for new results"
  (input)="onInput(input.value)" #input>

In your component you have two choices to handle this input, either with observables or without. Let me show you first without:

export class GridComponent {
  private timeout?: number;

  onInput(value: string): void {
    window.clearTimeout(this.timeout);

    this.timeout = window.setTimeout(() => this.constructNewGrid(value), 300);
  }

  constructNewGrid(value: string): void {
    // expensive operations
  }
}

This looks easy enough, and for your use case it might be enough. But what about those cool rxjs streams people keep talking about. Well that looks like this:

export class GridComponent {
  private search$ = new BehaviorSubject('');

  private destroy$ = new Subject<void>();

  ngOnInit(): void {
    this.search$.pipe(
      // debounce for 300ms
      debounceTime(300),
      // only emit if the value has actually changed
      distinctUntilChanged(),
      // unsubscribe when the provided observable emits (clean up)
      takeUntil(this.destroy$)
    ).subscribe((search) => this.constructNewGrid(search)); 
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  onInput(value: string): void {
    this.search$.next(value);
  }

  constructNewGrid(value: string): void {
    // expensive operations
  }
}

That looks like a lot more code for such a simple thing, and it is. So it’s up to you.


If however you feel like this pattern is something you are going to use more often, you can also think about writing a directive, which would look like this:

@Directive({
  selector: '[debounceInput]'
})
export class DebounceInputDirective {
  @Input()
  debounceTime: number = 0;

  @HostListener('input', '[$event]')
  onInput(event: UIEvent): void {
    this.value$.next((event.target as HTMLInputElement).value);
  }

  private value$ = new Subject<string>();

  @Output()
  readonly debounceInput = this.value$.pipe(
    debounce(() => timer(this.debounceTime || 0)),
    distinctUntilChanged()
  );
}

This you can use in your component like this:

<input type="text" placeholder="Search for new result"
  (debounceInput)="onInput($event)" [debounceTime]="300">

Another way to write this directive in an even more rxjs style is:

@Directive({
  selector: 'input[debounceInput]'
})
export class DebounceInputDirective {
  @Input()
  debounceTime: number = 0;

  constructor(private el: ElementRef<HTMLInputElement>) {}

  @Output()
  readonly debounceInput = fromEvent(this.el.nativeElement, 'input').pipe(
    debounce(() => timer(this.debounceTime)),
    map(() => this.el.nativeElement.value),
    distinctUntilChanged()
  );
}

The good thing about using directive (and unrelated, the async pipe), is that you do not have to worry about lingering rxjs subscriptions. These can be potential memory leaks.


But wait! There’s more. You can forget all those things, and go back to the roots of typescript with angular. Decorators! How about a fancy debounce decorator on your method. Then you can leave everything as you had it before, and just add @debounce(300) above your method:

@debounce(300)
constructNewGrid(event): void {
  // ...
}

What? Really? What does this debounce decorator look like. Well, it could be as simple as this:

function debounce(debounceTime: number) {
  let timeout: number;

  return function (
    _target: any,
    _propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod: Function = descriptor.value;
    
    descriptor.value = (...args: any[]) => {
      window.clearTimeout(timeout);
      timeout = window.setTimeout(() => originalMethod(...args), debounceTime);
    };

    return descriptor;
  };
}

But this is untested code though, but it’s to give you an idea as to what’s all possible 🙂

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top