Angular Component Providers — an alternative for event bubbling
This post is sort of a response to an article I read this morning on Angular University discussing Smart Components vs Presentational Components. It’s a great article as an intro to “smart” vs “presentation” (or “dumb”) components and a part of a great series I recommend reading.
The article above discusses possible workarounds for “bubbling” events in deeply nested “presentation” components back up to a “smart” component. It proposes two solutions:
- Make a lower-level component “smart”; or
- Wrap the presentation component in a custom “smart” component.
Both of these are perfectly reasonable workarounds — but I’d like to propose a third: component-level providers.
This hinges on a particular phrasing used in the above article relating to smart components:
This (smart) type of component is inherently tied to the application itself, so as we can see it receives in the constructor some dependencies that are application-specific, like the
LessonsService
.
Note: dependencies that are application-specific. Not that a presentation component can’t have dependencies — just that it can’t have application specific dependencies.
The problem quickly
Note: The example above is a contrived example of what we’re talking about. For the time being, let’s ignore that this app could probably be done in many different ways and hitting problems like this might be a hint at poor application architecture.
Imagine our app has an API service injected into it. Our “Smart Component” wants to know when that delete button at the bottom of the tree (“Button Component” above) is clicked so we can tell the API to delete an item. What we definitely don’t want to do here is wire up @Output()
events the whole way up the chain just so we can listen for button clicks at the bottom.
Component Level Providers
Before we talk about the solution, it’s worth having a TL;DR rundown of the way Angular’s injector bubbling works. In short: to find a dependency for something, Angular first looks at that element’s Injector. If it doesn’t find it, it goes to the element’s parent injector. It continues this way “bubbling” up the tree until it hits the “root injector”.
In very simple applications, you’ll typically only provide
things at the root level — the ApplicationModule. However, besides providing at the Module level, you can also provide things at the component level. The series of articles mentioned in the introduction actually talks about this in one of the later chapters as a way of providing local service state. If you’ve ever implemented a custom FormControl component using ControlValueAccessor you’ve probably done something like this:
@Component({
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: MyComponent,
multi: true
}]
})
If you provide
a dependency at your component level — any child components who inject that dependency get the one provided by your component.
How is this useful for the problem?
Let’s think about it — our button component just needs something to pump events into. It doesn’t necessarily need to be an @Output
. Well, we can make one of those as an abstract class:
Note, this doesn’t have to be a class. You could use any old InjectionToken
to provide, for example, an EventEmitter instance to the button. I’m using a class because it hopefully makes it more clear how the DI is working.
Right, so we’ve got a dependency for our button. Note, it’s not an application specific dependency. There’s nothing about a ClientList
or a DeleteClientAction
or event an NgRx Store
. It’s a ‘dumb dependency’ if you will. The button component does not care what it does or where it comes from — just that it’s there. In fact, if you make it @Optional
it doesn’t even need to be there.
Next, we inject the handler our button component, and start sending click events to it:
Great. But now if we use this custom button element in our app we get everyone’s favourite Angular error:
So this is usually the point where you jump to your AppModule and add a service to the providers
array there, right? Except here we’re going to head into the AppComponent (or smart component of choice) instead:
So we’re now providing the CustomButtonEventHandler
on our AppComponent. And what are we providing? The AppComponent itself via the useExisting
property. Our AppComponent is implementing the CustomButtonEventHandler that the button component expects. What this means is that any custom-button
elements that are children of AppComponent
— and are not descendants of other components that provide a CustomButtonEventHandler
— will get a reference to the app component to notify when the button is pressed.
So that custom button can be 30 ‘presentation’ components down (please don’t do this) and we can still get events from it easily at our “smart” component level. Neat.
Drawbacks
Depending on how you look at things — you might see this as coupling your components together. The parent component needs to know that somewhere in its component tree there is a button that requires this CustomButtonEventHandler
. If it’s not a direct child — it may mean coupling to the implementation of any number of child components.
There’s a couple of counters to this. First, we’re assuming the parent in this case is a smart component — so having some degree of knowledge about its children and their makeup is probably acceptable. Second, the button handler need not be associated directly with the specific custom button. We could have a generic button handler token for our whole application — used by any number of buttons. Using this pattern, all the parent component is saying is “if a button gets clicked — I want to know about it” rather than “listen for clicks on any specific CustomButtonComponent
s”.
Final Thoughts
Providing at the component level is there for a reason — to be useful. Yet, I so rarely see it actually used in applications. I think it affords some interesting tricks to avoid making “man-in-the-middle” presentation components more complex than they have to be (like having Outputs just for the sake of bubbling, or being smart components just for the sake of handling events).
This isn’t to say it’s a pattern you should use extensively throughout your apps. Just another tool at your disposal the next time you need a solution to the above sort of problem.
Check the StackBlitz for a working example.
Hopefully you’ve found this information useful. Have a safe and wonderful rest of your day.