Skip to content

fix: [#2389] form-associated element's form attribute wins over an ancestor <form> - #2392

Open
marciomazza wants to merge 1 commit into
capricorn86:masterfrom
marciomazza:fix/form-attribute-precedence
Open

fix: [#2389] form-associated element's form attribute wins over an ancestor <form>#2392
marciomazza wants to merge 1 commit into
capricorn86:masterfrom
marciomazza:fix/form-attribute-precedence

Conversation

@marciomazza

Copy link
Copy Markdown

Description

Resolves #2389
Refs #2371

A form control carrying form="ownerId" is owned by that <form>, even when it sits
inside another <form>. happy-dom returned the ancestor <form> instead, so
control.form and form.elements disagreed with the browser. A form attribute
pointing at a missing id or a non-<form> element also leaked the wrong node.

const ancestor = document.createElement('form');
const owner = document.createElement('form');
owner.id = 'owner';
const input = document.createElement('input');
input.setAttribute('form', 'owner');
ancestor.appendChild(input);
document.body.append(ancestor, owner);

input.form === owner;                    // happy-dom: false — real browsers: true
[...ancestor.elements].includes(input);   // happy-dom: true  — real browsers: false

Root cause: the seven built-in get form() getters checked the cached ancestor
PropertySymbol.formNode first and only fell back to the form attribute; the
attribute lookup used getElementById with no tag check. formNode can't just be
made attribute-aware — Node.ts propagates it to descendants, and a
<fieldset form="B"> has children.

The fix: a shared HTMLFormControlElementUtility.getFormOwner() — attribute present →
resolve by id within the element's root (connected or not), return it only if it's a
<form>, else null; attribute absent → the cached ancestor formNode. It now backs
the seven get form() getters, radio-group scoping in HTMLInputElement,
ValidityState, and HTMLFormElement[getFormControlItems] (which powers
form.elements / namedItem / reset / validation / FormData). formNode keeps
meaning "nearest ancestor <form>" only.

Shares the new helper and the getFormControlItems change with #2371 — whichever
merges first, the other drops the duplicate.

Tests

Added 6 HTMLInputElementget form() tests: attribute beats ancestor form;
missing-id and non-<form> target both give null; attribute pointing at the
ancestor itself still resolves; resolves in a disconnected tree; target in a different
tree gives null; owner re-resolves as the attribute is added / changed / removed.
Full npm test passes.

Verification

Cross-checked in Chrome 152: input.form follows the attribute to the owner <form>,
ancestor.elements excludes it, owner.elements includes it once, and form="missing"
/ form="<div id>" both give null.

Before submitting the PR, please make sure you do the following:

  • Read the contributing guidelines.
  • It's really useful if your PR references an issue where it is discussed ahead of time.
  • Please check Allow edits by maintainers to make review process faster. Note that this option is not available for repositories that are owned by Github organizations.

Tests

  • Make sure to add tests for your changes. Run your test in a real browser to make sure that the test tests what a real browser would do (e.g. by running the code in the browser console).
    • Ran the repro in Chrome 152: input.form follows the form attribute to the owner <form>; ancestor.elements excludes the input, owner.elements includes it once; form="missing" and form="<div id>" both yield null.
  • Run the tests with npm test locally to make sure that all tests pass before submitting the PR.

Title

  • The title of the pull request should be in the format of "type: [#issue] description". The type can be feat, fix, chore or BREAKING CHANGE. The issue is optional and can be omitted if the pull request does not relate to an issue.
  • The title should be concise and descriptive. The title will be used when generating release notes. Make sure that the title is easily understood by users of the library.

AI

I used Claude Code to write this, over multiple iterations. I guided and reviewed
it myself at every step.

…orm>

Per the WHATWG form-owner algorithm, a present "form" content attribute is
authoritative: the owner is the referenced <form>, or null if the id resolves
to nothing or to a non-form element. The ancestor <form> is only consulted when
the attribute is absent. happy-dom checked the ancestor first.

Adds HTMLFormControlElementUtility.getFormOwner() as the single resolver and
routes the 7 built-in get form() getters, radio-group scoping, ValidityState
and HTMLFormElement.getFormControlItems() through it. PropertySymbol.formNode
keeps its meaning (nearest ancestor <form> only) since it propagates down the
tree; the attribute is resolved per-element on read, so no cache invalidation
is needed when it changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

form.elements and control.form ignore the form attribute when the control is inside another <form>

1 participant