Skip to main content
cngx-src documentation

CngxMatAccordion

DirectivePrimaryv0.1.0WCAG AA

projects/ui/mat-accordion/mat-accordion.directive.ts

Import#

import { CngxMatAccordion } from '@cngx/ui/mat-accordion'

Description#

The Material twin of the cngx accordion: attach cngxMatAccordion to a vanilla <mat-accordion> and it is driven by the headless CngxAccordion brain. Consumers gain the controlled [(openIds)] group model and single-open arbitration on Material markup — neither of which <mat-accordion> exposes (Material has only per-panel [expanded]). Mirrors [cngxMatStepper] / [cngxMatTabs].

This is the instrumentation pattern: Material owns rendering and the consumer authors native <mat-expansion-panel> markup; cngx is the behaviour layer. [multi] and the two-way [(openIds)] are forwarded from CngxAccordion via hostDirectives.

  <mat-accordion cngxMatAccordion [(openIds)]="openIds" [multi]="false">
    <mat-expansion-panel>
      <mat-expansion-panel-header>Personal info</mat-expansion-panel-header>
      <p>Tell us who you are.</p>
    </mat-expansion-panel>
    <mat-expansion-panel>
      <mat-expansion-panel-header>Account</mat-expansion-panel-header>
      <p>Choose your sign-in method.</p>
    </mat-expansion-panel>
  </mat-accordion>

multi is authoritative on the cngx side. MatAccordion inherits its own multi input from CdkAccordion, and both directives sit on the same host element, so a single [multi] binding feeds both — the directive therefore pins matAccordion.multi = true (via an effect that re-asserts on every multi change) so Material never runs its own single-open close while the cngx effectiveOpenIds clamp arbitrates.

Metadata#

Controlled open-set

import { ChangeDetectionStrategy, Component, computed, signal, ViewEncapsulation } from '@angular/core';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';

import { CngxMatAccordion } from '@cngx/ui/mat-accordion';

// Re-export forces compodocx to ship app.config.ts in the StackBlitz manifest
// - the only seam for EnvironmentProviders in the playground.
export { appConfig } from './app.config';

/**
 * Mat-accordion bridge - the controlled group model on Material markup.
 *
 * A vanilla `<mat-accordion>` upgraded with one attribute, `cngxMatAccordion`,
 * gaining exactly what Material's own accordion does not expose:
 *
 * - **Controlled `[(openIds)]`.** Expansion is a single `ReadonlySet<string>`
 *   source of truth, not per-panel `[expanded]` bookkeeping. Seed it, read it,
 *   write it - the "collapse all" button just calls `openIds.set(new Set())`.
 * - **Single/multi arbitration.** Flip `[multi]`: in single mode the brain's
 *   `effectiveOpenIds` clamp collapses siblings and `openIds()` holds the last
 *   id; in multi mode every open id stays. Material's own `multi` is pinned
 *   true underneath so it never fights the cngx clamp.
 *
 * Everything visible - headers, ARIA, arrow-key roving between headers - stays
 * Material's. The directive re-renders none of it.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  styleUrl: './bridge-example.component.scss',
  imports: [
    MatExpansionModule,
    MatButtonModule,
    MatButtonToggleModule,
    MatCardModule,
    CngxMatAccordion,
  ],
  template: `
    <mat-card appearance="outlined" style="max-width: 640px">
      <mat-card-header>
        <mat-card-title>Onboarding steps</mat-card-title>
        <mat-card-subtitle>
          Vanilla &lt;mat-accordion&gt; upgraded with <code>cngxMatAccordion</code>
        </mat-card-subtitle>
      </mat-card-header>

      <mat-card-content>
        <div class="bridge-demo__chrome">
          <mat-button-toggle-group
            [value]="multi() ? 'multi' : 'single'"
            (change)="multi.set($any($event).value === 'multi')"
            aria-label="Open mode"
            hideSingleSelectionIndicator
          >
            <mat-button-toggle value="single">single</mat-button-toggle>
            <mat-button-toggle value="multi">multi</mat-button-toggle>
          </mat-button-toggle-group>

          <button mat-stroked-button type="button" (click)="collapseAll()">Collapse all</button>
        </div>

        <mat-accordion cngxMatAccordion [(openIds)]="openIds" [multi]="multi()">
          <mat-expansion-panel>
            <mat-expansion-panel-header>Personal info</mat-expansion-panel-header>
            <p>Tell us who you are.</p>
          </mat-expansion-panel>

          <mat-expansion-panel>
            <mat-expansion-panel-header>Account</mat-expansion-panel-header>
            <p>Choose your sign-in method.</p>
          </mat-expansion-panel>

          <mat-expansion-panel>
            <mat-expansion-panel-header>Confirm</mat-expansion-panel-header>
            <p>Review everything, then finish.</p>
          </mat-expansion-panel>
        </mat-accordion>

        <p class="bridge-demo__readout">Open panels: {{ openCount() }}</p>
      </mat-card-content>
    </mat-card>
  `,
})
export class AccordionBridgeExample {
  protected readonly multi = signal(false);
  protected readonly openIds = signal<ReadonlySet<string>>(new Set());
  protected readonly openCount = computed(() => this.openIds().size);

  protected collapseAll(): void {
    this.openIds.set(new Set());
  }
}
export { AccordionBridgeExample as AppComponent };