Skip to main content
cngx-src documentation

CngxMatTabs

DirectivePrimaryv0.1.0WCAG AA

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

Import#

import { CngxMatTabs } from '@cngx/ui/mat-tabs'

Description#

Attaches to an existing <mat-tab-group> and wires it to a CngxTabGroupPresenter - consumers get the commit-action lifecycle, CNGX_STATEFUL (so <cngx-toast-on /> and <cngx-banner-on /> compose as children), and the tab-handle registry from one attribute.

Topology inverts <cngx-mat-stepper>: Material is the host, cngx is the instrumentation layer. inject(MatTabGroup, { self: true }) resolves on the consumer's own element - no content projection blocker applies here.

Rejection (--error) and aggregator (--has-errors) visuals live in the package-private projector factories under ./decorations/; this directive supplies their reactive triggers and the delegators consumer templates call.

Metadata#

Providers#

CNGX_TAB_OVERFLOW_DOM_ADAPTER_FACTORY
useValue createCngxMatTabOverflowDomAdapter
CNGX_TAB_PANEL_HOST
useExisting CngxMatTabsPanelHostAdapter

Relationships

Index#

Methods#

clearLastFailed#void

Clears presenter.lastFailedIndex, dismissing the rejection decoration. Mirrors CngxTabGroup.clearLastFailed() so a template ref (#mt="cngxMatTabs") is enough - no host-token injection needed.

Form error aggregation

import { ChangeDetectionStrategy, Component, ViewEncapsulation, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { MatTabsModule } from '@angular/material/tabs';
import { startWith } from 'rxjs/operators';

import { injectErrorAggregator } from '@cngx/common/interactive';
import { CngxMatTabError, CngxMatTabs } from '@cngx/ui/mat-tabs';

/**
 * Per-tab form error aggregation on `<mat-tab-group>`.
 *
 * A single `cngxMatTabs` attribute wires the existing `<mat-tab-group>`
 * into the cngx tab-handle registry. Each `<mat-tab>` then accepts
 * `[cngxMatTabError]` bound to a `CngxErrorAggregator` built with
 * `injectErrorAggregator`. The aggregator's source signal reads
 * `statusChanges` (via `toSignal` + `startWith`) and the matching
 * Material tab header gains a `cngx-mat-tab--has-errors` badge plus
 * an SR descriptor span — fully signal-derived, no manual toggles.
 * Both tabs start with valid pre-filled values; clear a field and
 * switch tabs to watch the badge appear on the header.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [MatTabsModule, ReactiveFormsModule, CngxMatTabs, CngxMatTabError],
  styleUrl: './form-errors.component.scss',
  template: `
    <p style="margin: 0 0 12px; opacity: 0.8; font-size: 0.875rem">
      Vanilla <code>&lt;mat-tab-group&gt;</code> + <code>cngxMatTabs</code>.
      Clear a field — the per-tab error aggregator reads
      <code>statusChanges</code> and a badge lights up on the tab header.
    </p>
    <mat-tab-group cngxMatTabs aria-label="Form error aggregation demo">
      <mat-tab label="Profile" [cngxMatTabError]="profileErrors">
        <form
          [formGroup]="profileForm"
          style="display: flex; flex-direction: column; gap: 8px; padding: 12px"
        >
          <label style="display: flex; flex-direction: column; gap: 4px">
            <span>Name</span>
            <input type="text" formControlName="name" />
          </label>
          <small style="opacity: 0.7">Required, min 2 characters</small>
        </form>
      </mat-tab>
      <mat-tab label="Account" [cngxMatTabError]="accountErrors">
        <form
          [formGroup]="accountForm"
          style="display: flex; flex-direction: column; gap: 8px; padding: 12px"
        >
          <label style="display: flex; flex-direction: column; gap: 4px">
            <span>Email</span>
            <input type="email" formControlName="email" />
          </label>
          <small style="opacity: 0.7">Required, valid email format</small>
        </form>
      </mat-tab>
      <mat-tab label="Notifications">
        <p style="padding: 12px">No aggregator bound — no badge on this tab.</p>
      </mat-tab>
    </mat-tab-group>
  `,
})
export class MatTabsFormErrorsExample {
  protected readonly profileForm = new FormGroup({
    name: new FormControl('Jane Doe', {
      nonNullable: true,
      validators: [Validators.required, Validators.minLength(2)],
    }),
  });
  protected readonly accountForm = new FormGroup({
    email: new FormControl('jane@example.com', {
      nonNullable: true,
      validators: [Validators.required, Validators.email],
    }),
  });

  private readonly profileStatus = toSignal(
    this.profileForm.statusChanges.pipe(startWith(this.profileForm.status)),
    { initialValue: this.profileForm.status },
  );
  private readonly accountStatus = toSignal(
    this.accountForm.statusChanges.pipe(startWith(this.accountForm.status)),
    { initialValue: this.accountForm.status },
  );

  protected readonly profileErrors = injectErrorAggregator(
    undefined,
    { profile: computed(() => this.profileStatus() === 'INVALID') },
    undefined,
    { profile: 'Profile name is required (min 2 chars)' },
  );
  protected readonly accountErrors = injectErrorAggregator(
    undefined,
    { account: computed(() => this.accountStatus() === 'INVALID') },
    undefined,
    { account: 'Account email must be valid' },
  );
}
export { MatTabsFormErrorsExample as AppComponent };