To reuse where it is possible is a natural instinct in all aspects of life. The idea that something can serve other purposes in contrast to consuming more is beautiful and sustainable. In reality, how easy an item can be reused is determined by how tightly it is coupled to a specific purpose. The same applies to software development, but it takes some know-how to efficiently create reusable components that will be flexible enough for your use cases while being worth the effort of making it reusable.
When having multiple teams working together it makes sense to reuse components between projects. There are two main ways to create reusable components in Angular:
- Pass inputs to the component, passing the necessary data to the component used for rendering and configuring the component. This normally involves iterating over the provided data and follow a convention for how to render the data.
- Use transclusion/content projection to pass a template to the reusable component and show the templateRefs inside the component using the ngTemplateOutlet directive or ng-content
The choice of which technique to reuse components you should use is determined by the desired flexibility. If you have a simple reusable component that doesn’t need to be very flexible, simply using inputs will do. An example of this is a simple questionnaire that should be dynamically rendered using json data from an API.
On the other hand, this becomes a pain when you need to pass lots of inputs to the component to provide the necessary data to the component. For example, you might need a fieldsDefinitions and actionDefinitions input for determining the fields and different actions in a list and inside the reusable component, you then need to have a lot of logic to render the input data. I have seen cases here where this has escalated to creating a dedicated DSL for rendering components. This gets very painful as the amount of input keeps growing, as well as the complexity of the reusable component as it should handle more edge cases, in the end making the component harder to reuse than if it were simply copied and modified to serve the purpose.
What you want to do instead is allow for an external template to plug into the component using either templateref or ng-content (check my plugin architecture post for an example of this with ng-content). In summary, use template projection when more flexibility is needed for the reusable component.
Should you use template reference or ng-content?
There is a subtle difference between using templateRef vs. using ng-content because of how Angular’s lifecycle management works. Angular’s OnInit
and onDestroy
hooks works for component where they are declared, not where they are used/rendered. That means, if using ng-content, the child will not be destroyed when destroying the component containing the ng-content. Also for a child component being instantiated with ng-content, the constructor and init hooks will also be invoked regardless of if the child component has been rendered in the DOM. For that reason, passing the template projection as templateRef is the most maintainable and performant, as the lifecycle hooks are only getting called if the templateRef have actually been rendered in the DOM and because it gets destroyed with the component instantiating the templateRef.
Creating a reusable card/list view component
To illustrate how you should keep components reusable and maintainable we are going to create a reusable card list component, which can toggle between cards and a list. This is based on my Angular todo app demo, a simple TODO management application:
The purpose of creating this feature is to learn how more complex reusable components can be created without going in the trap of needing to create a lot of input data and maintain a convention for how the reusable component should be rendered based on this data. Been there done that, wasn’t fun. What we are going to do instead is using transclusion, that is passing template references to the reusable component. This is going to cause slightly more duplication but easier use and maintenance of the reusable component. The point is: less code duplication is not beneficial it if it makes the code harder to use and maintain.
We want to create a card-list component that takes in a listRef, cardRef and data to be shown and is used like this:
Note how simple the interface of the cards-list component is because we simply utilize templateRefs and map data using let-todos="data"
which will map data to todos when we are passing data to a templateRef with ngTemplateOutletContext
.
We are then going to create the card-list component.
Open the terminal, go to shared folder and type:
ng g m cards-list
Go to cards-list and create the cards and list components:
ng g c cards
ng g c list
We should now have:
Since the only input we are working with here is template refs and data, to be shown in the template refs, we are going to have very simple presentation components. The list component looks like this:
To render this it only takes in a listRef and the data to render the list.
The cards component template is slightly different because it is iterating over each item (todo item in this case), and are rendering them using ngTemplateOutlet
and is setting the data for the ngTemplateOutlet with ngTemplateOutletContext
. It is setting the data which in our templateRef is passed to the todo data using let-todo="data"
.
Some styling is applied to these cards to make them wrap nicely:
Now we can display the card-list component and easily change the cards or list by simply changing the template ref provided.
The card and list row components are created like presentation/dumb components in the shared folder:
The card component is created with Angular Material directives:
The list row is created with Bootstrap (got to spice stuff up):
These are being used as template references in the reusable component.
The complete demo can be found here:
Conclusion
In this post, we looked at two ways of creating a reusable component. The first way only works for simple components as they will become harder to use and maintain if the complexity grows because of lots of input to be configured for the specific use case and as well as a lot of “duct tape” programming to handle all the different applications of the reusable component. The way of handling this is by using transclusion instead of using input data and conventions of how to render this. Transclusion using templateRef is preferred over ng-content performance and maintenance wise because it keeps the life cycle in sync with where it is used as well as supporting conditional instantiation.
To put all this into practice we created a card list reusable component for rendering an array of data as either a card or a list. The naive approach to creating this would be to create a lot of inputs for being able to render this by iterating over data to determine rows and actions in lists and cards. This quickly becomes tiresome because it is not scaling to more complex usages. What we do instead is we used templateRefs for a card and a list, as well as the data to display, and created this reusable component in an easily maintainable way using only three inputs. The lesson of the day is: reusable components should be easy to use as well as easy to maintain.
If you liked this post, make sure to follow me on Twitter, subscribe for weekly blog posts and give some feedback in the comment section.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.
7 thoughts on “Creating Reusable Angular Components – How To Avoid the Painful Trap Most Go In”
Thanks for the explanation!
Just as a suggestion, consider replacing instances of “gonna” with “going to”. They make the post look unprofessional.
You are right 🙂 Thank you.
Sure, I fixed that. 🙂 Thanks for pointing it out.
Pingback: The Hardcoded, Dynamic and Hybrid Approach in Angular Apps – Christian Lüdemann IT
Hi.
Trying to learn your project, but i can’t build provided in github one (possible conflict of angular versions, 8.0.1 is on my local).
In case I create different project coping your sources, I’m getting errors like
ERROR in src/app/app.module.ts(6,32): error TS2307: Cannot find module '@app/app-init.service'.
src/app/app.module.ts(7,30): error TS2307: Cannot find module '@app/app.component'.
src/app/app.module.ts(8,33): error TS2307: Cannot find module '@app/app.routes'.
src/app/app.module.ts(9,28): error TS2307: Cannot find module '@app/core/core.module'.
I really confused because of this error. How it was imported in original project but broken in mine. Do you have local npm repo with required modules? Is it possible to teach me also how to do that?
Thanks.
it seems i’ve fixed part of issues and project is runnable for me also.
Can you please check merge request please?
Hi! This was very useful! How can this approach be modified to return data to the parent while also taking data from the parent component to populate initial values?