Reputation: 5591
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.
Upvotes: 18
Views: 38544
Reputation: 1340
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.
Upvotes: 1
Reputation: 342
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();
};
});
}
}
Upvotes: 1
Reputation: 111
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.
Upvotes: 3
Reputation: 161
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
Upvotes: 1
Reputation: 3094
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.
Upvotes: 18
Reputation: 7405
Here is a directive that you can use. It uses the shiny IntersectionObserver API
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)
}
}
}
<div *isVisible>I'm on screen!</div>
Upvotes: 9