Wrapping a JavaScript Library in an Angular Directive
This is the second installment in a series about building a flexible JavaScript library that can be integrated across various frameworks.
In the previous article, we created a vanilla TypeScript/JavaScript library for detecting swipes in web browsers. While this library can be used directly within any JavaScript framework, we want to elevate it to a seamless, first-class experience for developers using their framework of choice.
Here, we'll build an Angular directive that wraps our swipe detection library.
💡 It is assumed that you're already familiar with the swipe detection library's public API. If you haven't read the first article in this series, this section will give you all the context required to follow along.
Intended Behavior
Ideally, detecting swipes on an element in your Angular template should be as simple as placing a dedicated predicate on that element:
<div ngSwipe (swipeEnd)="onSwipeEnd($event)">Swipe me!</div>
An attribute directive fits this purpose perfectly, since we won't be directly manipulating the DOM structure.
Accessing the Host Element
Let's revisit what our swipe subscription needs. Based on the underlying library's public interface, we're required to supply the following configuration:
export function createSwipeSubscription({
domElement,
onSwipeMove,
onSwipeEnd
}: SwipeSubscriptionConfig): Subscription {
// ...
}
Consequently, we need access to the element our directive is attached to and pass it to the `createSwipeSubscription` function. For an Angular component, this is a straightforward task:
constructor(
private elementRef: ElementRef
) {}
The nativeElement property from the injected elementRef provides a direct reference to the actual DOM element. This reference can then be passed when creating the swipe subscription:
this.swipeSubscription = createSwipeSubscription({
domElement: this.elementRef.nativeElement,
//..
});
The Complete Implementation
The remaining logic for the directive is uncomplicated. Here is the full implementation:
import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { createSwipeSubscription, SwipeEvent } from 'ag-swipe-core';
@Directive({
selector: '[ngSwipe]'
})
export class SwipeDirective implements OnInit, OnDestroy {
private swipeSubscription: Subscription | undefined;
@Output() swipeMove: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
@Output() swipeEnd: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
constructor(
private elementRef: ElementRef,
private zone: NgZone
) {}
ngOnInit() {
this.zone.runOutsideAngular(() => {
this.swipeSubscription = createSwipeSubscription({
domElement: this.elementRef.nativeElement,
onSwipeMove: (swipeMoveEvent: SwipeEvent) => this.swipeMove.emit(swipeMoveEvent),
onSwipeEnd: (swipeEndEvent: SwipeEvent) => this.swipeEnd.emit(swipeEndEvent)
});
});
}
ngOnDestroy() {
this.swipeSubscription?.unsubscribe?.();
}
}
The directive's operation is quite simple:
- It acquires the reference to the underlying DOM element.
- It sets up a swipe subscription with `onSwipeMove` and `onSwipeEnd` callbacks that trigger the directive's `Output` properties when the appropriate events occur.
- It cleans up the subscription when the `ngOnDestroy` lifecycle hook is invoked (i.e., the host component is being destroyed).
We also need to expose this directive through an Angular module that the consuming application can import:
@NgModule({
imports: [CommonModule],
declarations: [SwipeDirective],
exports: [SwipeDirective]
})
export class SwipeModule {}
Of course, this is no longer the only approach. We're simply not bold enough to adopt a cutting-edge feature like standalone directives in a public library just yet.
Noteworthy Considerations
Using zone.runOutsideAngular()
You might have noticed one additional provider being injected in our code:
private zone: NgZone
This is later used to wrap the swipe subscription within zone.runOutsideAngular. This is a widely used pattern to prevent unnecessary change detection cycles triggered by every tracked asynchronous event in the DOM. In our scenario, we specifically want to avoid excessive change detection runs caused by each individual mousemove event.
Handling Both Events
The public interface of the ag-swipe-core library that powers our implementation allows you to provide just one, or both, event handlers: onSwipeMove and onSwipeEnd. In our Angular wrapper, we simplify the API by avoiding extra input flags and always subscribing to both events. It becomes the consumer's responsibility to listen to the specific Output they are interested in.
This is a deliberate design choice, favoring a simpler directive contract over a potential performance optimization. I believe that simplicity should be prioritized over premature optimization when it's reasonable, though this is, of course, a matter for debate.
Summary
You can access the complete library source code on GitHub at this link.
The corresponding npm package can be found at this link.
And there you have it! We've successfully created a small, 30-line Angular directive to wrap our swipe detection library. A spoiler for the next part: the React version will be even more concise. 😄 But that's a story for another article.
