@ContentChildren
INTRODUCTION
As Angular developer you should well know the difference between stateless vs stateful components and how transclusion, Input and Output decorators work.
If not, you should read this interesting article by Todd Motto about this topic.
STATELESS COMPONENTS
First of all, we need to create a stateless collapsable panel. It will be an important part of our Accordion but can be also work as standalone component, as you’ll see below:
<panel title="Profile Form" [opened]="isOpen" (toggle)="isOpen = !isOpen"> <input type="text"> ... any element or component ... </panel>
Why we don’t create a stateful panel component instead?
It would be very comfortable to use since it would handle the behaviors from within the component itself:
<panel title="Profile Form"> <input type="text"> ... any element or component ... </panel>
Stateless components often need a stateful parent to manage their state instead.
Although it may seem a limit, this represents one of the key-points to create presentational components in Flux / Redux / or in any other FE architectural pattern.
Furthermore, since we’ll use these panels as children of the accordion, this approach will be necessary in order to manage them.

PANEL COMPONENT
Here the completed source code of the panel component:
import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'panel', template: ` <div class="panel panel-info"> <div class="panel-heading" (click)="toggle.emit()"> {{title}} </div> <div class="panel-body" *ngIf="opened"> <ng-content></ng-content> </div> <div> ` }) export class PanelComponent { @Input() opened = false; @Input() title: string; @Output() toggle: EventEmitter<any> = new EventEmitter<any>(); }
KEY POINTS
- The
title
input property and theng-content
component are used to transfer content from the parent to the component.
The first one is used to pass a string value as attribute while the second one can be used to transfer any kind of content (aka transclusion). - The panel accepts an
opened
Input property too, that can be used to display or hide the body of the panel - When the header of the panel is clicked, a
toggle
“Output” event is emitted - When
toggle
event is emitted, the parent should perform a logical negation to theopened
property
Of course, you could also apply an *ngFor
directive to a panel component and populate each cloned template with dynamic data:
<panel *ngFor="let c of cities" [title]="c.name" [opened]="c.opened" (toggle)="c.opened = !c.opened"> {{c.desc}} </panel>
and the result will be very similar to an accordion component:
Tip for React devs
You can think @Input
decorators as React props
and transclusion as the special children
property.
ACCORDION
INTRODUCTION
Our goal is the creation of an Accordion component whose children are represented by the Panel
components:
<accordion> <panel title="Rome">... any ...</panel> <panel title="New York">... any ...</panel> <panel *ngFor="let c of cities" [title]="c.name">{{c.desc}}</panel> </accordion>
It would be very cool but we cannot do it.
In fact, since our panel
components are stateless, we need to pass an opened
value to each one, listening each toggle
event in order to toggle the state of the panel when an header is clicked.
Of course, you could manually do it for each panel:
<accordion> <panel title="Rome" [opened]="isOpen" (toggle)="isOpen = !isOpen">...</panel> <panel title="Trieste" [opened]="isOpen2" (toggle)="isOpen2 = !isOpen2">...</panel> <panel title="Milano" [opened]="isOpen3" (toggle)="isOpen3 = !isOpen3">...</panel> </accordion>
or by using a data-driven approach as well:
<accordion> <panel *ngFor="let c of cities" [title]="c.name" [opened]="c.opened" (toggle)="c.opened = !c.opened">{{c.desc}}</panel> </accordion>
Anyway, in both scenarios we have to manually handle inputs / outputs and it’s not very comfortable.
When I need to reuse a component I want to spend as little time as possible to remember how to add it to my template.
So how can accomplish this task in order to be used in the following way?
<accordion> <panel title="Rome">... any ...</panel> <panel title="New York">... any ...</panel> <panel *ngFor="let c of cities" [title]="c.name">{{c.desc}}</panel> </accordion>
ACCORDION COMPONENT
First, the accordion component should orchestrate Inputs/Outputs of any child and it should be done “behind the scenes” to enhance the “developer experience”.
Furthermore, we have other two minor goals too:
- automatically open the first panel when an accordion is instantiated
- close the active panel when a new one is toggled
So, following the completed source code of the accordion and I have published a working demo on StackBlitz:
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core'; import { PanelComponent } from './panel.component'; @Component({ selector: 'accordion', template: '<ng-content></ng-content>' }) export class AccordionComponent implements AfterContentInit { @ContentChildren(PanelComponent) panels: QueryList<PanelComponent>; ngAfterContentInit() { // Open the first panel this.panels.toArray()[0].opened = true; // Loop through all panels this.panels.toArray().forEach((panel: QueryList<PanelComponent>) => { // subscribe panel toggle event panel.toggle.subscribe(() => { // Open the panel this.openPanel(panel); }); }); } openPanel(panel: PanelComponent) { // close all panels this.panels.toArray().forEach(p => p.opened = false); // open the selected panel panel.opened = true; } }
KEY POINTS
ContentChildren
is a special decorator you can use to select children by using a rule. In this scenario our rule is simply “select all the children whose class name isPanelComponent
“. You can think to it as a special querySelectorAll for your Angular components- The HTML template simply contains an
ng-content
component to receive and display dynamic content into it (in this scenario our panels) - We need to wait the
ngAfterContentInit
lifecycle hook to be sure all children are ready and available - Now we can open the first panel, setting its
opened
property totrue
, and loop through all panels to manually subscribe eachtoggle
event. So, when a panel is toggled, theopenPanel
will be automatically invoked. - The
openPanel
method closes all panels and opens the current one. - In a real world scenario you should unsubscribe all events when the
ngOnDestroy
hook is invoked.
QueryList
is an unmodifiable list of items that Angular keeps up to date when the state of the application changes so any time a child element is added, removed, or moved the query list will be updated. In fact QueryList provides a changes
property you can subscribe to get notified when something happens to the list.
Tip for React devs
@ContentChildren
is a special way to get references to Angular classes and components, very similar to React refs
but more powerful
Leave A Comment