Skip to main content
cngx-src documentation

CngxChipInput

DirectivePrimaryv0.1.0WCAG AA

projects/common/interactive/chip-input/chip-input.directive.ts

Import#

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

Description#

Tokenizing input that turns user-entered strings into emitted tokenCreated events on separator keys (default: comma + Enter) and on multi-token paste. Pairs naturally with <cngx-multi-chip-group> or any consumer-managed chip list.

Validation slot - async-state producer. The directive provides CNGX_STATEFUL via useExisting, exposing a ManualAsyncState<string> slot driven by [validateToken] invocations. Bridge directives (<cngx-toast-on />, <cngx-banner-on />) auto-discover this slot through the CNGX_STATEFUL token without an explicit [state] input wiring.

State machine: idle → pending → success | error per separator-key invocation. Concurrent validations supersede via a monotonic validationId - when a second token is entered before the first resolves, the first's resolution is dropped and only the latest outcome touches the slot. Mirrors createCommitController's supersede contract.

Removal contract. (tokenRemoved) fires on Backspace at empty input - semantically "user wants to remove the last chip". The directive does not own the chip list; the consumer drops the last entry from their state in response.

Duplicates. [existingTokens] is read on every separator-key invocation; when allowDuplicates is false (default), a token already present in the list is silently dropped. The directive does not track a private "previously emitted" set - that would be an unbounded memory leak. Consumers pass their canonical chip list as [existingTokens].

Paste. Pasting text containing one or more single-character separators (e.g. "red, green, blue") emits one tokenCreated per non-empty fragment. Multi-char separators ('Enter') are keyboard-only and ignored by the paste path. Single-token paste (no embedded separator) is left to the default paste behaviour so the input value populates normally and the user can decide.

<input
  cngxChipInput
  placeholder="Type a tag and press Enter"
  [existingTokens]="tags()"
  (tokenCreated)="addTag($event)"
  (tokenRemoved)="popTag()"
  [validateToken]="validateTag"
/>
<cngx-toast-on />

https://cngxjs.github.io/cngx/examples/#/common/interactive/chip/input/async-validation-with-supersede-semantics https://cngxjs.github.io/cngx/examples/#/common/interactive/chip/input/synchronous-tokenization

Metadata#

Host#

Providers#

CNGX_STATEFUL
useExisting CngxChipInput

Relationships

Index#

Inputs#

allowDuplicates#boolean
input()
default false
existingTokens#readonly string[]
input()
default []
separators#readonly string[]
input()
default [',', 'Enter']
trimWhitespace#boolean
input()
default true

Outputs#

tokenCreated#string
output()
tokenRemoved#void
output()
validationError#unknown
output()

Instance Properties#

Readonly

Validation slot - CngxStateful<string> contract surface. The view is built from raw signals via buildAsyncStateView rather than createManualState (which lives in @cngx/common/data) because @cngx/common/data → @cngx/common/interactive already exists via CngxSearch, and re-importing data from interactive would create a circular package dependency. The 30-LOC inline is acceptable scope-wise; a future refactor relocating createManualState to @cngx/core/utils would let this collapse to a single call.

buildAsyncStateView<string>({
  status: this.statusState.asReadonly(),
  data: this.dataState.asReadonly(),
  error: this.errorState.asReadonly(),
  progress: signal<number | undefined>(undefined).asReadonly(),
  isFirstLoad: computed(() => !this.hadSuccess()),
  lastUpdated: signal<Date | undefined>(undefined).asReadonly(),
})
validateToken#unknown
Readonly
input<
  ((value: string) => Promise<string> | Observable<string>) | undefined
>(undefined)

Methods#

handleKeydown#void
Protected
handleKeydown(event: KeyboardEvent)
@parameventKeyboardEvent
handlePaste#void
Protected
handlePaste(event: ClipboardEvent)
@parameventClipboardEvent

HostBindings#

BindingExpression
[attr.aria-busy]isPending() ? "true" : null
[attr.aria-invalid]isInvalid() ? "true" : null

HostListeners#

EventHandler
(keydown)keydown()
(paste)paste()