DEV Community

Wrap your library in an Angular directive

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>
Enter fullscreen mode Exit fullscreen mode

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 {
// ...
}
Enter fullscreen mode Exit fullscreen mode

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
) {}
Enter fullscreen mode Exit fullscreen mode

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,
  //..
});
Enter fullscreen mode Exit fullscreen mode

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?.();
  }
}
Enter fullscreen mode Exit fullscreen mode

The directive does the following simple routine:

  • Gets the reference of the underlying DOM element.
  • Creates a swipe subscription with onSwipeMove and onSwipeEnd event handlers that emit to directive's Outputs 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 {}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)