Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wait for the last keystroke before executing a function in Angular?

I have following input:

 <input type="text" placeholder="Search for new results" (input)="constructNewGrid($event)" (keydown.backslash)="constructNewGrid($event)">

and the function

constructNewGrid(e){
    // I want to wait 300ms after the last keystroke before constructing the new grid
    // If the passed time is <300ms just return without doing something
    // else start constructing new grid
}

I am not quite sure how to build such a condition. How should I approach this problem? I read about debounceTime in RxJS which exactly what I want but I do not use observables in the function, so: How would you build such a condition in your function?

like image 301
CNIS Avatar asked Jan 25 '26 22:01

CNIS


1 Answers

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

like image 93
Poul Kruijt Avatar answered Jan 28 '26 16:01

Poul Kruijt