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
@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:
@ViewChildrenlet us query into the
Viewof a Directive;
@ContentChildrenlet us query into a Directive’s content;
@HostBindingwill bind a class property to it’s Host property;
@HostListenerwill 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:
- Support the same API as
- 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
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:
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
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 is the type of the resulting args and
R is the type of
T when piped through the provided operator. Defaulting
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!
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
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.