Two Angular components, specifically an accordion with expansion panels, are in need to be written without a framework. ContentChild is utilized to handle all the expansion panels within the accordion component. The goal is to parse these components to examine their state and update a property value from the parent container accordingly.
Currently, each expansion-panel emits an event that indicates whether it is open or closed.
The task involves tracking the state$ of each ChildContent after checking them all. A default needs to be set if none exist, and any changes should trigger an event to the parent accordion whenever an inner child instance is toggled so that the expanded index can be determined.
The objective is to loop through each ChildContent instance to detect any openings. If no panel is open, the first one should be opened by modifying its state. In the wrapper, a subscription to each panel is made to check if they are open. If none are open, the first child should expand.
Whenever one child panel expands, the rest should contract except for the selected one.
@Component({
selector: 'app-accordion',
templateUrl: './accordion.component.html'
})
export class AccordionComponent extends Destroyable implements AfterContentInit, OnDestroy {
@ContentChildren(ExpansionPanelComponent) expansionPanels: QueryList<ExpansionPanelComponent>;
...
This is the inner child component (expansion panel)
@Component({
selector: 'app-expansion-panel',
templateUrl: './expansion-panel.component.html'
})
export class ExpansionPanelComponent extends Destroyable implements OnInit, OnDestroy {
@Input() title!: string;
@Input() content!: string | string[];
/**
* The default for this input would be overwritten if the optional
* @Input() defaultState is provided during initialization.
* The defaultState override occurs inside of this.setDefaultState();
*/
@Input() defaultState: ExpansionPanelState = ExpansionPanelState.Collapsed;
@Input() footerContent?: string;
@Input() headerIcon?: IconName;
@Input() toggleButton?: ViewChild;
@Output() expanded$ = new EventEmitter<ExpansionPanelState>();
setState(expansionPanelState: ExpansionPanelState): void {
if (this.expanded$.observers.length > 0) {
this.expanded$.emit(expansionPanelState);
}
this.state$.next(expansionPanelState);
}
This template gives an idea of how simple the container component (Accordion) should be...
{{ accordionTitle }} {{ accordionSubtitle }} /** ' * The ContentChildren renders ALL of the child components as expanded, no * matter what approach I have tried. I have verified that the event is being * dispatched to the parent, I also see the individual panels toggling as I * expect. What cant get is the dynamic mutually exclusive behavior described * above when the user expands one panel, it should contract the rest. When * the panels are used alone, they should be able to be configured and used * independently of the accordion (this is fine now). How to map/mutate/set * this once I have dynamically adjusted the values of the child * components state? */The issue arises due to wrapping ContentChild in a <ng-container>, preventing reassignment of a mapped version back to it using the ContentChild.prototype.map method. The goal here is to avoid manually rendering the component.
The expansion panel is functioning well and has been unit tested. However, managing the accordion and selecting specific expansion panels poses a challenge, resulting in infinite loops. There must be a simpler way to dynamically update ContentChildren – any guidance on this?
How can the query and update of ContentChildren from within its wrapper component be achieved?
The mapping process should occur in AccordionComponent's ngAfterContentInit(), but the provided approach does not seem to work effectively within the linked sandbox.
Thank you!