Skip to main content
cngx-src documentation

CngxMultiChipGroup

cngx-multi-chip-group

ComponentPrimaryOnPushNo encapsulationv0.1.0WCAG AA

projects/common/interactive/multi-chip-group/multi-chip-group.component.ts

Import#

import { CngxMultiChipGroup } from '@cngx/common/interactive'

Description#

Multi-select chip group. Owns a selectedValues = model<T[]>([]) (the canonical multi-value source) and exposes the parent contract via CNGX_CHIP_GROUP_HOST. Behaves as a role="listbox" aria-multiselectable="true" with CngxRovingTabindex-driven keyboard navigation; consumers project <cngx-chip cngxChipInGroup> children that derive their own aria-selected from this group's isSelected(value).

Mode is static per feedback_select_family_split: this is the multi-select half of the chip-group split.

Internals lean on createSelectionController from @cngx/core/utils for membership tracking - the controller's structural-equality selected snapshot prevents downstream computed cascades from thrashing when a re-emission produces the same logical contents with a different array reference. Mirrors CngxCheckboxGroup's controller wiring.

value is a structural alias of selectedValues so the group satisfies CngxControlValue<T[]> without owning two synchronised models - both names point to the same ModelSignal<T[]> instance.

[keyFn] lets consumers with object-typed values map each value to a stable membership key (typically (v) => v.id). Without it, membership uses identity / primitive equality.

[state] accepts CngxAsyncState<unknown> for async-loaded chip lists; reactively drives aria-busy. Slot directives for skeleton/empty/error are deferred - the harmonized Phase 3-4 group surface ships only the aria-busy projection of [state], and chip-group follows that precedent for cross-family consistency (plan deviation 2026-05-01: see CngxChipGroup JSDoc).

Roving strategy (accepted-debt §5). This molecule uses CngxRovingTabindex (per the harmonized Phase 3-4 group precedent), NOT createChipStripRoving. The strip-roving controller would solve clamp-on-removal automatically but its tabindex contract requires the parent to bind [attr.tabindex] per chip wrapper at the template level - which a content-projected CngxChipInGroup leaf model does not allow. Tracked in form-primitives-accepted-debt §5; UX consequence is a one-shot focus reset to index 0 after mid-strip removal, instead of clamping to the next valid sibling.

Removal flow (plan deviation). Plan revision claimed createChipRemovalHandler from @cngx/forms/select would drive the remove path. That import is a layer violation (@cngx/common/interactive is Level 2; @cngx/forms/select is Level 3). Removal here is a thin controller.deselect(value) - the WeakMap-closure-cache machinery in the select-family handler targets ngTemplateOutlet chip-slot stability, which content- projected chip-in-group leaves do not exhibit.

Per Pillar 1 (Ableitung statt Verwaltung), aria-busy is a computed() from state.status(). Per Pillar 3 (Komposition statt Konfiguration), the group composes CngxRovingTabindex and emits no implicit children - consumers project chip rows themselves.

<cngx-multi-chip-group label="Tags" [(selectedValues)]="tags">
  @for (tag of options(); track tag) {
    <cngx-chip cngxChipInGroup [value]="tag">{{ tag }}</cngx-chip>
  }
</cngx-multi-chip-group>

https://cngxjs.github.io/cngx/examples/#/common/interactive/chip/multi-group/multi-select-chips-with-selection-count https://cngxjs.github.io/cngx/examples/#/forms/field/form-primitives/coming-in-a-follow-up https://cngxjs.github.io/cngx/examples/#/forms/field/form-primitives/reactive-forms-same-atom-just-bind-formcontrol https://cngxjs.github.io/cngx/examples/#/forms/field/form-primitives/signal-forms-drop-the-atom-into-cngx-form-field

Metadata#

Host#

Providers#

CNGX_CHIP_GROUP_HOST
useExisting CngxMultiChipGroup
CNGX_CONTROL_VALUE
useExisting CngxMultiChipGroup
CNGX_FORM_FIELD_CONTROL
useExisting CngxMultiChipGroup

Relationships

Index#

Inputs#

disabled#boolean
model()
default false
errorMessageId#string | null
input()

Optional id of an external error message element. When set, the host emits aria-errormessage="<id>"; consumers MUST render an element with that id. Default null skips the attribute. Note: WAI-ARIA dictates that AT ignores this attribute when aria-invalid is absent or "false", so a stable always-emitted id is harmless when the field is valid.

default null
invalid#boolean
model()

Bridge-writable invalid state. model<boolean> mirrors disabled so external integrations (RF/Signal-Forms bridges, custom validity adapters) can drive it without a parallel API path - consumers typically read only.

default false
keyFn#(value: T) => unknown
default (v) => v
label#string | undefined
input()
orientation#"horizontal" | "vertical"
input()
default 'horizontal'
required#boolean
model()
default false
selectedValues#T[]
model()
default []

Outputs#

disabled#boolean
model()
invalid#boolean
model()

Bridge-writable invalid state. model<boolean> mirrors disabled so external integrations (RF/Signal-Forms bridges, custom validity adapters) can drive it without a parallel API path - consumers typically read only.

required#boolean
model()
selectedValues#T[]
model()

Instance Properties#

focused#unknown
Readonly
this.focusedState.asReadonly()
id#unknown
signal()Readonly
signal(nextUid('cngx-multi-chip-group-')).asReadonly()
isDisabled#unknown
Readonly

CngxChipGroupHost - leaf-side cascade source.

this.disabled
selectedCount#unknown
Readonly

Public membership count - useful for label hints + announcements.

this.controller.selectedCount
value#unknown
Readonly
this.selectedValues

Methods#

handleFocusIn#void
Protected
handleFocusOut#void
Protected
isSelected#boolean
isSelected(value: T)
@paramvalueT
remove#void
remove(value: T)
@paramvalueT
toggle#void
toggle(value: T)
@paramvalueT

HostBindings#

BindingExpression
[attr.id]id()
[attr.aria-label]label()
[attr.aria-disabled]disabled() ? "true" : null
[attr.aria-required]required() ? "true" : null
[attr.aria-invalid](invalid() || errorState()) ? "true" : null
[attr.aria-errormessage]errorMessageId()
[attr.aria-busy]ariaBusy() ? "true" : null
[class.cngx-multi-chip-group--horizontal]orientation() === "horizontal"

HostListeners#

EventHandler
(focusin)focusin()
(focusout)focusout()

Default visuals for CngxMultiChipGroup. The host carries .cngx-multi-chip-group and frames a flex layout for projected <cngx-chip> children in multi-select mode. Stacks vertically by default. Gap cascades through the single-select sibling container so spacing stays in sync across both group types unless a consumer overrides one explicitly.

Variants

  • --horizontal - flips to a row with flex-wrap: wrap so chips fold to additional lines instead of overflowing

Inheritance

Gap chains through the sibling group then the foundation:

  • --cngx-multi-chip-group-gap -> --cngx-chip-group-gap -> --cngx-space-sm

Layout

--cngx-multi-chip-group-gap#*
Default value 0.5rem

Gap between chip children. Cascades through the single-select group's --cngx-chip-group-gap and then --cngx-space-sm.