High Performance Accordion Component
Combining multiple components
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: The Role of Components and Directives PUBLIC
- Lesson 3: When to Use Custom Components & Directives PRO
- Lesson 4: Building a Simple Component PRO
- Lesson 5: Building a Simple Directive PRO
- Lesson 6: Setting up Components and Directives PRO
- Lesson 7: Using @Input and @Output PRO
- Lesson 8: Using ElementRef and Renderer PRO
- Lesson 9: Understanding Content Projection PRO
- Lesson 10: Understanding @ViewChild and @ContentChild PRO
- Lesson 11: Building a Complex Component PRO
- Lesson 12: Listening for Events and Binding to Properties PRO
- Lesson 13: Building a Skeleton Card Component PRO
- Lesson 14: Creating an Autogrow Directive for a Textarea PRO
- Lesson 15: Handling Error Messages with a Custom Component PRO
- Lesson 16: Building a Parallax Header Directive PRO
- Lesson 17: High Performance Accordion Component PRO
- Lesson 18: Conclusion PRO
Lesson Outline
High Performance Accordion Component
In this final lesson, we are going to go all out and create a highly complex animated accordion that will actually consist of two individual components. The end result will look like this:
The main issue with building an animated accordion component is that you would naturally animate the height
of the content that is being opened in the accordion list. This is what gives it that "accordion" feel. One item expands pushing the other down, and then when it is closed it collapses all of the items below it back again. However, the problem with animating height
is that it is bad for performance. Animating height
, as we've touched on a few times, will trigger browser layouts as items are pushed around the screen and need to have their positions recalculated. This is an expensive process for the browser, especially if it needs to do it a lot (e.g. as it does when animating the height
of an item in an accordion list).
In some scenarios, animating height
might still keep your application peformant enough to be acceptable, but if we want to animate whilst maintaining a high degree of performance we should focus on animating only the transform
property for this kind of behaviour. That's easier said than done, though. The good thing about a transform
is that it only impacts the element being transformed, meaning that the positions of other elements on the screen won't be impacted and so the browser doesn't need to perform expensive layout recalculations. The bad thing for our scenario is that we want the other items in the list to be impacted - when one item is opened, all the other items need to move down the screen.
To solve this catch-22 situation, we use a trick that I also made use of in Advanced Animations & Interactions with Ionic to create a high performance delete animation. This module is focused on structuring components/directives so we aren't going to dive too deeply into the animations here, but we will at least cover what is happening at a basic level.
The Trick
Before we get into the code for this I want to highlight how the concept works in general, otherwise things might get a bit confusing. Here's the general process for how opening an accordion item will work:
- An accordion item is clicked
- The content for the item is displayed immediately (no animation)
- Every item below the item being opened is transformed up so that it hides the content that was just displayed (at this point, there will be no noticeable change on screen, because the items were just instantly transformed back into the position that they were at initially before the result is rendered to the screen)
- The elements that were just transformed up have the transforms animated away. This will cause them to slide down to reveal the content that was just displayed.
By translating the position of all of the items below the one being opened, we can give the appearance of the height
of the content being animated, but really everything else below it is just being moved out of the way with a transform
. The one remaining issue with this is that since we rely on the elements below the one being opened to initially block the content from being visible, we run into a problem when either:
- The last element in the accordion list is being opened (it won't have anything to block the content, so the content will just appear immediately and won't be animated)
- The content for an item earlier in the accordion list is long enough that it extends past the bottom of the list anyway, in which case we will see the content leaking out of the bottom of the list.
To handle this, we create an invisible "blocker" element that sits at the bottom of the list, and will change its height dynamically to make sure it is large enough to block any content from being visible (e.g. if the item being opened has content that is 250px
high, the blocker will dynamically be set to a height of 250px
). If you're thinking - hey! you said we weren't going to use height - the important difference here is that we are not animating the height, it will just instantly be set to whatever value we need.
To make this blocker "invisible" we have to set it to be the same colour as the background, which creates one weird limitation for this component that it can only be used on pages with a solid background colour (e.g. not a gradient or image).
Even with this overview description, I still think the concept is more than a little bit confusing. To help, I've created a diagram of what this process actually looks like. I have given the "blocker" element an obvious colour, and reduced the opacity of the items so that we can better see what is going on behind the scenes:
In this example, the blocker isn't actually necessary because we are opening the second item in the accordion list and the content is not long enough to extend past the bottom of the list. However, if this item was one of the last two items the blocker would come into play to hide the content.
Once you understand this process, closing the item again is quite a bit simpler. We just first animate all of those items back with a transform so that they are covering the content again, and once they are covering the content we remove the content (basically the same process, just in reverse).
Setting up the Components
Once we have finished building these components, we will be able to use them like this:
<app-accordion-group>
<app-accordion-item>
<ng-container header>
<h3>Title goes here</h3>
</ng-container>
<ng-container content>
<p>Content goes here</p>
</ng-container>
</app-accordion-item>
<app-accordion-item>
<ng-container header>
<h3>Title goes here</h3>
</ng-container>
<ng-container content>
<p>Content goes here</p>
</ng-container>
</app-accordion-item>
</app-accordion-group>
We have an <app-accordion-group>
that contains multiple <app-accordion-item>
components. You might notice this is similar to how Ionic components like <ion-list>
and <ion-item>
work, or like <ion-card>
and <ion-card-content>
.
We will eventually add multiple <ng-content>
components to our component to project content, and since there is multiple we need a way to specify which content belongs where. We do this using teh header
and content
attributes. To avoid needing to add a pointless container <div>
we use an <ng-container>
which allows us to specify this attribute, but won't actually add anything to the DOM.
If we wanted to, we might have instead structured it like this by adding more components:
<app-accordion-group>
<app-accordion-item>
<app-accordion-item-header></app-accordion-item-header>
<app-accordion-item-content></app-accordion-item-content>
</app-accordion-item>
</app-accordion-group>
But this doesn't really add anything for us aside from removing the need for <ng-container>
. Let's create the components we need now:
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).