PiwiTheKiwi
PiwiTheKiwi

Reputation: 129

How can I emit a value whenever a particular element comes into view, using Angular?

I created a directive that should emit an event once its element is visible in the viewport.

@Directive({
  selector: '[scrollListener]',
})
export class ScrollListenerDirective {
  @Output() scrollListener: Observable<number>;

  constructor(private el: ElementRef) {
    this.scrollListener = fromEvent(document, 'scroll').pipe(
      map(() => this.el.nativeElement.getBoundingClientRect()),
      map(({ top }) => Math.abs(top)),
      filter((top) => top <= 100)
    );
  }
}

The problem is that this event will emit whenever the top is less than 100px. That results in way too many events. How do I change this event so it only emit once the element is in view, stop emitting when it isn't, and then once the next time when it is visible.

Upvotes: 1

Views: 134

Answers (2)

Eddy Lin
Eddy Lin

Reputation: 683

use distinctUntilChanged

this.scrollListener = fromEvent(document, 'scroll').pipe(
  map(() => this.el.nativeElement.getBoundingClientRect()),
  map(({ top }) => Math.abs(top)),
  distinctUntilChanged((p, c) => c <= 100 === p <= 100),
  filter((top) => top <= 100)
);

Upvotes: 1

kellermat
kellermat

Reputation: 4495

You could use the scan operator to cache the preceding value of top. By comparing the preceding value with the current value of top you can determine when the transition from > 100 to <= 100 happens. At the time of this transition, the observable emits a value.

@Directive({
    selector: '[scrollListener]',
  })
  export class ScrollListenerDirective {
    @Output() scrollListener: Observable<number>;
  
    constructor(private el: ElementRef) {
      this.scrollListener = fromEvent(document, 'scroll').pipe(
        map(() => this.el.nativeElement.getBoundingClientRect()),
        map(({ top }) => Math.abs(top)),
        scan((acc: number[], curr: number) =>  [acc[1], curr], [0, 0]),
        filter(valuesOfTop => valuesOfTop[0] > 100 && valuesOfTop[1] <= 100),
        map(valuesOfTop => valuesOfTop[1]),
      );
    }
}

Upvotes: 3

Related Questions