Skip to content

Stop nesting form elements in the checkout address step - #1099

Merged
tblivet merged 2 commits into
PrestaShop:2.xfrom
boo-code:fix/checkout-address-form-nesting-36563
Sep 22, 2026
Merged

tblivet merged 2 commits into
PrestaShop:2.xfrom
boo-code:fix/checkout-address-form-nesting-36563

Conversation

@boo-code

@boo-code boo-code commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor
Questions Answers
Description? The checkout address step opens a <form> around the whole block and then renders full address <form> elements inside it. HTML forbids nesting, and the parser resolves it by ignoring the inner start tag and letting the inner </form> close the OUTER form - so every control rendered after an address form loses its form owner. This attaches the step's own controls (address selector radios and the Continue button) to the step form through the form attribute and leaves that form element empty so the address forms are siblings rather than children.
Type? bug fix
BC breaks? no
Deprecations? no
Fixed ticket? PrestaShop/PrestaShop#36563
Sponsor company -
How to test? On a 9.2 shop with this theme active: reach checkout with two saved addresses, click "Billing address differs from shipping address", then "Edit" on the delivery address. Before this change the Continue button and both invoice radios have no form owner (document.querySelector('button[name=confirm-addresses]').form is null), so Continue does nothing. After, they belong to the step form.

Measured on 9.2.0 with this theme active, counting <form> tags in the address step of the served HTML and
reading form ownership from the parsed DOM:

state                                       before         after
guest, 0 addresses, delivery form       4 tags depth 2       -
1 address,  editAddress=delivery        6 tags depth 2   6 tags depth 1
2 addresses, editAddress=delivery       4 tags depth 2   4 tags depth 1
selectors only                                  -        2 tags depth 1
newAddress=delivery                             -        4 tags depth 1

Form ownership in the two-address state, before -> after:

button[name=confirm-addresses]   null -> the step form
input[name=id_address_invoice]   null -> the step form   (both radios)

Tests

src/js/form-validation.test.ts - 3 cases for initFormValidation, which had none: a submit button nested in the form, a link marked
as the submit control, and a form with no submit button. The link case is the one a form.elements lookup would
miss, and it fails if the lookup is switched to it. Full jest suite 42/42, eslint clean.

Why the form attribute rather than moving the markup

The step form is needed for the address selectors and the Continue button; the address form is needed for
the address fields. They are never the same submission, but they interleave: a delivery selector can be
followed by an invoice form, so no arrangement of open/close tags puts every selector in the same form as
the Continue button. Explicit form ownership is the platform feature for exactly that, and it keeps source
order, markup and CSS unchanged.

Companion

PrestaShop/classic-theme needs the same change (branch fix/checkout-address-form-nesting-36563, base
develop), plus replacing the extra <form> its checkout/_partials/address-form.tpl opens around the
Continue button with a <div> - hummingbird already does that.

The address step wrapped the whole block in a <form>, and rendered full address
<form> elements inside it. The HTML parser resolves that by dropping the inner
start tag and letting the inner </form> close the outer form, so every control
after the address form loses its form owner. Measured on 9.2.0: with two saved
addresses, a separate invoice address and the delivery address open for editing,
the Continue button, both invoice radios and the not-valid-addresses input all
report form === null.

Attach those controls to the step form through the form attribute instead, and
leave the form element itself empty so the address forms stay siblings. Form
validation now looks the submit button up in form.elements, which covers both
DOM containment and the form attribute.
@tblivet
tblivet marked this pull request as ready for review September 16, 2026 14:25
@github-project-automation github-project-automation Bot moved this to Ready for review in PR Dashboard Sep 16, 2026
tblivet
tblivet previously approved these changes Sep 16, 2026

@tblivet tblivet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi @boo-code, and thank you !

Two non-blocking remarks:

  • form.elements trades a documented capability for one nothing uses. It only contains listed form controls, so an <a data-ps-action="form-validation-submit"> or an <input type="image"> is never found, where querySelector matched any descendant and CONTEXT.md documents data-ps-action as "used on buttons/links". Nothing breaks today (all 9 marked buttons are real <button>s sitting inside their form), but a marked link would silently stop being wired and the form would submit unvalidated, with no error.

    In exchange, the new capability finding a button attached through form= isn't used anywhere: the buttons carrying the marker and the ones carrying form= are two disjoint sets. Marking the Continue button wouldn't help either, since was-validated would land on the empty #checkout-addresses-form and every Bootstrap rule is a descendant selector (.was-validated .form-control:invalid).

    So i would advice to revert to querySelector and keep the test file initFormValidation had no coverage at all before, and cases 1 and 3 pass either way.

  • form= on #not-valid-addresses does nothing the input has no name, so it's never submitted either way. Core only reads it with .val().

@ps-jarvis ps-jarvis added the Waiting for QA Status: Action required, Waiting for test feedback label Sep 16, 2026
@tblivet tblivet added this to the v2.1.1 milestone Sep 16, 2026
@ps-jarvis ps-jarvis moved this from Ready for review to To be tested in PR Dashboard Sep 16, 2026
form.elements only lists form controls, so a link marked with
data-ps-action="form-validation-submit" was no longer wired, and nothing
used the form-attribute case it added. The test that pinned that case now
pins the link instead. Also drop the form attribute from the
not-valid-addresses input, which has no name and is only read by script.
@boo-code

Copy link
Copy Markdown
Contributor Author

@tblivet Both applied: initFormValidation is back to querySelector, and the form attribute on #not-valid-addresses is gone. The test file stays, with the form-attribute case replaced by a link marked data-ps-action="form-validation-submit", the case form.elements would have dropped; switching the lookup to form.elements fails exactly that one.

@tblivet tblivet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you @boo-code !

@tblivet

tblivet commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🟢 Approved: the bug reproduces on the code this branched from, and is gone on this branch.

AI-assisted QA: an agent drove a real browser through the steps below and drafted this comment. Worth a sanity check.

Tested on PrestaShop 9.3.0, PHP 8.1.33, hummingbird 2.1.0, signed in with two saved addresses, "Billing address differs from shipping address" selected, then the delivery address opened for editing.

before (7c7e145) after (9f9bdd5)
<form> tags in the served address step 2, nested 2 deep 2, nested 1 deep
Controls with no form owner the Continue button and both invoice radios none
Pressing Continue stays on the address step moves to Shipping Method

The screenshots show the same moment in both states: pressing Continue does nothing before, and reaches the Shipping Method step after.

Also checked in both states and unchanged: the checkout, login and contact pages still render, the smoke pages still answer, and the front page, login and cart show no sideways scrolling at 375 and 768 wide.

Before (locked at this step):
09-press-continue-and-see-whether-the-checkout-move

After (ended to the shipping step):
09-press-continue-and-see-whether-the-checkout-move

@tblivet tblivet added QA with AI ✓ AI-assisted QA and removed Waiting for QA Status: Action required, Waiting for test feedback labels Sep 22, 2026
@tblivet
tblivet merged commit 0998a99 into PrestaShop:2.x Sep 22, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from To be tested to Merged in PR Dashboard Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA with AI ✓ AI-assisted QA

Projects

Status: Merged

Development

Successfully merging this pull request may close these issues.

3 participants