bigskysoftware / bigskysoftware/htmx

Regression (4.0.0-beta4): form-associated custom element value not sent when it is a standalone (non-form) trigger

Open
#3,871 3 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.