13

In angular, how do I detect if a certain element is in view?

For example, I have the following:

<div class="test">Test</div>

Is there a way to detect when this div is in view?

Thanks.

Steve Kim
  • 5,293
  • 16
  • 54
  • 99
  • Possible duplicate of [How to tell if a DOM element is visible in the current viewport?](https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport) – ConnorsFan Mar 11 '18 at 01:16
  • Take a look at [this answer](https://stackoverflow.com/a/7557433/1009922). – ConnorsFan Mar 11 '18 at 01:17
  • 1
    Inject *$element* in your constructor like this: `@Inject('$element') private readonly $element,`. then: `var isVisible = this.$element.find('div').is(':visible') ? true : false;` – Joel Oct 12 '18 at 13:54
  • 2
    @ConnorsFan I think his asking about the 'angular' way of doing so, probably an angular directive without having to use custom javascript code. For that extent, I think this https://stackoverflow.com/a/52643937/1833622 answers the question. – mdarefull Nov 13 '18 at 23:16

4 Answers4

16

Based off this answer, adapted to Angular:

Template:

<div #testDiv class="test">Test</div>

Component:

  @ViewChild('testDiv', {static: false}) private testDiv: ElementRef<HTMLDivElement>;
  isTestDivScrolledIntoView: boolean;

  @HostListener('window:scroll', ['$event'])
  isScrolledIntoView(){
    if (this.testDiv){
      const rect = this.testDiv.nativeElement.getBoundingClientRect();
      const topShown = rect.top >= 0;
      const bottomShown = rect.bottom <= window.innerHeight;
      this.isTestDivScrolledIntoView = topShown && bottomShown;
    }
  }

Example with scroll event binding

Another nice feature is to determine how much of that <div> is to be considered as "within view". Here's a reference to such implementation.

noamyg
  • 2,747
  • 1
  • 23
  • 44
8

Here is a directive that you can use. It uses the shiny IntersectionObserver API

The directive

import {AfterViewInit, Directive, TemplateRef, ViewContainerRef} from '@angular/core'

@Directive({
  selector: '[isVisible]',
})

/**
 * IS VISIBLE DIRECTIVE
 * --------------------
 * Mounts a component whenever it is visible to the user
 * Usage: <div *isVisible>I'm on screen!</div>
 */
export class IsVisible implements AfterViewInit {

  constructor(private vcRef: ViewContainerRef, private tplRef: TemplateRef<any>) {
  }

  ngAfterViewInit() {
    const observedElement = this.vcRef.element.nativeElement.parentElement

    const observer = new IntersectionObserver(([entry]) => {
      this.renderContents(entry.isIntersecting)
    })
    observer.observe(observedElement)
  }

  renderContents(isIntersecting: boolean) {

    this.vcRef.clear()

    if (isIntersecting) {
      this.vcRef.createEmbeddedView(this.tplRef)
    }
  }
}

Usage

<div *isVisible>I'm on screen!</div>
GuCier
  • 6,919
  • 1
  • 29
  • 36
  • const observedElement = this.vcRef.element.nativeElement.parentElement does this mean it mounts whenever the parent is visible? so if our parent is the entire page, it will always be considered visible, in that case, i put a div container around the component to make it more localised. – Rusty Rob Aug 16 '22 at 01:53
  • 1
    You should handle ngOnDestroy here to cleanup when you are done.. – Peter Nov 02 '22 at 08:35
3

I had the same task in one of the latest projects I've worked on and ended up using a npm package, which provides a working directive out of the box. So if someone doesn't feel like writing and testing directives, like me, check out this npm package ng-in-view. It works like a charm in Angular 14.
I hope that this post will help someone to save some time writing directives.

1

I ended up creating a slightly vanilla directive (though it uses the @HostListener decorator and stuff). If the height of the directive-bound element is fully within the viewport, it emits a boolean value ($event) of true, otherwise it returns false. It does this through JS getBoundingClientRect() method; where it can then take the rect element's position and do some quick/simple math with the viewport's height against the top/bottom position of the element.

inside-viewport.directive.ts:

import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';

@Directive({
  selector: '[insideViewport]'
})
export class InsideViewportDirective {
  @Output() insideViewport = new EventEmitter();
  constructor(
    private elementRef: ElementRef;
  ) { }

  @HostListener('body:scroll', ['$event'])
  public onScrollBy(): any {
    const windowHeight = window.innerHeight;
    const boundedRect = this.elementRef.nativeElement.getBoundingClientRect();

    if (boundedRect.top >= 0 && boundedRect.bottom <= windowHeight) {
      this.insideViewport.emit(true);
    } else {
      this.insideViewport.emit(false);
    }
  }
}

Then, in the template app.component.html, I added an additional second argument (string), to follow the first $event arg (which emits the boolean value) so that the component could identify which element was scrolled into view, given that you can apply this directive to multiple elements:

<div (insideViewport)="onElementView($event, 'blockOne')">
  <span *ngIf="showsText"> I'm in view! </span>
</div>

And in the component (aka the "do stuff" part) app.component.ts, now we can receive the boolean value to drive the behavior of the conditional component property; while also qualifying the element as the first block (aka blockOne) in a view that might have multiple "blocks":

// ... other code
export class AppComponent implements OnInit {

  showsText!: boolean;

  //... other code

  onElementView(value: any, targetString: string): void {
    if (value === true && targetString === 'blockOne') {
      this.showsText = true;
    } else { this.showsText = false; }
  }
}

Hope this helps as I've been trying to figure the most vanilla-y way to do this (with core angular stuff) for a while lol -- but I am not nearly versed enough on best-case APIs sadface

Ria Pacheco
  • 151
  • 1
  • 3