How to Safeguard Child Content in Angular 2+

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?

Interactive Sandbox Demo

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!

Answer №1

To achieve the specific task mentioned in your query, you can set all panels to be closed by iterating over the existing expansionPanels instance attribute:

import { ExpansionPanelState } from "PATH/TO/FILE/expansion-panel.model";

ngAfterContentInit() {
  this.expansionPanels
    .forEach(panel => panel.setState(ExpansionPanelState.Collapsed));
}

A helpful hint: when working with the template of your ExpansionPanelComponent, there is no need to send the state back to TypeScript since it is already accessible within the template. You can update it as follows:

<header (click)="toggle()"...

We can now make some adjustments in the toggle method to retrieve the current state of your BehaviorSubject instead of extracting its value. The use of the take operator ensures that we automatically unsubscribe after receiving the last emission:

import {take} from 'rxjs/operators';
...
toggle(): void {
  this.state$.pipe(take(1)).subscribe((value: ExpansionPanelState) => {
    switch (value) {...}
  });
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I am curious about the types of props for the methods within the 'components' object in react-markdown

Having some trouble using 'react-markdown' in NextJs 13 with typescript. TypeScript is showing errors related to the props of the 'code' method, and after searching online, I found a solution that involves importing 'CodeProps&apos ...

detecting modifications in the div tag

Utilizing a third-party library to scan QR codes is a necessity. Once the scanning is finished, the content within the <div id="qr-code-status"></div> element changes accordingly. While the QR code is being scanned, the innerText of t ...

Managing Scroll Behavior in Ionic

I'm in the process of developing a quiz application using Ionic and Angular. My goal is to display one card at a time, similar to how Instagram and Facebook do it. This means that as users scroll down the app, only one card should be visible on their ...

the process of accessing information from a service in an Angular Typescript file

After making a POST request using Angular's HTTP client, the response data can be accessed within the service. However, is there a way to access this data in the app.component.ts file? I am able to retrieve the response data within the service, but I ...

Creating a routed component with several different templates

I'm exploring the idea of creating a routed component with multiple template entry points in Angular and I'm unsure if it's feasible. Let me provide more details. In Angular, we have the ability to create nested elements that contain templa ...

Tips for resolving the 'No Spec Found' issue with TypeScript

Encountering a 'No Specfound error' while attempting to run test cases. Utilized the existing protractor methods wrapped in custom helper methods with async await. Tried relocating the spec file and experimenting with different naming convention ...

Looping issue with ForEach in Typscript with Firebase Functions

While browsing through various questions on this topic, I've noticed that the specific answers provided don't quite fit my situation. My query involves converting a Google Firebase RTB datasnapshot into an array of custom objects, each representi ...

Export an array of objects using the ExcelService module

I am working with an array named listTutors that looks like this: listTutors = [{ countryId: 'tt', gender: 'both', levelId: 'tg', sessionType: 'inPerson', dashboardStatus: ['notPublished', 'p ...

The comparison between importing and requiring mutable values for export

I'm exploring the distinction between import and require in relation to exporting and importing mutable values. Picture a file a.ts: export let a = 1; export function f() { a = 2; } Next, we have three versions of a main file, index1.ts: import { ...

Can you explain the distinction between Array<string> and string[]?

Can you explain the contrast between Array<string> and string[]? var companies: Array<string> = ['Samsung', 'Sony', 'LG']; var businesses: string[] = ['Lenovo', 'Asus', 'Acer']; ...

Employing async await for postponing the execution of a code block

I have been working on an Angular component where I need to perform some actions after a data service method returns some data asynchronously. Although I attempted to use async/await in my code, I feel that I may not have fully understood how it works. Her ...

The module for the class could not be identified during the ng build process when using the --

Encountering an error when running: ng build --prod However, ng build works without any issues. Despite searching for solutions on Stack Overflow, none of them resolved the problem. Error: ng build --prod Cannot determine the module for class X! ...

ngFor returning undefined despite array containing value

While iterating through an array using ngFor, I'm encountering an issue where trying to access data in the 'value' variable results in it being undefined. Why is this happening? myArray = ['a', 'b', 'c', 'd ...

The declaration module in Typescript with and without a body

I am facing an issue with importing a .mdx file. When I include the following in my mdx.d.ts: /// <reference types="@mdx-js/loader" /> import { ComponentType } from "react"; declare module '*.mdx' { const Component: ...

Angular2 bootstrapping of multiple components

My query pertains to the following issue raised on Stack Overflow: Error when bootstrapping multiple angular2 modules In my index.html, I have included the code snippet below: <app-header>Loading header...</app-header> <app-root>L ...

Tips on making Angular Material form controls dynamic

I am currently facing a dilemma where I am unsure how to dynamically set form controls. Below is the code snippet that illustrates my issue: <div [formGroup]="form"> <mat-form-field appearance="legacy"> <input matI ...

Tips on utilizing the `arguments` property in scenarios where Parameters<...> or a similar approach is anticipated

How can you pass the arguments of a function into another function without needing to assert the parameters? Example: function f(a:number, b:number){ let args:Parameters<typeof f> = arguments // Error: Type 'IArguments' is not assignab ...

Is there a way to avoid waiting for both observables to arrive and utilize the data from just one observable within the switchmap function?

The code snippet provided below aims to immediately render the student list without waiting for the second observable. However, once the second observable is received, it should verify that the student is not enrolled in all courses before enabling the but ...

Encountered an issue locating module '@apollo/client/core' or its corresponding type declarations.ts while attempting to implement GraphQL in an Angular configuration

Hey there, I recently started exploring the world of GraphQL within Angular, but I'm a newbie to both technologies. I encountered an error while setting up the schema for GraphQL queries. To tackle this, I attempted to install apollo-client, as I&apos ...

Is it possible to enforce a certain set of parameters without including mandatory alias names?

My inquiry pertains to handling required parameters when an alias is satisfied, which may seem complex initially. To illustrate this concept, let's consider a practical scenario. If we refer to the Bing Maps API - REST documentation for "Common Param ...