How I created an ObservableHostListener

And why you probably shouldn’t.

Angular provides a handful of decorators for Directives. The most commonly used ones are @Input and @Output. They define the public API of a Directive or Component.

Please note that a component is essentially a Directive with a template, so I’ll only talk about Directives from now on.

To gather information about the context in which a Directive lives, we can use a few more:

  • @ViewChild / @ViewChildren let us query into the View of a Directive;
  • @ContentChild / @ContentChildren let us query into a Directive’s content;
  • @HostBinding will bind a class property to it’s Host property;
  • And @HostListener will listen for an event on a Host element.

I’d like to focus on HostListener for this article. If you’re unfamiliar with HostListener , I’d recommend reading over the documentation on attribute directives.

HostListener decorates a class method and passes an optional argument to that method. In this example, a message would be logged to the console each time the host element gets clicked.

That works just fine, but Angular has been written with a heavy dependency on RxJS. And RxJS even provides a way to get an Observable stream of DOM events out of the box: fromEvent(document.querySelector('#elem'), 'click') .

It stands to reason that there should be a decorator that turns DOM events into Observable streams. In fact, there is an issue from 2015 where a member of the Angular team proposes an ObserveChild decorator. Sadly, as of yet, this hasn’t been implemented.

Why would you want an ObservableHostListener? Well, for instance you could combine streams into a meaningful stream, or gating an event stream based on other events (drag-and-drop comes to mind: dragEvents$ = dragStart$.pipe(takeUntil(dragEnd$))).

Of course you could do something like this:

Which is perfectly fine. But’s it’s way more fun to use HostListener as a starting point to build our own decorator that turns DOM events into an Observable! Which we totally can.

Building an ObservableHostListener

I really want this decorator to do a couple of things:

  • Turn HostListener from a MethodDecorator into a PropertyDecorator;
  • Support the same API as HostListener does;
  • Support RxJS operators as a third argument;
  • Clean up the Observable when the Directive is destroyed;
  • Provide a modicum of type safety.

Eventually I really want to be able to do something like this:

Remember that decorators are nothing more than functions. And functions can be called. So we begin with creating a function that prepares HostListener and returns a PropertyDecorator.

HostListener is weakly typed, i.e. cast to any, but with some reasoning we know that it is in fact a decorator factory, so it should return a decorator.

Note that you can actually import the implementation interfaces (in this case UnaryFunction) for pipeable operators from RxJS:

Internally, HostListener adds some metadata to a decorated class that is picked up by the angular compiler when compiling a template. This is how the event is actually bound to the decorated method I believe, but I’ve not dug into the Angular core deep enough to actually fully understand what’s going on here. But we do need this binding from the template to our directive class, so we need to call HostListener.

The returned PropertyDecorator function gets called with two arguments: target, which is the class the decorated property belongs to, and the property key.

Angular will try to call a method on target whenever the specified event fires. But our target does not have such a method. We’ll need to monkey patch one on the target instead.

(This is where I found out that HostListener does not support Symbols. Monkey patching a method on a class seems a bit … dirty, but if I really need to, I’d quite like it to be a Symbol. That way I can be sure that it definitely does not exist on the decorated class, even if a key exists with exactly the same name. I don’t know if this is a bug or by design, but after some experimentation I found out that none of Angular’s decorators support decorating a Symbol property or method.)

So the first part of our returned decorator should look something like this:

Angular will call the shadowed key method with new values as events are happening, which we next() into a subject we’re assigning to the originally decorated key.

If a custom operator is defined, the internal subject is piped through that operator, otherwise we can just return that subject as an Observable.

Okay, so far so good. Our target class has the shape needed to make everything work: a super secret method Angular can call with new events, and our internal subject which is assigned to the decorated key. We’ll still need to call the preparedDecorator though to tie everything together. And we need to clean up after ourselves, so we should tap into ngOnDestroy somehow. This is quite easily done:

I noted previously I wanted some modicum of type safety. This is quite hard. The best option I found was giving ObservableHostListener two type arguments T and R, where T is the type of the resulting args and R is the type of T when piped through the provided operator. Defaulting R to never will make sure TypeScript complains when you don’t provide it, because no type returned by an operator chain can be assigned to never.

This then is our final decorator:

And here is a StackBlitz where it is working in action. Click around on the document to see the ball following your clicks!

Click the document to have the ball follow you around.

Should you actually do this?

I’m tempted to say “yes”, but the actual answer should be “no, unless you have a very good reason to”.

Tackling this problem has been a lot of fun. I learned plenty about decorators and about the way Angular implements their binding decorators. It was a great little puzzle that resulted in a pretty abstraction. But it’s pretty hard to follow and relies heavily on the internal implementation of HostListener which is not guaranteed to keep working as it does, especially when Ivy is released.

There are many ways that lead to Rome. This way makes the directive’s code a bit less cluttered, but you have to be explicit with your typings, you have to know that you can actually import pipe and UnaryFunction as seperate symbols from RxJS and the use cases are very specific.

99% of the time you’ll be best off just decorating a method, and if you really need to put those events into a stream, just next() some private subject. It’s more readable and guaranteed to keep working in new versions of Angular (unless explicitly noted).

Should that prevent you experimenting with stuff like I did in this blog? Absolutely not! I learned a lot, and I had so much fun messing about with Angular internals and TypeScript. But along the way I quickly realised it will become messy.

If you somehow do want to use this, by all means: go ahead. But please let me know! I’m curious.

Thank you!