Wrap your library in an Angular directive
Alexander Goncharuk
Posted on June 29, 2022
This is the second article in the series on designing a flexible JS library to be used in multiple frameworks.
In the first article in the series, we have built a vanilla TS/JS library for swipe detection in the browser. Although it can be used in your application built with any popular JS framework as is, we want to go a little further and make our library a first-class citizen when used in the framework of your choice.
In this article, we are going to wrap our swipe detection library in an Angular directive.
💡 The article implies you are familiar with the public interface of the swipe detection library used under the hood. If you haven't read the first article in the series, this section alone will be enough to follow along with the material of this one.
How should it work
When we need to detect swipes on an element in our Angular component, doing this should be as easy as attaching a dedicated attribute directive to the target element:
<div ngSwipe (swipeEnd)="onSwipeEnd($event)">Swipe me!</div>
An attribute directive will be just enough here as we are not going to manipulate the DOM.
Getting access to the host element
Let's recall what our swipe subscription expects. According to the public interface of the underlying library, we should provide the following configuration:
export function createSwipeSubscription({
domElement,
onSwipeMove,
onSwipeEnd
}: SwipeSubscriptionConfig): Subscription {
// ...
}
So we need to get access to the host element our directive is attached to and pass one to the createSwipeSubscription
function. This is a no-brainer type of task for our Angular component:
constructor(
private elementRef: ElementRef
) {}
The nativeElement
property of the injected elementRef
holds the reference to the underlying native DOM element. So when creating a swipe subscription, we can use this reference to pass the target DOM element:
this.swipeSubscription = createSwipeSubscription({
domElement: this.elementRef.nativeElement,
//..
});
Complete solution
The rest of the directive code is pretty straightforward. Here is the complete solution:
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 does the following simple routine:
- Gets the reference of the underlying DOM element.
- Creates a swipe subscription with
onSwipeMove
andonSwipeEnd
event handlers that emit to directive'sOutput
s whenever a relevant event occurs. - Unsubscribes when the
ngOnDestroy
hook is called (host component is being destroyed).
We also need to ship our directive in an Angular module that the consuming application will be importing:
@NgModule({
imports: [CommonModule],
declarations: [SwipeDirective],
exports: [SwipeDirective]
})
export class SwipeModule {}
By the way, this is no longer the only option. We are just not hipster enough to use a cutting edge feature like standalone directives in a public library yet.
A couple of things worth mentioning
zone.runOutsideAngular()
You may have noticed we have one more provider injected:
private zone: NgZone
And later used to wrap the swipe subscription in zone.runOutsideAngular
. This is a common practice of avoiding unnecessary change detection triggers on every tracked asynchronous event happening in the DOM. In our case, we don't want the change detection to be triggered excessively on every mousemove
event.
Subscribing to both swipeMove and swipeEnd
The public interface of the ag-swipe-core
library we used under the hood allows providing only one of two event handlers: onSwipeMove
and onSwipeEnd
. In the Angular wrapper though, we avoid additional input parameters and always handle both events and leave it up to the directive consumer to only listen to the Output
it is interested in.
In this case, it is a conscious choice to prefer a simpler directive contract to possible performance optimization. I believe that simplicity should prevail over the optional optimization when it makes sense, but it is a subject for discussion of course.
Wrapping up
You can find the complete library code on GitHub by this link.
And the npm
package by this link.
That was it! We have built a simple Angular directive wrapper for our swipe detection library in 30 lines of code. Spoiler alert: the React version will be shorter. 😄 But that's a whole different story for the next article.
Posted on June 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.