How to Handle Click Events Not Bubbling Up to Document

Replacing event listeners on document with custom event streams in Angular & RxJS for greater control

Michael Jacobson
Level Up Coding

--

Dolphin blowing bubble underwater
Photo by MattiaATH on Shutterstock

The Initial, Happy Scenario

An interesting scenario I recently encountered was a list of items, each of which contained a dropdown, like so:

Animated gif showing list items with labels and dropdowns

Each list item is an instance of a component called ListItemComponent.

The dropdown is a PrimeNG Dropdown, and, like all good-hearted component developers, PrimeNG makes sure that clicking outside the dropdown menu triggers it to close.

They do this via the common practice of attaching a click event listener on document that closes the menu upon click outside of the menu.

That was all well and good until the scenario got a slight change.

The Troublemaker Scenario

Our happy scenario turned into a bit of a troublemaker when the list items were extended to allow for an expandable/collapsible detail row, like so:

Animated gif showing expandable/collapsible detail rows

Why troublemaker, you ask? Great question! Here’s why:

Animated gif showing list item detail rows expanding/collapsing when clicking on dropdown

Clicks on the dropdowns bubble up to the list item summary rows and trigger the expand/collapse of the detail rows.

Very bad user experience. Very, very, very bad.

Animated gif of Seinfeld’s Babu wagging his finger in a scolding manner

The Search for a Fix

The most obvious solution is to stopPropagation of the dropdown clicks to prevent the bubble-up.

Here’s what that gives us:

Animated gif showing multiple dropdowns open at the same time due to stopPropagation

This fixes the unwanted expand/collapse of the detail rows, but introduces a new problem: multiple dropdowns can now be open at the same time!

Animated gif of a panda in a rocking chair acting frustrated and exhausted

When we stop propagation of clicks on the dropdowns, the click listeners on document never receive the click events and therefore don’t close their associated dropdowns.

A Different Approach

To fix this, we need to build a more reliable event notification system that can’t be thwarted by an innocently inserted stopPropagation.

Since we need to communicate between components that might not be part of the same parent-child tree structure, we need to look to an Angular service.

Angular Service & RxJS to the Rescue!

We can easily build a service to act as the communications center for all events related to this interface.

Click events on the dropdowns can be reported to the service, and the service, via Observables, can notify subscribers of those click events.

A simple version of the service would look like this:

Points of interest:

  • The service exposes an Observable called events$ that emits an EventNotification object whenever the service’s notify method is called.
  • We’re using a Subject internally within the service (_events$) to emit the events but we’re not exposing it directly, we’re keeping it private and exposing its Observable form because, in general, subscribers should not have access to the raw Subject unless they’ll also be emitting events from it, and that’s not the case here.
  • We created an enum called EventType that currently has just one member event, ListItemDropdownOpened, but will help keep things organized as we add other events.
  • We created a type called EventNotification that has a type property for the EventType and an optional details property.
  • The optional details property is of type EventDetails, which currently has one optional property, componentInstance, that will indicate which ListItemComponent instance was involved in the event. Subscribers will need this information so they know if they’re receiving a notification about themselves or about a different ListItemComponent instance.

Subscribing to the Events

Here’s how our ListItemComponent can subscribe to our new service’s events$ Observable and react accordingly:

Points of interest:

  • We created an Observable called otherDropdownOpened$ that filters the service’s event stream for events of type ListItemDropdownOpened that are associated with instances of ListItemComponent other than this one.
  • Upon receiving one of those filtered events, we close our dropdown by calling this.dropdown.hide(). (This happens to be PrimeNG’s API for programmatically closing its dropdown, but others could be different, of course.)

Triggering the Events

In the previous ListItemComponent code sample, you probably noticed that I left in a spoiler giving away this part of the article: a method called onShowDropdown that calls the EventNotificationService's notify method.

Here it is:

The notify method accepts an EventNotification object. On that object we set the type to ListItemDropdownOpened (using the EventType enum) and in the details we set the componentInstance to this, a reference to our instance of ListItemComponent.

The onShowDropdown method is invoked when the component’s dropdown is opened. In our scenario, it’s a PrimeNG dropdown, so our component’s template looks like this, hooking into the dropdown’s onShow event:

But the basic idea is that your component would have some way of knowing when its dropdown has been opened so it can then notify the service.

Extending the Service

As you extend the service with more event types, you could enhance it by, for example, creating useful pre-defined event bundles, like this:

The allComponentOpenedEvents$ Observable would be useful for certain components, like dropdowns, to subscribe to if they need to react to any type of component opening itself.

Then if you someday add a new component that also “opens,” you could simply add its EventType to that filter rather than having to find and update every subscriber listening for component “open” events.

Conclusion

Here’s a StackBlitz with the working code:

I hope this provided some useful information and ideas.

Go forth and stream!

Dog in stream in woods
Photo by Robert Bodnar T on Shutterstock

--

--

Frontend Developer working with Angular for 10+ years. I love solving problems and building cool stuff. I sweat the details because…I love the details.