19

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.

4
  • Possible duplicate of How to tell if a DOM element is visible in the current viewport? Commented Mar 11, 2018 at 1:16
  • Take a look at this answer. Commented Mar 11, 2018 at 1: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; Commented Oct 12, 2018 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 stackoverflow.com/a/52643937/1833622 answers the question. Commented Nov 13, 2018 at 23:16

6 Answers 6

18

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.

Sign up to request clarification or add additional context in comments.

Comments

9

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> 

2 Comments

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.
You should handle ngOnDestroy here to cleanup when you are done..
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.

Comments

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

Comments

1

https://stackoverflow.com/a/68484378/9566462 has a bug, here is a corrected version

Use like

<div appIsOnScreen #isOnScreen="isOnScreen">123</div> {{ isOnScreen.isOnScreen$ | async }} 

Directive

import { Directive, ElementRef } from '@angular/core'; import { defer, Observable, shareReplay } from 'rxjs'; @Directive({ exportAs: 'isOnScreen', selector: '[appIsOnScreen]', standalone: true, }) export class IsOnScreenDirective { public readonly isOnScreen$: Observable<boolean>; constructor(private readonly _elementRef: ElementRef) { this.isOnScreen$ = defer(() => { return this.getIsOnScreen$(this._elementRef.nativeElement); }).pipe( shareReplay({ bufferSize: 1, refCount: true, }), ); } private getIsOnScreen$(element: Element): Observable<boolean> { return new Observable<boolean>((subscriber) => { const intersectionObserver = new IntersectionObserver( (entries) => { // Sometimes entries receive multiple entries // Last one is correct subscriber.next(entries[entries.length - 1].isIntersecting); }, { threshold: 1, }, ); intersectionObserver.observe(element); return () => { intersectionObserver.disconnect(); }; }); } } 

Comments

1

Depending on what you want to achieve, you can use Angular's Deferrable Views.

With the new control flow introduced in Angular 17, you can create a loop to iterate over a list and display a placeholder until the element is visible in the viewport.

@for (element of elements; track element.id) { @defer (on viewport) { <my-component /> } @placeholder { <div class="skeleton-loader"></div> } } 

Note that the loop isn't mandatory here, it's just an example of implementation.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.