Skip to content

fix(material/chips): emit null instead of undefined when deselecting the only selected chip - #33708

Open
CSchulz wants to merge 1 commit into
angular:mainfrom
CSchulz:fix/chip-listbox-null-on-deselect
Open

fix(material/chips): emit null instead of undefined when deselecting the only selected chip#33708
CSchulz wants to merge 1 commit into
angular:mainfrom
CSchulz:fix/chip-listbox-null-on-deselect

Conversation

@CSchulz

@CSchulz CSchulz commented Aug 20, 2026

Copy link
Copy Markdown

Summary

MatChipListbox._propagateChanges() emits undefined as the control value whenever a single-select listbox transitions from one chip selected to none selected (e.g. the user clicks the selected chip again to deselect it). This is inconsistent with:

  • The multi-select branch of the same method, which already emits [] for an empty selection rather than undefined.
  • The rest of Angular's forms ecosystem, where null is the conventional "nothing selected" sentinel (e.g. FormControl's own default value).

More importantly, some consumers assign a different meaning to undefined than to an empty/absent value. We hit this with Angular's new Signal Forms (@angular/forms/signals):
its internal FieldNodeStructure.computeChildrenMap() treats a model property whose value is strictly undefined as "this field no longer exists" and removes its child FieldNode accordingly. When mat-chip-listbox writes undefined into a signal-forms-bound field on deselect, the field's own node gets deleted from the framework's internal map. Any subsequent access (including FormField's own state computed, which does this.field()()) then finds no field there, and the read undefined() throws TypeError: this.field(...) is not a function. This is easy to trigger with a plain double-click (select, then deselect) on a chip bound via [formField].

Since undefined and null are not interchangeable to that kind of consumer, and null is the type-agnostic, form-idiomatic choice (unlike '', which would be wrong for non-string chip values), this PR changes the single-select branch to emit null instead.

Root cause, verified

Confirmed by reading computeChildrenMap() in @angular/forms's signals implementation: it explicitly special-cases childValue === undefined to delete the corresponding child field entry, but has no such handling for null. Reproduced the crash in an isolated minimal Angular 22 app (mat-chip-listbox [formField]="field", single-select, backed by a form() from @angular/forms/signals) and confirmed:

  • Before this fix: a real dblclick() on a chip crashes immediately with TypeError: this.field(...) is not a function; repeating the interaction produced 16 crashes across 6 toggles.
  • After this fix: the same interactions produce zero crashes, and the bound field's value correctly settles to its type's empty value ('' for a string field, via FormField's own value normalization) instead of vanishing.

Test plan

  • Added should propagate null, not undefined, when the selected chip is deselected to chip-listbox.spec.ts, alongside the existing single-selection reactive-forms tests.
  • Ran the full material/chips unit test suite (pnpm test chips) — 244/244 passing.
  • Audited every existing toBeUndefined()/toBe(undefined) assertion in the spec file; none of them exercise the code path this changes (they cover .selected after chip removal, a disabled-chip path that never reaches _propagateChanges(), and the multi-select branch, which already emitted []).
  • Sanity-checked the new test is actually exercised by temporarily breaking its expectation and confirming the suite fails on it, then restored the correct assertion.
  • Grepped the rest of the repo for other consumers of MatChipListbox/ MatChipListboxChange — none exist outside the material/chips package itself, so there's no cross-package fallout.

Fixes #33705

…the only selected chip

MatChipListbox._propagateChanges() emitted `undefined` as the control
value whenever a single-select listbox went from one chip selected to
none selected. Consumers that treat `undefined` and `null` differently
(e.g. Angular Signal Forms, which uses `undefined` to mean "this field
no longer exists" internally) can crash on this transition. Emit `null`
instead, matching the "nothing selected" convention already used
elsewhere in Angular forms (e.g. FormControl's own initial value).

The multi-select branch is unaffected: it already emits `[]` for an
empty selection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signal Forms: [formField] bound to a CVA control (e.g. mat-chip-listbox) crashes with "this.field(...) is not a function" on a native double-click

1 participant