Skip to content

Add DOMTokenList-style list value type#893

Open
myabc wants to merge 3 commits into
hotwired:mainfrom
myabc:feature/list-value-type
Open

Add DOMTokenList-style list value type#893
myabc wants to merge 3 commits into
hotwired:mainfrom
myabc:feature/list-value-type

Conversation

@myabc

@myabc myabc commented Jul 4, 2026

Copy link
Copy Markdown

Closes #892.

This adds a list value type that stores an array of string tokens as a space-separated attribute, mirroring how the platform encodes class, rel, and other DOMTokenList-backed attributes — an HTML-native alternative to the JSON-encoded Array type for simple token lists.

export default class extends Controller {
  static values = { permittedTypes: DOMTokenList }
}
<div data-controller="upload" data-upload-permitted-types-value="image video audio"></div>

this.permittedTypesValue returns ["image", "video", "audio"].

Design notes

Declared via the DOMTokenList constructor, 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. parseValueTypeConstant guards with typeof DOMTokenList !== "undefined" so non-DOM environments neither throw a ReferenceError nor falsely match an undefined constant.

Reading returns a plain string[] snapshot, not a live token-list object. Tokens are split on whitespace and deduplicated keeping the first occurrence, matching DOMTokenList'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 TypeError for non-arrays and for tokens that are empty or contain whitespace, since those cannot round-trip through a space-delimited attribute (analogous to DOMTokenList's InvalidCharacterError). The error message points to the Array type 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 set DOMTokenList uses (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: invokeChangedCallback skipped 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 now rawOldValue !== undefined; as a side effect, a raw "" old value now decodes per type (booleans/numbers get true/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.md is updated.

myabc added 2 commits July 4, 2026 17:55
`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
Copilot AI review requested due to automatic review settings July 4, 2026 16:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via DOMTokenList, with whitespace-splitting + deduping on read and token validation + space-joining on write.
  • Fix invokeChangedCallback to decode falsy-but-present old raw values (notably "") by checking rawOldValue !== 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.

Comment thread docs/reference/values.md Outdated
Comment thread docs/reference/values.md
Comment thread docs/reference/values.md
Comment on lines +316 to +320
if (!Array.isArray(value)) {
throw new TypeError(
`expected value of type "list" but instead got value "${value}" of type "${parseValueTypeDefault(value)}"`
)
}

@myabc myabc Jul 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@myabc

myabc commented Jul 4, 2026

Copy link
Copy Markdown
Author

N.B. I'm not 100% sold on the naming - ofc there is also the discrepancy between the type that is declared (DOMTokenList) and the actual runtime type (an Array).

I've ended up implementing a function in a downstream project to provide full DOMTokenList semantics, but I'm pretty sure this is too heavyweight / over-engineered for this use case:

https://github.com/opf/openproject/blob/dev/frontend/src/app/shared/helpers/dom-helpers.ts#L166

@myabc
myabc force-pushed the feature/list-value-type branch from c1fa853 to 42a0e02 Compare July 4, 2026 17:05
@myabc myabc changed the title Add DOMTokenList-style list value type Add DOMTokenList-style list value type Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support Arrays values in DOMTokenList-style

2 participants