Angular LAB: lets create a Visibility Directive
In this article I'm going to illustrate how to create a very simple Angular Directive that keeps track of an element's visibility state, or in other words, when it goes in and out of the viewport. I hope this will be a nice and perhaps useful exercise!
In order to do this, we're going to use the IntersectionObserver JavaScript API which is available in modern browsers.
What we want to achieve
We want to use the Directive like this:
<p visibility [visibilityMonitor]="true" (visibilityChange)="onVisibilityChange($event)" > I'm being observed! Can you see me yet? </p>
- visibility is the selector of our custom directive
- visibilityMonitor is an optional input which specifies whether or not to keep observing the element (if false, stop monitoring when it enters the viewport)
- visibilityChange will notifies us
The output will be of this shape:
type VisibilityChange = | { isVisible: true; target: HTMLElement; } | { isVisible: false; target: HTMLElement | undefined; };
Having an undefined target will mean that the element has been removed from the DOM (for example, by an @if).
Creation of the Directive
Our directive will simply monitor an element, it will not change the DOM structure: it will be an Attribute Directive.
@Directive({ selector: "[visibility]", standalone: true }) export class VisibilityDirective implements OnInit, OnChanges, AfterViewInit, OnDestroy { private element = inject(ElementRef); /** * Emits after the view is initialized. */ private afterViewInit$ = new Subject<void>(); /** * The IntersectionObserver for this element. */ private observer: IntersectionObserver | undefined; /** * Last known visibility for this element. * Initially, we don't know. */ private isVisible: boolean = undefined; /** * If false, once the element becomes visible there will be one emission and then nothing. * If true, the directive continuously listens to the element and emits whenever it becomes visible or not visible. */ visibilityMonitor = input(false); /** * Notifies the listener when the element has become visible. * If "visibilityMonitor" is true, it continuously notifies the listener when the element goes in/out of view. */ visibilityChange = output<VisibilityChange>(); }
In the code above you see:
- the input and output we talked about earlier
- a property called afterViewInit$ (an Observable) which will act as a reactive counterpart to the ngAfterViewInit lifecycle hook
- a property called observer which will store the IntersectionObserver in charge of monitoring our element
- a property called isVisibile which will store the last visibility state, in order to avoid re-emitting the same state twice in a row
And naturally, we inject the ElementRef in order to grab the DOM element on which we apply our directive.
Before writing the main method, let's take care of the lifecycle of the directive.
ngOnInit(): void { this.reconnectObserver(); } ngOnChanges(): void { this.reconnectObserver(); } ngAfterViewInit(): void { this.afterViewInit$.next(); } ngOnDestroy(): void { // Disconnect and if visibilityMonitor is true, notify the listener this.disconnectObserver(); if (this.visibilityMonitor) { this.visibilityChange.emit({ isVisible: false, target: undefined }); } } private reconnectObserver(): void {} private disconnectObserver(): void {}
Now here's what happens:
- Inside both ngOnInit and ngOnChanges we restart the observer. This is in order to make the directive reactive: if the input changes, the directive will start behaving differently. Notice that, even if ngOnChanges also runs before ngOnInit, we still need ngOnInit because ngOnChanges doesn't run if there are no inputs in the template!
- When the view is initialized we trigger the Subject, we'll get to this in a few seconds
- We disconnect our observer when the directive is destroyed in order to avoid memory leaks. Lastly, if the developer asked for it, we notify that the element has been removed from the DOM by emitting an undefined element.
IntersectionObserver
This is the heart of our directive. Our reconnectObserver method will be the one to start observing! It'll be something like this:
private reconnectObserver(): void { // Disconnect an existing observer this.disconnectObserver(); // Sets up a new observer this.observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { const { isIntersecting: isVisible, target } = entry; const hasChangedVisibility = isVisible !== this.isVisible; const shouldEmit = isVisible || (!isVisible && this.visibilityMonitor); if (hasChangedVisibility && shouldEmit) { this.visibilityChange.emit({ isVisible, target: target as HTMLElement }); this.isVisible = isVisible; } // If visilibilyMonitor is false, once the element is visible we stop. if (isVisible && !this.visibilityMonitor) { observer.disconnect(); } }); }); // Start observing once the view is initialized this.afterViewInit$.subscribe(() => { this.observer?.observe(this.element.nativeElement); }); }
Trust me, it's not as complicated as it seems! Here's the mechanism:
- First we disconnect the observer if it was already running
- We create an IntersectionObserver and define its behavior. The entries will contain the monitored elements, so it will contain our element. The property isIntersecting will indicate if the element's visibility has changed: we compare it to the previous state (our property) and if it's due, we emit. Then we store the new state in our property for later.
- If visibilityMonitor is false, as soon as the element becomes visible we disconnect the observer: its job is done!
- Then we have to start the observer by passing our element, so we wait for our view to be initialized in order to do that.
Lastly, let's implement the method which disconnects the observer, easy peasy:
private disconnectObserver(): void { if (this.observer) { this.observer.disconnect(); this.observer = undefined; } }
Final code
Here's the full directive. This was just an exercise, so be free to change it to whatever you like!
type VisibilityChange = | { isVisible: true; target: HTMLElement; } | { isVisible: false; target: HTMLElement | undefined; }; @Directive({ selector: "[visibility]", standalone: true }) export class VisibilityDirective implements OnChanges, OnInit, AfterViewInit, OnDestroy { private element = inject(ElementRef); /** * Emits after the view is initialized. */ private afterViewInit$ = new Subject(); /** * The IntersectionObserver for this element. */ private observer: IntersectionObserver | undefined; /** * Last known visibility for this element. * Initially, we don't know. */ private isVisible: boolean = undefined; /** * If false, once the element becomes visible there will be one emission and then nothing. * If true, the directive continuously listens to the element and emits whenever it becomes visible or not visible. */ visibilityMonitor = input(false); /** * Notifies the listener when the element has become visible. * If "visibilityMonitor" is true, it continuously notifies the listener when the element goes in/out of view. */ visibilityChange = output (); ngOnInit(): void { this.reconnectObserver(); } ngOnChanges(): void { this.reconnectObserver(); } ngAfterViewInit(): void { this.afterViewInit$.next(true); } ngOnDestroy(): void { // Disconnect and if visibilityMonitor is true, notify the listener this.disconnectObserver(); if (this.visibilityMonitor) { this.visibilityChange.emit({ isVisible: false, target: undefined }); } } private reconnectObserver(): void { // Disconnect an existing observer this.disconnectObserver(); // Sets up a new observer this.observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { const { isIntersecting: isVisible, target } = entry; const hasChangedVisibility = isVisible !== this.isVisible; const shouldEmit = isVisible || (!isVisible && this.visibilityMonitor); if (hasChangedVisibility && shouldEmit) { this.visibilityChange.emit({ isVisible, target: target as HTMLElement }); this.isVisible = isVisible; } // If visilibilyMonitor is false, once the element is visible we stop. if (isVisible && !this.visibilityMonitor) { observer.disconnect(); } }); }); // Start observing once the view is initialized this.afterViewInit$.subscribe(() => { this.observer?.observe(this.element.nativeElement); }); } private disconnectObserver(): void { if (this.observer) { this.observer.disconnect(); this.observer = undefined; } } }
The above is the detailed content of Angular LAB: lets create a Visibility Directive. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics











JavaScript is the cornerstone of modern web development, and its main functions include event-driven programming, dynamic content generation and asynchronous programming. 1) Event-driven programming allows web pages to change dynamically according to user operations. 2) Dynamic content generation allows page content to be adjusted according to conditions. 3) Asynchronous programming ensures that the user interface is not blocked. JavaScript is widely used in web interaction, single-page application and server-side development, greatly improving the flexibility of user experience and cross-platform development.

The latest trends in JavaScript include the rise of TypeScript, the popularity of modern frameworks and libraries, and the application of WebAssembly. Future prospects cover more powerful type systems, the development of server-side JavaScript, the expansion of artificial intelligence and machine learning, and the potential of IoT and edge computing.

Different JavaScript engines have different effects when parsing and executing JavaScript code, because the implementation principles and optimization strategies of each engine differ. 1. Lexical analysis: convert source code into lexical unit. 2. Grammar analysis: Generate an abstract syntax tree. 3. Optimization and compilation: Generate machine code through the JIT compiler. 4. Execute: Run the machine code. V8 engine optimizes through instant compilation and hidden class, SpiderMonkey uses a type inference system, resulting in different performance performance on the same code.

Python is more suitable for beginners, with a smooth learning curve and concise syntax; JavaScript is suitable for front-end development, with a steep learning curve and flexible syntax. 1. Python syntax is intuitive and suitable for data science and back-end development. 2. JavaScript is flexible and widely used in front-end and server-side programming.

JavaScript is the core language of modern web development and is widely used for its diversity and flexibility. 1) Front-end development: build dynamic web pages and single-page applications through DOM operations and modern frameworks (such as React, Vue.js, Angular). 2) Server-side development: Node.js uses a non-blocking I/O model to handle high concurrency and real-time applications. 3) Mobile and desktop application development: cross-platform development is realized through ReactNative and Electron to improve development efficiency.

This article demonstrates frontend integration with a backend secured by Permit, building a functional EdTech SaaS application using Next.js. The frontend fetches user permissions to control UI visibility and ensures API requests adhere to role-base

I built a functional multi-tenant SaaS application (an EdTech app) with your everyday tech tool and you can do the same. First, what’s a multi-tenant SaaS application? Multi-tenant SaaS applications let you serve multiple customers from a sing

The shift from C/C to JavaScript requires adapting to dynamic typing, garbage collection and asynchronous programming. 1) C/C is a statically typed language that requires manual memory management, while JavaScript is dynamically typed and garbage collection is automatically processed. 2) C/C needs to be compiled into machine code, while JavaScript is an interpreted language. 3) JavaScript introduces concepts such as closures, prototype chains and Promise, which enhances flexibility and asynchronous programming capabilities.
