// Import the core angular services. import { Directive } from "@angular/core"; import { ElementRef } from "@angular/core"; import { NgZone } from "@angular/core"; // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // @Directive({ selector: "[stickyClass]", inputs: [ "stickyClass" ] }) export class StickyClassDirective { public stickyClass!: string; private elementRef: ElementRef; private observer: IntersectionObserver | null; private zone: NgZone; // I initialize the sticky class directive. constructor( elementRef: ElementRef, zone: NgZone ) { this.elementRef = elementRef; this.zone = zone; this.observer = null; } // --- // PUBLIC METHODS. // --- // I get called once when the host element is being destroyed. public ngOnDestroy() : void { this.observer?.disconnect(); this.observer = null; } // I get called once after the inputs have been bound for the first time. public ngOnInit() : void { // Since the intersection events won't change any view-model (in this demo), // there's no need to trigger any change-detection digests. As such, we can bind // the interaction observer callback outside of the Angular Zone. this.zone.runOutsideAngular( () => { // By using threshold values of both 0 and 1, we will observe a change // when even 1px of the host element passes into the viewport as well as // when the entire element moves out of the viewport. this.observer = new IntersectionObserver( this.handleIntersection, { threshold: [ 0, 1 ] } ); this.observer.observe( this.elementRef.nativeElement ); } ); } // --- // PRIVATE METHODS. // --- // I handle changes in the observed intersections of the targets. private handleIntersection = ( entries: IntersectionObserverEntry[] ) => { for ( var entry of entries ) { // CAUTION: Since we know that the TOP specified in the "sticky" // configuration of our CSS class has a "-1px" value, then when the element's // rendering behavior switches from "static" to "sticky", the bounding client // rectangle will place the TOP of the element at ABOVE THE VIEWPORT (at // about -1px). As such, any time the top of the bounding client rectangle is // less than zero (while the BOTTOM is still visible), we can consider the // element to be "sticky" / "stuck". if ( ( entry.boundingClientRect.top < 0 ) && ( entry.boundingClientRect.bottom > 0 ) ) { this.elementRef.nativeElement.classList.add( this.stickyClass ); } else { this.elementRef.nativeElement.classList.remove( this.stickyClass ); } } }; }