Stop Repeating Yourself in Angular: How to Create Abstract Components
I have been working on Angular (not AngularJS, not anymore) since it was at developer preview and have learnt a bit about best practices for that framework throughout all those years. It is quite a long list, arguably too long to cover in a single blog post. Thus, for my first article on Medium, I picked a single best practice that I find easiest to take on and most rewarding when applied: Extending Angular components from —kind of — abstract classes. This could also be one of the most underused/rated features of Angular, considering it is an object-oriented framework.
Let us build a sample component which will implement ControlValueAccessor and see what we have to deal with each time we want to have a value on a component to be accessible via FormControlName, FormControlDirective or NgModel directives.
Wow, that is plenty! Yet, it seems inevitable to add this much code whenever two-way binding should be enabled on a component. Imagine a big project with lots of these components. How much code repetition would it have? Would the readability of your component class not drop off?
Well, this can be solved through a fundamental OOP principle: Inheritance. If we build a base component with common properties and methods useful to all extending components, that would decrease the code repetition and mass to a minimum. This is available in Angular, but with a twist.
[UPDATE on May 20, 2019] If you have noticed, we used the Component
decorator instead of an abstract class. This is my preferred way to define a base component because of how dependency injection system works. When you use an abstract class and inject anything to it, you need to call super
and inject them in your the constructor of your subclasses as well. While it is true that Injector helps you with this, you can avoid that constructor
and the super
call when you use the decorator instead and declare the component in a module. This is particularly useful when working with abstract components which are used repeatedly in your project, just like the one we have in this example.
You probably have noticed that our abstract component has a generic type: <T = any>
. This is particularly useful when you have a variety of models applicable and would like to get the proper interface when referring to the inherited value. Generics can also have a default type, any
in this case, to prevent pointless errors when defining one is redundant.
Above is a basic implementation of what I have described. In addition to all mandatory methods required by ControlValueAccessor
interface, the abstract component contains some input properties along with a ChangeDetectorRef
instance injected to it. If you are a ChangeDetectionStrategy.OnPush
junkie like I am, you presumably have appreciated why I chose to do so. Now, we have some foundation to build upon.
Aha! Apparently, we can keep extending to the degree which we see fit for our holy cause, i.e. avoiding code repetition. We have added some attributes and events shared by all text inputs, thus we will not have to add them again on our input components. In case an irregular component needs a differentiated property, getter, setter, or method other than the inherited one, we can always overwrite it. The only thing we have to keep in mind is the type system. Next, we will observe the results using this abstract on two separate components.
The benefits are obvious. With a little bit of code, we have created two fully functional components which can be used in any form, template-driven or reactive. Better yet, a third one will take only a fragment of time it normally would. Revisions will be a breeze and, provided that we keep using same abstracts over and over, our bundle size will decrease too.
Here is the link for a sample app on what is discussed and depicted so far: https://stackblitz.com/edit/angular-extend.
Conclusion
By its own nature, the abstracts implemented in this article are incomplete. At Etiya, where I work, we have much more robust classes with mappers, default value getters, secondary generic types, conditional value limiters, predefined lifecycle-hooks, and more. We owe that to OOP features available in Angular and an unrelenting search for better code.
I cannot think of any reason not to adopt such a productive practice immediately. In fact, we have not stumbled on any drawbacks so far and are enjoying it every day.
Happy new year everyone!