Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make observable debounceTime conditional

Tags:

angular

rxjs

I have a simple debounce on an element input event like so:

Observable
        .fromEvent(this.elInput.nativeElement, 'input')
        .debounceTime(2000)
        .subscribe(event => this.onInput(event));

I would like to make the debounce conditional based on the value of the event when emitted, is this possible?

Thanks

like image 977
mindparse Avatar asked Oct 29 '18 11:10

mindparse


People also ask

How does debounce work with observables?

debounce emits a value form the source Observable only after a particular life span determined by another Observable has passed without another source emission. //... // emits the `searchText` into the stream. This will cause the operators in its pipe function (defined in the ngOnInit method) to be run. `debounce` runs and then `map`.

What is debouncetime in angular observable?

It is used with pipe operator of Observable. debounceTime is useful in operation where user changes inputs frequently such as search operation. Find the technologies being used in our example. 1. Angular 7.0.0

How does the debouncetime operator count time?

The Debouncetime operator starts counting time after it receives a value. If the source observable emits a value before the timeout duration, then counting is reset to zero & started again. When the timeout duration elapses the operator emits the last value and the counting stops. Counting starts again when the operators receive another value.

What is debouncetime?

The Debouncetime emits the last received value from the source observable after a specified amount of time has elapsed without any other value appearing on the source Observable Where dueTime The timeout duration in milliseconds.


2 Answers

Yes, that's completely possible. Just make use of the debounce operator instead of debounceTime. It is passed a selector function that receives the previous operators notifaction when invoked.

In your example:

import { fromEvent } from 'rxjs';
import { debounce, of, timer } from 'rxjs/operators';
//...
fromEvent(this.elInput.nativeElement, 'input')
    .pipe(debounce(ev => ev.hasSomeValue ? timer(2000) : of({})))
    .subscribe(event => this.onInput(event));

The selector function expects an ObservableLike and waits for it to emit before forwarding the last notification that debounce received. All other notifications are discarded, like with debounceTime. You can use EMPTY to immediately forward the notifcation without any timeout (though this will be async, see below)

From learn-rxjs:

Though not as widely used as debounceTime, debounce is important when the debounce rate is variable!

Note: Debounce will always asynchronously schedule the forwarding of the last value, even if the inner Observable emits instantly. To avoid this, you'd have to create a second observable and use filter to avoid debounce altogether.

like image 125
ggradnig Avatar answered Oct 21 '22 08:10

ggradnig


Previous answer works if no variables are to be processed by the pipe. Plus passing 0 and asap to debounceTime (as so: debounceTime(0, asap)) does mean the next operator will be immediately called. In fact, once you use debounceTime you may experience a half second delay anyway, which is way too long in some cases.

So, after long time messing up with iif and friends, I decided to copy paste debounceTime and modify it to take a conditional function. Any ideas / improvements are most welcome:

This is intended for Angular 9 and may raise warnings/errors with previous versions.

Use it just like you would with debounceTime, just pass a conditional function as first boolean parameter. If it is true, it will debounce, if false it will immediately continue.

Example:

observable$.pipe(
    conditionalDebounceTime(() => conditionFlag, 1000),
    tap(observedValue ... => ...)
    ...

conditional-debounce-time.ts

import {MonoTypeOperatorFunction, Observable, Operator, SchedulerLike, Subscriber, Subscription, TeardownLogic} from 'rxjs';
import {async} from 'rxjs/internal/scheduler/async';

class ConditionalDebounceTimeSubscriber<T> extends Subscriber<T> {
  private debouncedSubscription: Subscription|null = null;
  private lastValue: T|null = null;
  private hasValue = false;

  constructor(destination: Subscriber<T>,
              private conditionFunc: () => boolean,
              private dueTime: number,
              private scheduler: SchedulerLike) {
    super(destination);
  }

  protected _next(value: T) {
    this.clearDebounce();
    this.lastValue = value;
    this.hasValue = true;
    if (this.conditionFunc()) {
      this.add(this.debouncedSubscription = this.scheduler.schedule(dispatchNext, this.dueTime, this));
    } else {
      (<Subscriber<T>>this.destination).next(this.lastValue);
    }
  }

  protected _complete() {
    this.debouncedNext();
    (<Subscriber<T>>this.destination)!.complete();
  }

  debouncedNext(): void {
    this.clearDebounce();

    if (this.hasValue) {
      const { lastValue } = this;
      // This must be done *before* passing the value
      // along to the destination because it's possible for
      // the value to synchronously re-enter this operator
      // recursively when scheduled with things like
      // VirtualScheduler/TestScheduler.
      this.lastValue = null;
      this.hasValue = false;
      (<Subscriber<T>>this.destination)!.next(lastValue!);
    }
  }

  private clearDebounce(): void {
    const debouncedSubscription = this.debouncedSubscription;

    if (debouncedSubscription !== null) {
      this.remove(debouncedSubscription);
      debouncedSubscription.unsubscribe();
      this.debouncedSubscription = null;
    }
  }
}

class ConditionalDebounceTime<T> implements Operator<T, T> {
  constructor(private conditionFunc: () => boolean, private dueTime: number, private scheduler: SchedulerLike) {
  }

  call(subscriber: Subscriber<T>, source: any): TeardownLogic {
    return source.subscribe(new ConditionalDebounceTimeSubscriber(subscriber, this.conditionFunc, this.dueTime, this.scheduler));
  }
}

export function conditionalDebounceTime<T>(conditionFunc: () => boolean, dueTime: number, scheduler: SchedulerLike = async): MonoTypeOperatorFunction<T> {
  return (source: Observable<T>) => source.lift(new ConditionalDebounceTime(conditionFunc, dueTime, scheduler));
}

function dispatchNext(subscriber: ConditionalDebounceTimeSubscriber<any>) {
  subscriber.debouncedNext();
}
like image 38
3 revs Avatar answered Oct 21 '22 07:10

3 revs