Creating Behavioral Components in Angular
When working on an enterprise application, or with a company that deals with multiple projects, it’s necessary to develop a solid foundation; Specifically, it’s important to have strong core components which are flexible enough to adapt to any variation or requirement, and can be shared across teams.
One real-world example is an accordion component, aka expansion panel. In our application, there are many pages or sections which contain variations of an accordion, which differ both in styling and in markup.
To make an accordion component that’s flexible enough for the developers, we should provide them with a component which handles the accordion behavior, leaving the styling and markup up to them.
Let’s create an accordion component that provides those requirements. We’ll have four building blocks components. An AccordionHeader
, AccordionContent
, AccordionGroup
, and Accordion
. Here’s how developers will use it:
Creating the Components
Let’s start creating them one by one:
Creating the Accordion Header Component
The header component is responsible for two things. First, it exposes a click$
observable so the parent Accordion
component can listen to it and toggle the group it belongs to.
Next, it implements an isOpen
setter where it updates its local isOpen
state. Based on this state, it uses HostBinding
to add a specific class to the host
so the developers can style it the way they want it.
Creating the Accordion Content Component
The content component responsibility is to toggle the projection content based on the visibility status that it received from the parent Accordion
component:
Creating the Accordion Group Component
The group component is a mediator component that’s responsible for communicating between the Accordion
component and the AccordionGroupChildren
— the AccordionHeader
and the AccordionContent
.
First, we use the ContentChild
decorator to grab a reference to the AccordionHeader
and AccordionContent
components.
Then, we create a toggle()
method that’ll be called by the parent Accordion
component when the user clicks on the header. The toggle()
method toggles the visibility status both on the header and the content.
Creating the Accordion Component
It’s time to connect all the pieces in the puzzle. You might have already guessed the next phase. We need to obtain a reference to all the AccordionGroup
components that exist within the accordion.
We’re using the ContentChildren
decorator that returns a QueryList
containing a collection of AccordionGroup
components.
Now we need to listen to each header click event and toggle the corresponding group. Let’s start by implementing the basic support:
We create a clicks
collection in which each item is the header click observable. We also need to know which group it belongs to, so we’re using the map
observable to map it to the current group.
Now that we have a collection of observables, it’s as simple as utilizing the merge
observable and invoking the group’s toggle method when one of them emits.
We’re still not there. One of the requirements is to support dynamic accordion’s groups. QueryList
exposes the changes
observable where we can listen to, add, or remove of children. Let’s refactor our code to use it:
The changes
observable doesn’t emit the initial value, so we use the startWith
operator to provide it with the initial groups
value. Now we’re reactive. 😎
Utilizing the ExportAs Feature
We’re still not flexible enough. We can do more. Imagine a case where the developers should use a caret icon for the header that indicates the group’s visibility status.
Currently, they don’t have any way to change the icon based on the status. Earlier, we implemented an isOpen
property in the AccordionHeader
component that we can expose to our template using the exportAs
feature:
The exportAs
property takes the name under which the component instance is exported in a template. Put simply, we can expose a directive or a component public API to the template.
Now we can access the AccordionHeader
instance in our template:
We create a local variable named header
that provides access to the AccordionHeader
instance, and we use it to set the proper icon based on the isOpen
property.
Supporting Lazy Content
At this point, we have a working accordion, but we can do better. Now, because of the way ng-content
works, we have one weakness.
When using ng-content
, the host
doesn’t have any control over the content. This behavior can lead to unexpected side-effects, as Angular will create the content that is within the AccordionContent
without taking into consideration whether it is currently visible or not.
Let’s create a demo component and use it to see this behavior in action:
Open the console, and you’ll see three logs of IconComponent ngOnInit
even though each one of the groups is closed.
There’re times when we can benefit from this behavior. For example, we may have a form or any other local state inside the AccordionContent
component that we want to persist on toggling.
On the other hand, there are times when we won’t want this behavior. For example, we might have components or directives that perform expensive operations. Another example is side-effects, such as HTTP requests, loading assets such as icons, etc.
In such cases, the right decision is to load it lazily only if the AccordionGroup
is open. The way we truly perform the components’ instantiation in a lazy manner is by using ng-template
.
Let’s add support for this feature in AccordionContent
component. First, we need to create a structural directive whose sole purpose is to expose a reference to its TemplateRef
:
Then, we can grab it in the AccordionContent
component using the ContentChild
decorator:
Now that we have it, we can pass the content
property which contains the template
we want to create to the ngTemplateOutlet
directive.
Let’s refactor the earlier example and use it:
Open the console to see the new lazy behavior. 🦊
Testing it with Spectator
Everything works well, but we won’t release it to developers without writing some unit tests. Testing such components with Angular can be overwhelming. But it’s your lucky day — because Spectator is here to the rescue!
Spectator is a powerful tool to simplify testing in Angular. Spectator helps you get rid of all the boilerplate grunt work, leaving you with readable, sleek, and streamlined unit tests.
Spectator comes with a wide range of features, allow me to show you a taste of it:
Pretty neat, huh? Check out the docs for more information. 😀
Support for Nested Accordions
Let’s finish with a small task for you. Add support for nested accordions. Let me give you a hint about this one:
The accordionChilds
property is holding a reference to all the AccordionComponent
children for this AccordionComponent
— use it.. Pay attention to an existing bug. The accordion itself will be inside this collection.
You can find the complete code in this repo, or see it live in ng-run.
🚀 In Case You Missed It
- Akita: One of the leading state management libraries, used in countless production environments. Whether it’s entities arriving from the server or UI state data, Akita has custom-built stores, powerful tools, and tailor-made plugins, which all help to manage the data and negate the need for massive amounts of boilerplate code.
- Spectator: A library that runs as an additional layer on top of the Angular testing framework, that saves you from writing a ton of boilerplate. V4 just came out!
- And of course, Transloco: The Internationalization library Angular 😀
Follow me on Medium or Twitter to read more about Angular, Akita and JS!