Add DOMTokenList-style list value type#893
Conversation
`invokeChangedCallback` skipped decoding falsy raw old values,
leaking the raw string `""` to changed callbacks instead of the
decoded value. No built-in type could hit this before: the written
defaults (`"0"`, `"false"`, `"[]"`, `"{}"`) are all truthy strings.
The new list type writes its default `[]` as `""`, so the first
changed callback after connect would receive `""` instead of `[]`.
Check for `undefined` instead. A raw `""` old value now decodes
like any other value, so booleans and numbers yield `true`/`0`,
matching what the getter returns.
Declare a value with the `DOMTokenList` constructor to store an
array of string tokens as a space-separated attribute, mirroring
how the platform encodes `class` and `rel`:
static values = { permittedTypes: DOMTokenList }
<div data-upload-permitted-types-value="image video audio">
Reading returns a deduplicated `string[]` snapshot (first occurrence
wins, like DOMTokenList's ordered-set parsing). Writing joins tokens
with single spaces and throws a `TypeError` for non-arrays and for
empty or whitespace-containing tokens. Array defaults are accepted
and validated at declaration time.
Closes hotwired#892
There was a problem hiding this comment.
Pull request overview
This PR adds a new Stimulus Values type that encodes/decodes token arrays using a space-separated attribute format (DOMTokenList-style), and fixes a related changed-callback decoding edge case that surfaces when a value’s encoded form is an empty string.
Changes:
- Add a new
"list"value type declared viaDOMTokenList, with whitespace-splitting + deduping on read and token validation + space-joining on write. - Fix
invokeChangedCallbackto decode falsy-but-present old raw values (notably"") by checkingrawOldValue !== undefined. - Add comprehensive tests for list values/defaults and for changed-callback behavior with
""old values; update Values reference docs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/core/value_properties.ts | Adds the list value type constant mapping, default, reader, and writer + default validation for list-typed defaults. |
| src/core/value_observer.ts | Fixes changed-callback old-value decoding for "" by switching to an undefined check. |
| src/tests/controllers/value_controller.ts | Declares a tokens: DOMTokenList value and captures tokensValueChanged callback values for assertions. |
| src/tests/controllers/default_value_controller.ts | Adds list-typed default values (including filled and override scenarios) for default-value behavior tests. |
| src/tests/modules/core/value_tests.ts | Adds list value behavior tests and regressions for changed callbacks when old raw value is "". |
| src/tests/modules/core/value_properties_tests.ts | Adds coverage for DOMTokenList parsing/type resolution and default validation for list tokens. |
| src/tests/modules/core/default_value_tests.ts | Adds list default-value tests (custom defaults, overriding via attribute, and setter behavior). |
| docs/reference/values.md | Documents the new list value behavior and defaults in the Values reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!Array.isArray(value)) { | ||
| throw new TypeError( | ||
| `expected value of type "list" but instead got value "${value}" of type "${parseValueTypeDefault(value)}"` | ||
| ) | ||
| } |
There was a problem hiding this comment.
Addressing this verbatim would lead to inconsistent behaviour: the existing array and object readers build their messages with the same parseValueTypeDefault pattern and produce the identical type "undefined" wording for null today. If this is modified it should be be changed for all three!
|
N.B. I'm not 100% sold on the naming - ofc there is also the discrepancy between the type that is declared ( I've ended up implementing a function in a downstream project to provide full https://github.com/opf/openproject/blob/dev/frontend/src/app/shared/helpers/dom-helpers.ts#L166 |
c1fa853 to
42a0e02
Compare
DOMTokenList-style list value type
Closes #892.
This adds a
listvalue type that stores an array of string tokens as a space-separated attribute, mirroring how the platform encodesclass,rel, and otherDOMTokenList-backed attributes — an HTML-native alternative to the JSON-encodedArraytype for simple token lists.this.permittedTypesValuereturns["image", "video", "audio"].Design notes
Declared via the
DOMTokenListconstructor, following the existing constructor-based pattern (Array,Boolean, …). The issue originally sketched a'List'string literal, but a plain string type definition already means "string value with this default", so a string keyword would be ambiguous or breaking.parseValueTypeConstantguards withtypeof DOMTokenList !== "undefined"so non-DOM environments neither throw aReferenceErrornor falsely match anundefinedconstant.Reading returns a plain
string[]snapshot, not a live token-list object. Tokens are split on whitespace and deduplicated keeping the first occurrence, matchingDOMTokenList's ordered-set parsing ("a b a"→["a", "b"]). A missing attribute yields the default ([]unless a custom default is declared); an empty or whitespace-only attribute yields[].Writing joins tokens with single spaces and throws a
TypeErrorfor non-arrays and for tokens that are empty or contain whitespace, since those cannot round-trip through a space-delimited attribute (analogous toDOMTokenList'sInvalidCharacterError). The error message points to theArraytype for arbitrary strings. Array defaults are accepted for list-typed values and their tokens are validated at declaration time, so an invalid default fails fast with a clear error instead of detonating at connect time.Custom defaults are returned verbatim (a default of
["a", "a"]reads as-is until a round-trip through the attribute dedupes it), consistent with how other types return their declared defaults.Token splitting uses JS
\s, which is slightly broader than the ASCII whitespace setDOMTokenListuses (e.g. it includes U+00A0). Reader and writer use the same class, so any token the writer accepts round-trips exactly.Bug fix
The first commit fixes a latent bug this type surfaces:
invokeChangedCallbackskipped decoding falsy raw old values, so changed callbacks would leak the raw string""instead of the decoded value. No built-in type could hit this before — their written defaults ("0","false","[]","{}") are all truthy strings — but list writes its default[]as"". The check is nowrawOldValue !== undefined; as a side effect, a raw""old value now decodes per type (booleans/numbers gettrue/0, matching what the getter returns), covered by a regression test.Tests cover round-tripping, dedupe, whitespace tolerance, defaults,
has*Value, changed callbacks, and all error paths;docs/reference/values.mdis updated.