Skip to main content
cngx-src documentation

CngxMatStepper

DirectivePrimaryv0.1.0WCAG AA

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

Import#

import { CngxMatStepper } from '@cngx/ui/mat-stepper'

Description#

The Material twin of the CNGX stepper: attach cngxMatStepper to a vanilla <mat-stepper> and it is bridged to a CngxStepperPresenter.
Consumers gain the commit-action lifecycle, CNGX_STATEFUL (so <cngx-toast-on /> / <cngx-banner-on /> compose as children), the shared CNGX_STEPPER_HOST contract (so a <cngx-stepper-footer> can drive Back / Next instead of Material's own buttons via [host]="ref.presenter"), and the step-handle registry - all from one attribute.

This is the instrumentation pattern: Material owns the rendering and the consumer authors native <mat-step> markup; CNGX is the behaviour layer. Topology mirrors [cngxMatTabs].

Inputs/outputs are forwarded from CngxStepperPresenter:

  • activeStepIndex (two-way, with activeStepIndexChange),
  • linear,
  • orientation,
  • commitAction,
  • commitMode.
    See the presenter for their semantics.
  <mat-stepper
    cngxMatStepper
    #s="cngxMatStepper"
    [(activeStepIndex)]="active"
    [commitAction]="commitAction"
    [commitMode]="mode()"
    cngxToastOn
    [toastError]="'Step transition failed'"
    cngxBannerOn
    bannerId="stepper:commit-error"
    [bannerError]="'The server rejected the step change.'"
    aria-label="Account setup"
  >
    <mat-step label="Personal info">
      <p>Tell us who you are.</p>
    </mat-step>
    <mat-step
      label="Account"
      [hasError]="!accepted()"
      errorMessage="Accept the terms to continue"
    >
      <p>Choose your sign-in method.</p>
    </mat-step>
    <mat-step label="Confirm">
      <p>Review everything, then finish.</p>
    </mat-step>
  </mat-stepper>

Metadata#

Dependencies#

CNGX_STEPPER_HOSTinject()presenter

Shared host contract. Public so a <cngx-stepper-footer> placed outside the <mat-stepper> can bind [host]="ref.presenter" (via #ref="cngxMatStepper") and drive navigation.

Bridge instrumentation

import { ChangeDetectionStrategy, Component, signal, ViewEncapsulation } from '@angular/core';
import { MatStepperModule } from '@angular/material/stepper';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
import { Observable } from 'rxjs';

import {
  type CngxStepperCommitAction,
  CngxStepperComplete,
  CngxStepperNext,
  CngxStepperPrevious,
} from '@cngx/common/stepper';
import { CngxMatStepper } from '@cngx/ui/mat-stepper';
import {
  CngxStepperFooter,
  CngxStepperFooterEnd,
  CngxStepperFooterStart,
} from '@cngx/ui/stepper';
import { CngxBannerOn, CngxBannerOutlet, CngxToastOn, CngxToastOutlet } from '@cngx/ui/feedback';

// 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-stepper bridge - everything the instrumentation directive offers.
 *
 * A vanilla `<mat-stepper>` upgraded with one attribute, `cngxMatStepper`,
 * pulling in the whole cngx stepper brain without touching Material's
 * rendering:
 *
 * - **Async commit lifecycle.** `[commitAction]` runs an `Observable`
 *   between transitions; `[commitMode]` picks `optimistic` (advance now,
 *   roll back on rejection) or `pessimistic` (hold until it resolves).
 * - **Feedback bridges.** `cngxToastOn` / `cngxBannerOn` self-wire off
 *   `CNGX_STATEFUL` - the rejection surfaces as a toast and a banner with
 *   zero `inject()` in the consumer.
 * - **cngx footer drives navigation.** `<cngx-stepper-footer>` binds the
 *   shared `CNGX_STEPPER_HOST` via `[host]="s.presenter"`, so Back /
 *   Continue / Finish replace `matStepperPrevious` / `matStepperNext`.
 *   `cngxStepperComplete` runs an async finish action on the last step.
 * - **Native Material chrome.** Per-step validation rides Material's own
 *   `[hasError]` + `errorMessage`; the indicator icons are overridden the
 *   native way through `<ng-template matStepperIcon>`.
 *
 * Toggle the mode and the two checkboxes to exercise the validation
 * channel (accept-terms) and the async-rejection channel (simulate
 * error) independently.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  styleUrl: './bridge-example.component.scss',
  imports: [
    MatStepperModule,
    MatButtonModule,
    MatButtonToggleModule,
    MatCheckboxModule,
    MatIconModule,
    MatCardModule,
    CngxMatStepper,
    CngxStepperFooter,
    CngxStepperFooterStart,
    CngxStepperFooterEnd,
    CngxStepperPrevious,
    CngxStepperNext,
    CngxStepperComplete,
    CngxToastOn,
    CngxBannerOn,
    CngxToastOutlet,
    CngxBannerOutlet,
  ],
  template: `
    <cngx-banner-outlet />

    <mat-card appearance="outlined" style="max-width: 640px">
      <mat-card-header>
        <mat-card-title>Account setup</mat-card-title>
        <mat-card-subtitle>
          Vanilla &lt;mat-stepper&gt; upgraded with <code>cngxMatStepper</code>
        </mat-card-subtitle>
      </mat-card-header>

      <mat-card-content>
        <div class="bridge-demo__chrome">
          <mat-button-toggle-group
            [value]="mode()"
            (change)="mode.set($any($event).value)"
            aria-label="Commit mode"
            hideSingleSelectionIndicator
          >
            <mat-button-toggle value="optimistic">optimistic</mat-button-toggle>
            <mat-button-toggle value="pessimistic">pessimistic</mat-button-toggle>
          </mat-button-toggle-group>

          <mat-checkbox [checked]="shouldFail()" (change)="shouldFail.set($event.checked)">
            Simulate server error
          </mat-checkbox>
        </div>

        <mat-stepper
          cngxMatStepper
          #s="cngxMatStepper"
          [(activeStepIndex)]="active"
          [commitAction]="commitAction"
          [commitMode]="mode()"
          cngxToastOn
          [toastError]="'Step transition failed'"
          cngxBannerOn
          bannerId="stepper:commit-error"
          [bannerError]="'The server rejected the step change.'"
          aria-label="Account setup"
        >
          <!-- Native indicator-icon overrides - no cngx slot needed. -->
          <ng-template matStepperIcon="edit"><mat-icon>edit</mat-icon></ng-template>
          <ng-template matStepperIcon="done"><mat-icon>check</mat-icon></ng-template>
          <ng-template matStepperIcon="error"><mat-icon>priority_high</mat-icon></ng-template>

          <mat-step label="Personal info">
            <p>Tell us who you are.</p>
          </mat-step>

          <!-- Validation channel: Material's own hasError + errorMessage. -->
          <mat-step
            label="Account"
            [hasError]="!accepted()"
            errorMessage="Accept the terms to continue"
          >
            <p>Choose your sign-in method.</p>
            <mat-checkbox [checked]="accepted()" (change)="accepted.set($event.checked)">
              I accept the terms
            </mat-checkbox>
          </mat-step>

          <mat-step label="Confirm">
            <p>Review everything, then finish.</p>
          </mat-step>
        </mat-stepper>

        <p class="bridge-demo__readout">
          Active step: {{ active() }} @if (done()) { &middot; submitted }
        </p>
      </mat-card-content>

      <mat-card-actions>
        <!-- The footer lives outside the stepper and is handed the host
             explicitly; its atoms drive Material via CNGX_STEPPER_HOST. -->
        <cngx-stepper-footer [host]="s.presenter" style="display: flex; gap: 8px; width: 100%">
          <button mat-button cngxStepperFooterStart cngxStepperPrevious>Back</button>
          @if (s.presenter.isLastStep()) {
            <button
              mat-raised-button
              cngxStepperFooterEnd
              [cngxStepperComplete]="finish"
              (completed)="done.set(true)"
            >
              Finish
            </button>
          } @else {
            <button mat-raised-button cngxStepperFooterEnd cngxStepperNext>Continue</button>
          }
        </cngx-stepper-footer>
      </mat-card-actions>
    </mat-card>

    <cngx-toast-outlet />
  `,
})
export class StepperBridgeExample {
  protected readonly active = signal(0);
  protected readonly mode = signal<'optimistic' | 'pessimistic'>('optimistic');
  protected readonly shouldFail = signal(false);
  protected readonly accepted = signal(false);
  protected readonly done = signal(false);

  /** Async transition gate - rejects when "simulate server error" is on. */
  protected readonly commitAction: CngxStepperCommitAction = (from, to) =>
    new Observable<boolean>((sub) => {
      const handle = setTimeout(() => {
        if (this.shouldFail()) {
          sub.error(new Error('Step ' + from + ' → ' + to + ' rejected'));
        } else {
          sub.next(true);
          sub.complete();
        }
      }, 800);
      return () => clearTimeout(handle);
    });

  /** Async finish action for `cngxStepperComplete` on the last step. */
  protected readonly finish = (): Promise<void> =>
    new Promise((resolve) => setTimeout(resolve, 600));
}
export { StepperBridgeExample as AppComponent };