bigskysoftware / bigskysoftware/htmx
Regression (4.0.0-beta4): form-associated custom element value not sent when it is a standalone (non-form) trigger
- Dominant language
- JavaScript
- Stars
- 49.4k
- Forks
- 1.7k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 30
Description
### Summary
In htmx 4, a **form-associated custom element (FACE)** that is used as a **standalone trigger** (i.e. it has a `name`, is *not* inside a ``, and fires its own request) no longer has its value included in the request. This worked in `4.0.0-beta3` and broke in `4.0.0-beta4` (changelog: *"Improved non-form input and checkbox handling"*), and is still broken in `4.0.0-beta5`.
This silently drops a parameter that used to be sent, which is hard to diagnose (no console error — the server just sees a missing param).
### htmx version
- ✅ Works: `4.0.0-beta3`
- ❌ Broken: `4.0.0-beta4`, `4.0.0-beta5`
### Minimal reproducible example
A standards-compliant form-associated custom element (no framework needed) that exposes `.value` and calls `ElementInternals.setFormValue`, used directly as the htmx trigger:
```html
class MyControl extends HTMLElement {
static formAssociated = true;
#internals = this.attachInternals();
connectedCallback() { this.value = 'hello'; }
get value() { return this.#value; }
set value(v) { this.#value = v; this.#internals.setFormValue(v); }
}
customElements.define('my-control', MyControl);
click me
```
Click the element and inspect the outgoing request body.
- **beta3:** body contains `foo=hello` ✅
- **beta4 / beta5:** body is empty — `foo` is missing ❌
(Our real-world case is a Web Awesome `` company switcher in a page header, outside any form. It stopped sending `tenantId` after upgrading.)
### Expected behavior
The triggering element's own value is included, as documented ("an element that causes a request will include its value if it has one") and as in beta3.
### Actual behavior
The parameter is omitted. No error is raised client-side.
### Root cause
In `4.0.0-beta3`, the non-form branch of `#collectFormData` appended the triggering element's own `name`/`value` directly:
```js
// beta3
if (!form && elt.name) {
if (validate && elt.reportValidity && !elt.reportValidity()) return
formData.append(elt.name, elt.value) // reads the host element's own value
included.add(elt);
}
```
A FACE exposes `.name`/`.value` on the host, so this worked.
In `4.0.0-beta4`/`beta5`, that branch now delegates to `#addInputValues`:
```js
// beta5
if (!form) {
if (validate && elt.reportValidity && !elt.reportValidity()) return
this.#addInputValues(elt, included, formData, isGet);
}
// ...
#addInputValues(elt, included, formData, isGet) {
let tag = elt.tagName;
let inputs = [];
if (tag === 'BUTTON') {
inputs = [elt];
} else if (['INPUT', 'SELECT', 'TEXTAREA', 'FIELDSET'].includes(tag) || !isGet) {
inputs = this.#queryEltAndDescendants(elt, 'input, select, textarea');
}
for (let input of inputs) {
if (!input.name || input.matches(':disabled') || included.has(input)) continue;
// ...append input.name / input.value
}
}
```
For a custom element the `tagName` is e.g. `MY-CONTROL`, which does not match `input, select, textarea`, and a FACE keeps its value in `ElementInternals` rather than a light-DOM ``. So `#queryEltAndDescendants(elt, 'input, select, textarea')` finds nothing and the element's own value is never appended. The host element's `.value` is no longer consulted at all.
Note this is asymmetric: a FACE *inside* a `` still works, because that path uses `new FormData(form)`, which does collect FACE values via `setFormValue`. Only the standalone-trigger path regressed.
### Suggested fix
In the non-form path, fall back to the triggering element's own `name`/`value` when it isn't a native form control but exposes a value (covers form-associated custom elements), e.g. preserve the beta3 behavior for the trigger itself:
```js
if (elt.name && !['INPUT','SELECT','TEXTAREA'].includes(elt.tagName) && !included.has(elt) && elt.value !== undefined) {
formData.append(elt.name, elt.value);
included.add(elt);
}
```
(or otherwise consult `elt.value` / the element's `ElementInternals` form value for the triggering element).
### Environment
- htmx `4.0.0-beta5` (also `beta4`)
- Reproduced in current Chrome/Firefox; not browser-specific (logic is in htmx).
Contributor guide
Research direction
Reproduce the standalone custom-element case from the issue and inspect the non-form path in #collectFormData, especially #addInputValues. Compare beta3 with beta5 and verify that a named form-associated custom element contributes its own value while descendant controls still work. Done means the request contains foo=hello for the standalone trigger without regressing native controls or FACE elements inside forms.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100