Skip to main content
cngx-src documentation

CngxSelect

ComponentPrimaryOnPushNo encapsulationv0.1.0WCAG AA

projects/forms/select/single-select/select.component.ts

Import#

import { CngxSelect } from '@cngx/forms/select'

Description#

Metadata#

Host#

Providers#

CNGX_FORM_FIELD_CONTROL
useExisting CngxSelect
CNGX_SELECT_PANEL_HOST
useExisting CngxSelect
CNGX_SELECT_PANEL_VIEW_HOST
useExisting CngxSelect

Relationships

Index#

Inputs#

announceChanges#boolean | null
input()
default null
input()
default null
ariaLabel#string | null
input()
default null, { alias: 'aria-label' }
ariaLabelledBy#string | null
input()
default null, { alias: 'aria-labelledby' }
autofocus#boolean
input()
default false
caretGlyph#TemplateRef | null

Replaces the built-in caret glyph. Ignored when *cngxSelectCaret is projected.

default null
clearable#boolean
input()
default false
clearButtonAriaLabel#string
input()
default this.config.ariaLabels?.clearButton ?? 'Clear selection',
clearGlyph#TemplateRef | null

Replaces the built-in glyph inside the default clear button while keeping the button frame, ARIA, and click handler. Ignored when *cngxSelectClearButton is projected.

default null
default null
commitErrorAnnouncePolicy#CngxCommitErrorAnnouncePolicy
input()

Scalar-commit error-announce policy. 'verbose' reads the error message; 'soft' reads "selection removed". Per-instance input wins over CngxSelectConfig.commitErrorAnnouncePolicy; default { kind: 'verbose', severity: 'assertive' }.

default
    this.config.commitErrorAnnouncePolicy ?? { kind: 'verbose', severity: 'assertive' },  
input()
default this.config.commitErrorDisplay
input()
default 'optimistic'
default cngxSelectDefaultCompare as CngxSelectCompareFn<T>,
disabledInput#boolean
input()
default false, { alias: 'disabled' }
hideCaret#boolean
input()
default !this.config.showCaret
hideSelectionIndicator#boolean
input()
default !this.config.showSelectionIndicator
idInput#string | null
input()
default null, { alias: 'id' }
label#string
input()
default ''
loading#boolean
input()
default false
input()
default this.config.loadingVariant
default [] as CngxSelectOptionsInput<T>
panelClass#string | readonly string[] | null
input()
default null
panelWidth#'trigger' | number | null
input()
default this.config.panelWidth
placeholder#string
input()
default ''
popoverPlacement#PopoverPlacement
input()

Popover placement relative to the trigger. Per-instance input wins over CngxSelectConfig.popoverPlacement.

default this.config.popoverPlacement
input()
default this.config.refreshingVariant
requiredInput#boolean
input()
default false, { alias: 'required' }
selectionIndicatorPosition#'before' | 'after' | null
input()
default null
input()
default null
skeletonRowCount#number
input()
default this.config.skeletonRowCount
default null
tabIndex#number
input()
default 0
typeaheadDebounceInterval#number
input()
default this.config.typeaheadDebounceInterval
value#T | undefined
model()

Outputs#

closed#void
output()
commitError#unknown
output()
opened#void
output()
openedChange#boolean
output()
retry#void
output()
selectionChange#CngxSelectChange
stateChange#AsyncStatus
output()
value#T | undefined
model()

Instance Properties#

commitState#unknown
Readonly

Read-only view of the commit lifecycle.

this.core.commitState
disabled#unknown
Readonly
this.core.disabled
isCommitting#unknown
Readonly

true while a commit is in flight.

this.core.isCommitting
retryFn#unknown
Readonly
input<(() => void) | null>(null)
selected#unknown
Readonly

Currently selected option, resolved against options. Structural equal on .value under compareWith - fresh OptionDef references for the same value don't cascade downstream re-renders (server refetch pattern).

computed<CngxSelectOptionDef<T> | null>(() => this.selectedOption(), {
  equal: (a, b) => {
    if (a === b) {
      return true;
    }
    if (a === null || b === null) {
      return false;
    }
    return (this.compareWith() as CngxSelectCompareFn<unknown>)(a.value, b.value);
  },
})

Methods#

clearLocalItems#void

Reset the local buffer. Idempotent.

close#void
focus#void
focus(options?: FocusOptions)
@paramoptions?FocusOptions
isEmpty#boolean
Protected
open#void
patchData#void
patchData(item: CngxSelectOptionDef<T>)

Append a pre-built option to the local buffer. Renders in the next panel emission and silently drops once the server includes a matching value. Idempotent under compareWith.

toggle#void

HostBindings#

BindingExpression
[id]resolvedId()
[attr.aria-readonly]ariaReadonly()

Structural styles shared across the entire CngxSelect family - imported via styleUrls from every variant (CngxSelect, CngxMultiSelect, CngxCombobox, CngxTypeahead, CngxTreeSelect, CngxReorderableMultiSelect, CngxActionSelect, CngxActionMultiSelect, CngxSelectShell, and the panel components).

All variants run encapsulation: None so the rules below are global; the .cngx-select__* BEM-style class prefix is the namespace.

State modifiers

  • .cngx-option--highlighted on an option row - primary-tinted background marking the keyboard-focused row
  • [aria-disabled='true'] on an option - opacity 0.5 + cursor: not-allowed
  • .cngx-select__option--pending - cursor: progress while a row's commit is in flight
  • .cngx-select__chip-list--reordering - whole-strip cursor: grabbing while a drag is active
  • .cngx-select__chip--dragging on a chip - Trello-style lift (shadow + scale + tilt + accent fill)
  • .cngx-select__chip--drag-over on a chip - vertical drop bar painted on the leading edge via ::before

Variants

Chip-list overflow modes via data-overflow:

  • default - flex-wrap chips across multiple rows
  • data-overflow='scroll-x' - single row + horizontal scroll
  • data-overflow='truncate' - single row + clipped overflow + the .cngx-select__chip-overflow-badge count pill

Error surfaces with two layouts:

  • .cngx-select__error - column-stacked block (message + retry)
  • .cngx-select__error--inline - inline banner above the option list
  • .cngx-select__commit-error - per-commit failure banner

Slots

  • .cngx-select__panel - the floating panel surface (anchor-positioned via position-try-fallbacks)
  • .cngx-select__group-header - uppercase optgroup label
  • .cngx-select__option / .cngx-select__check - option row + the selected-state checkmark
  • .cngx-select__loading / .cngx-select__empty - inline message rows
  • .cngx-select__skeleton / .cngx-select__skeleton-row - first-load shimmer placeholders (static under prefers-reduced-motion)
  • .cngx-select__spinner-wrap / .cngx-select__spinner - first-load spinner ring
  • .cngx-select__loading-bar - first-load top-edge progress bar
  • .cngx-select__refreshing / .cngx-select__refreshing-spinner / .cngx-select__refreshing-dots - subsequent-load indicators
  • .cngx-select__option-spinner / .cngx-select__option-error - per-row commit feedback
  • .cngx-select__chip-list / .cngx-select__chip-overflow-badge - chip strip + overflow count
  • .cngx-select__chip-wrap / .cngx-select__chip-handle - reorderable chip surface + optional drag-handle glyph
  • .cngx-select__error / .cngx-select__error--inline / .cngx-select__error-message / .cngx-select__error-retry / .cngx-select__commit-error - error block, banner, and retry pieces

Inheritance

Tokens are registered top-level in @layer cngx.tokens via @property so any consumer (cngx.theme layer, app layer) can override without learning the type.

Panel surface, every primary-tinted glyph, and every danger-toned error surface delegate to the foundation so a dark-mode swap cascades through the floating panel even when anchor positioning detaches it from the trigger's DOM context:

  • --cngx-select-panel-border -> --cngx-color-border
  • --cngx-select-panel-bg -> --cngx-color-surface
  • --cngx-select-panel-color -> --cngx-color-text
  • --cngx-select-panel-shadow -> --cngx-shadow-md
  • --cngx-select-check-color -> --cngx-color-primary
  • --cngx-select-spinner-color -> --cngx-color-primary
  • --cngx-select-loading-bar-color -> --cngx-color-primary
  • --cngx-select-refreshing-color -> --cngx-color-primary
  • --cngx-select-option-spinner-color -> --cngx-color-primary
  • --cngx-select-option-error-color -> --cngx-color-danger
  • --cngx-select-error-color -> --cngx-color-danger
  • --cngx-select-chip-remove-hover-color -> --cngx-color-danger
  • --cngx-select-chip-drag-bg -> --cngx-color-primary
  • --cngx-select-chip-drop-bar-color -> --cngx-color-primary
  • --cngx-select-trigger-invalid-border-color -> --cngx-color-danger
  • --cngx-select-trigger-invalid-outline-color -> --cngx-color-danger

--cngx-select-panel-min-width is intentionally NOT registered via @property so the anchor-size(width) fallback survives when the consumer doesn't override it; registering it with any initial-value would shadow the var() fallback and break anchor positioning.

Dark mode

Three hooks swap alpha-on-black surfaces (panel shadow, option highlight, spinner track, chip-overflow tint, chip remove-hover tint, chip handle muted, chip drag shadow) plus the skeleton shimmer gradient and the option-spinner track:

  • prefers-color-scheme: dark
  • [data-color-scheme="dark"]
  • .dark class

Foundation-delegated tokens ride the foundation cascade unchanged.

Pair with

  • @cngx/themes/material/select-theme - Material 3 surface treatment across the entire family

Trigger skin for CngxSelect - the scalar single-select. The host carries .cngx-select; this file owns only the trigger-button visual and the host layout - the parts that differ per select-type. Panel frame, option rows, skeletons, spinners, error banners, and refreshing indicators ride on shared/select-base.css (loaded via this component's styleUrls).

State modifiers

  • :focus-visible on .cngx-select__trigger - focus-ring outline + offset
  • [aria-disabled='true'] on the trigger - opacity dim + cursor: not-allowed
  • [aria-invalid='true'] on the trigger - danger border + box-shadow glow at focus-within time
  • :hover on .cngx-select__clear - opacity 1
  • :focus-visible on the clear button - 2px primary outline

Slots

  • .cngx-select__root - display: contents host shim
  • .cngx-select__trigger - the bordered role="combobox" row (user-select: none prevents drag-click text selection)
  • .cngx-select__label - ellipsis-truncating value label (inline-flex with a small gap between an optional glyph and text)
  • .cngx-select__caret - panel-state glyph
  • .cngx-select__clear - reset button

Inheritance

Trigger surface + focus tokens delegate to the foundation so dark-mode swaps on --cngx-color-border / --cngx-color-primary cascade through the trigger host:

  • --cngx-select-border -> --cngx-color-border
  • --cngx-select-focus-outline -> --cngx-color-primary
  • invalid surfaces fall back to --cngx-select-trigger-invalid-* -> --cngx-color-danger

Shorthand tokens pin their structural part (1px solid / 2px solid) and inherit only the colour through the delegating chain.

Pair with

  • @cngx/themes/material/select-theme - Material 3 surface treatment shared across the entire select family

Index#

Surface

--cngx-select-panel-border#*
Default value 1px solid oklch(0.85 0.01 250)

Border shorthand of the dropdown panel. Falls back through --cngx-color-border. inherits: true so the :root delegating value reaches the floating panel host (anchor-positioned, may sit outside the normal DOM flow).

See: [[--cngx-color-border]]

--cngx-select-panel-bg#<color>
Default value oklch(1 0 0)

Background of the dropdown panel. Falls back through --cngx-color-surface.

See: [[--cngx-color-surface]]

--cngx-select-panel-color#*
Default value currentColor

Text color inside the panel. syntax: '*' + initial-value currentColor lets the panel inherit text color from its ancestor by default. Registering as <color> would forbid currentColor since <color> requires a computationally- independent value.

--cngx-select-panel-shadow#*
Default value 0 4px 12px oklch(0 0 0 / 0.12)

Drop-shadow shorthand. Falls back through --cngx-shadow-md. inherits: true so the :root dark-mode override (deeper shadow on dark) reaches the floating panel host.

--cngx-select-placeholder-color#<color>
Default value oklch(0 0 0 / 0.5)

Color of the placeholder text shown when no value is selected.

--cngx-select-caret-color#<color>
Default value oklch(0 0 0 / 0.5)

Color of the dropdown caret glyph shared across every variant. Muted by default to mirror the placeholder token; the Material bridge maps it to --mat-sys-on-surface-variant. inherits: true so a trigger-level override reaches the glyph element.

--cngx-select-clear-color#<color>
Default value oklch(0 0 0 / 0.5)

Color of the clear-button glyph shared across every variant. Tracks the same muted default as the caret so both trigger affordances read uniformly. inherits: true so a trigger-level override reaches the glyph element.

--cngx-select-border#*
Default value 1px solid oklch(0.85 0.01 250)

Border shorthand of the trigger.

--cngx-select-bg#<color>
Default value transparent

Background of the trigger.

--cngx-select-color#*
Default value currentColor

Text color of the trigger.

Layout

--cngx-select-panel-radius#*
Default value 0.25rem

Corner radius of the dropdown panel.

--cngx-select-panel-padding#*
Default value 0.25rem

Inner padding of the panel.

--cngx-select-panel-max-height#*
Default value 16rem

Maximum height before vertical scrolling kicks in.

--cngx-select-option-padding#*
Default value 0.375rem 0.5rem

Padding shorthand of each option row.

--cngx-select-option-min-height#*
Default value auto

Minimum height of an option row - defaults to auto so dense rows stay compact.

--cngx-select-option-radius#*
Default value 0.125rem

Corner radius of an option row.

--cngx-select-caret-size#*
Default value 1.25em

Font-size of the dropdown caret glyph shared across every variant.

--cngx-select-chip-gap#*
Default value 0.25rem

Gap between chips inside the trigger chip list.

--cngx-select-chip-wrap-radius#*
Default value 0.25rem

Corner radius of the reorderable chip wrap container.

--cngx-select-chip-wrap-gap#*
Default value 0.25rem

Gap between the chip body and any projected drag handle.

--cngx-select-min-width#*
Default value 10rem

Minimum inline size of the trigger.

--cngx-select-min-height#*
Default value 2.25rem

Minimum block size of the trigger.

--cngx-select-gap#*
Default value 0.5rem

Gap between the label, caret, and clear button.

--cngx-select-padding#*
Default value 0.5rem 0.75rem

Padding shorthand of the trigger.

--cngx-select-radius#*
Default value 0.25rem

Corner radius of the trigger.

State / Highlighted

--cngx-select-option-highlight-bg#<color>
Default value oklch(0.66 0.19 50 / 0.1)

Background of the keyboard-highlighted option row. Defaults to a low-alpha tint of --cngx-color-primary so the highlight reads as brand-accented without competing with selection state.

See: [[--cngx-color-primary]]

State / Selected

--cngx-select-check-color#<color>
Default value oklch(0.66 0.19 50)

Color of the selected-option checkmark glyph. Falls back through --cngx-color-primary.

See: [[--cngx-color-primary]]

State / Disabled

--cngx-select-disabled-opacity#<number>
Default value 0.5

Opacity multiplier applied when the trigger is disabled. Shared family-wide so every variant dims its disabled trigger identically.

State / Loading

--cngx-select-skeleton-gap#*
Default value 0.25rem

Vertical gap between skeleton placeholder rows.

--cngx-select-skeleton-padding#*
Default value 0.25rem

Padding around the skeleton placeholder block.

--cngx-select-skeleton-row-height#*
Default value 1.75rem

Height of each skeleton placeholder row.

--cngx-select-skeleton-row-radius#*
Default value 0.125rem

Corner radius of each skeleton placeholder row.

--cngx-select-spinner-padding#*
Default value 1rem

Padding around the first-load spinner wrapper.

--cngx-select-spinner-size#*
Default value 1.5rem

Diameter of the first-load spinner ring.

--cngx-select-spinner-border#*
Default value 2px solid oklch(0 0 0 / 0.15)

Track stroke of the first-load spinner ring. inherits: true so the :root dark-mode override (alpha-on-white on dark) reaches the spinner inside the floating panel.

--cngx-select-spinner-color#<color>
Default value oklch(0.66 0.19 50)

Indicator stroke of the first-load spinner ring. Falls back to --cngx-color-primary.

--cngx-select-loading-bar-height#<length>
Default value 3px

Height of the first-load loading bar.

--cngx-select-loading-bar-color#<color>
Default value oklch(0.66 0.19 50)

Color of the first-load loading bar. Falls back to --cngx-color-primary.

State / Refreshing

--cngx-select-refreshing-height#<length>
Default value 2px

Height of the subsequent-load refreshing bar.

--cngx-select-refreshing-color#<color>
Default value oklch(0.66 0.19 50)

Color of the refreshing bar gradient. Falls back to --cngx-color-primary.

--cngx-select-refreshing-spinner-padding#*
Default value 0.25rem

Padding around the refreshing spinner wrapper.

--cngx-select-refreshing-dots-gap#*
Default value 0.25rem

Gap between the three refreshing dots.

--cngx-select-refreshing-dots-padding#*
Default value 0.375rem

Padding around the refreshing dots block.

--cngx-select-refreshing-dot-size#*
Default value 0.375rem

Diameter of each refreshing dot.

--cngx-select-refreshing-dot-color#*
Default value currentColor

Color of each refreshing dot.

State / Commit

--cngx-select-option-spinner-size#*
Default value 0.875rem

Diameter of the per-row commit spinner.

--cngx-select-option-spinner-color#<color>
Default value oklch(0.66 0.19 50)

Indicator stroke of the per-row commit spinner.

--cngx-select-option-error-color#<color>
Default value oklch(0.6 0.18 25)

Glyph color of the per-row commit error indicator. Falls back to --cngx-color-danger.

--cngx-select-commit-error-padding#*
Default value 0.375rem 0.5rem

Padding of the commit error banner.

--cngx-select-commit-error-radius#*
Default value 0.125rem

Corner radius of the commit error banner.

State / Overflow

--cngx-select-chip-overflow-badge-bg#<color>
Default value oklch(0 0 0 / 0.08)

Background of the chip overflow badge shown in truncate overflow mode.

--cngx-select-chip-overflow-badge-color#<color>
Default value oklch(0 0 0 / 0.6)

Text color of the chip overflow badge.

State / Remove

--cngx-select-chip-remove-size#*
Default value 1.25rem

Hit-target diameter of the chip remove button inside a reorderable chip wrap.

--cngx-select-chip-remove-hover-bg#<color>
Default value oklch(0 0 0 / 0.12)

Background tint of the chip remove button on hover.

--cngx-select-chip-remove-hover-color#<color>
Default value oklch(0.6 0.18 25)

Foreground color of the chip remove button on hover. Falls back to --cngx-color-danger.

State / Reorder

--cngx-select-chip-handle-color#<color>
Default value oklch(0.5 0.01 250)

Color of the optional projected drag-handle glyph.

--cngx-select-chip-handle-size#*
Default value 0.75rem

Font-size of the optional projected drag-handle glyph.

State / Dragging

--cngx-select-chip-drag-shadow#*
Default value 0 8px 20px oklch(0 0 0 / 0.28)

Drop-shadow of the chip lifted into the dragging state. inherits: true so the :root dark-mode override (deeper shadow on dark) reaches the chip while it's being dragged.

--cngx-select-chip-drag-bg#<color>
Default value oklch(0.66 0.19 50)

Background of the chip lifted into the dragging state. Falls back to --cngx-color-primary.

See: [[--cngx-color-primary]]

--cngx-select-chip-drag-color#<color>
Default value oklch(1 0 0)

Text color of the chip lifted into the dragging state.

--cngx-select-chip-drag-scale#<number>
Default value 1.06

Scale multiplier of the dragging chip - Trello-style lift.

--cngx-select-chip-drag-tilt#<angle>
Default value -1.5deg

Rotation tilt applied to the dragging chip - Trello-style lift.

--cngx-select-chip-drop-bar-width#<length>
Default value 3px

Width of the drop-indicator bar between chips.

--cngx-select-chip-drop-bar-color#<color>
Default value oklch(0.66 0.19 50)

Color of the drop-indicator bar between chips. Falls back to --cngx-color-primary.

State / Error

--cngx-select-error-gap#*
Default value 0.5rem

Gap between the error message and the retry button.

--cngx-select-error-padding#*
Default value 0.5rem 0.75rem

Padding of the panel-wide error block.

--cngx-select-error-color#<color>
Default value oklch(0.6 0.18 25)

Text color of every error surface. Falls back to --cngx-color-danger.

See: [[--cngx-color-danger]]

--cngx-select-error-inline-padding#*
Default value 0.375rem 0.5rem

Padding of the inline error banner shown above the option list.

--cngx-select-error-inline-radius#*
Default value 0.125rem

Corner radius of the inline error banner.

--cngx-select-error-retry-border#*
Default value 1px solid currentColor

Border shorthand of the error retry button.

State / Trigger invalid

--cngx-select-trigger-invalid-border-color#<color>
Default value oklch(0.6 0.18 25)

Border color painted on the trigger wrapper when aria-invalid="true". Defaults track --cngx-color-danger via the delegating :root entry below.

--cngx-select-trigger-invalid-border-width#<length>
Default value 1px

Border width painted on the trigger wrapper when aria-invalid="true".

--cngx-select-trigger-invalid-outline-color#<color>
Default value oklch(0.6 0.18 25)

Outline color layered on the invalid trigger when focus is inside it. Defaults track --cngx-color-danger via the delegating :root entry below.

--cngx-select-trigger-invalid-glow#*
Default value 0 0 0 3px oklch(0.6 0.18 25 / 0.2)

Soft halo layered behind the invalid trigger at focus time. Authored as a box-shadow so it composes with any consumer-supplied focus outline.

State / Focus

--cngx-select-focus-outline#*
Default value 2px solid oklch(0.66 0.19 50)

Focus-ring outline shorthand. Falls back to --cngx-color-primary.

See: [[--cngx-color-primary]]

--cngx-select-focus-offset#<length>
Default value 2px

Outline offset of the focus ring.

Material theme

import { ChangeDetectionStrategy, Component, ViewEncapsulation, signal } from '@angular/core';

import { CngxSelect, type CngxSelectOptionDef } from '@cngx/forms/select';

/**
 * `CngxSelect` rendered against a Material 3 palette via the published
 * `@cngx/themes/material/select-theme` bridge.
 *
 * The stylesheet builds a real M3 theme: `mat.theme` emits the `--mat-sys-*`
 * system tokens, then `select.theme($theme)` routes every `--cngx-select-*`
 * token onto its Material counterpart, so the trigger, panel surface, and
 * selected-option tones inherit the palette with no per-instance overrides.
 * `ViewEncapsulation.None` lets the global `html` theme and the
 * `:where(cngx-select)` bridge rules reach both the trigger and the
 * top-layer panel.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [CngxSelect],
  styleUrl: './material-theme.component.scss',
  template: `
    <div class="demo">
      <cngx-select
        [label]="'Favorite color'"
        [options]="colors"
        [(value)]="value"
        [clearable]="true"
        [selectionIndicatorPosition]="'after'"
        placeholder="Pick a color…"
      />
      <p class="demo__readout">Selected: {{ value() ?? '—' }}</p>
    </div>
  `,
})
export class MaterialThemeExample {
  protected readonly value = signal<string | undefined>(undefined);

  protected readonly colors: CngxSelectOptionDef<string>[] = [
    { value: 'red', label: 'Red' },
    { value: 'green', label: 'Green' },
    { value: 'blue', label: 'Blue' },
    { value: 'amber', label: 'Amber' },
    { value: 'violet', label: 'Violet' },
  ];
}
export { MaterialThemeExample as AppComponent };

Commit action

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Observable } from 'rxjs';

import {
  CngxSelect,
  CngxSelectCommitError,
  type CngxSelectCommitAction,
  type CngxSelectOptionDef,
} from '@cngx/forms/select';

/**
 * CngxSelect commit-action — optimistic / pessimistic with rollback.
 *
 * `[commitAction]` runs an async save operation before the selection
 * actually commits. In `optimistic` mode the option is selected
 * immediately, the panel closes, and an error rolls the value back
 * (with the previously selected option restored). In `pessimistic`
 * mode the panel stays open and a per-row spinner shows on the intended
 * option until the action resolves; the panel only closes on success.
 *
 * Toggle "Simulate error" and the mode buttons to exercise all four
 * quadrants. The `*cngxSelectCommitError` slot template controls the
 * inline error UI inside the panel — by default a banner above the
 * options carries `error.message`.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [CngxSelect, CngxSelectCommitError],
  template: `
    <p style="margin: 0 0 12px; opacity: 0.8; font-size: 0.875rem">
      Pick a status — the commit-action simulates a server save. Tick
      "Simulate error" and watch the rollback (optimistic) or the panel
      staying open with the per-row spinner (pessimistic).
    </p>
    <div
      style="display: flex; gap: 12px; align-items: center; margin-bottom: 12px; flex-wrap: wrap"
    >
      <label>
        <input
          type="checkbox"
          [checked]="shouldFail()"
          (change)="shouldFail.set($any($event.target).checked)"
        />
        Simulate error
      </label>
      <button
        type="button"
        (click)="mode.set('optimistic')"
        [style.fontWeight]="mode() === 'optimistic' ? 'bold' : 'normal'"
      >
        optimistic
      </button>
      <button
        type="button"
        (click)="mode.set('pessimistic')"
        [style.fontWeight]="mode() === 'pessimistic' ? 'bold' : 'normal'"
      >
        pessimistic
      </button>
    </div>

    <cngx-select
      label="Status"
      placeholder="Pick a status…"
      [options]="options"
      [(value)]="value"
      [commitAction]="commitAction"
      [commitMode]="mode()"
    >
      <ng-template cngxSelectCommitError let-error>
        {{ error?.message }}
      </ng-template>
    </cngx-select>

    <p style="margin-top: 12px">Selected: {{ value() ?? '–' }}</p>
  `,
})
export class CommitActionExample {
  protected readonly options: CngxSelectOptionDef<string>[] = [
    { value: 'draft', label: 'Draft' },
    { value: 'review', label: 'In Review' },
    { value: 'approved', label: 'Approved' },
    { value: 'published', label: 'Published' },
  ];

  protected readonly value = signal<string | undefined>(undefined);
  protected readonly mode = signal<'optimistic' | 'pessimistic'>('optimistic');
  protected readonly shouldFail = signal(false);

  protected readonly commitAction: CngxSelectCommitAction<string> = (intended) =>
    new Observable<string | undefined>((sub) => {
      const handle = setTimeout(() => {
        if (this.shouldFail()) {
          sub.error(new Error('Server rejected: ' + intended));
        } else {
          sub.next(intended);
          sub.complete();
        }
      }, 900);
      return () => clearTimeout(handle);
    });
}
export { CommitActionExample as AppComponent };