Skip to content

fix: escape IDs in restorePreservedElements, selectOOB, and anchor scroll - #3982

Open
n0vdd wants to merge 1 commit into
bigskysoftware:devfrom
n0vdd:fix/css-escape-remaining-selectors
Open

fix: escape IDs in restorePreservedElements, selectOOB, and anchor scroll#3982
n0vdd wants to merge 1 commit into
bigskysoftware:devfrom
n0vdd:fix/css-escape-remaining-selectors

Conversation

@n0vdd

@n0vdd n0vdd commented Aug 27, 2026

Copy link
Copy Markdown

Description

Three remaining call sites build a CSS selector from a raw element id without CSS.escape(), causing failures when the id contains CSS-special characters (., /, :, etc.):

  1. restorePreservedElements (line ~1521) — find('#' + preservedElt.id)querySelector('#' + id). On browsers with moveBefore (Chrome), htmx moves the preserved element into the pantry div during handlePreservedElements, then restorePreservedElements looks it back up by id to put it in place. A dotted id like payment.parcelas becomes the selector #payment.parcelas (id=payment AND class=parcelas), matches nothing, and the next line throws TypeError: Cannot read properties of null (reading 'parentNode'), aborting the swap.

  2. selectOOB handling (line ~1929) — fragment.querySelector('#' + id). Same pattern: the id from hx-select-oob is interpolated raw.

  3. Anchor scroll (line ~2004) — resolveTarget('#' + swapOptions.anchor)find()querySelector(). An anchor id with special chars hits the same parse error.

The sibling call sites in oobSwap (fixed by #3304) and handleAttributes (fixed by #3752) already use CSS.escape() correctly — these three were missed.

Repro (restorePreservedElements — the most visible one)

<div hx-get="/update">
  <input id="payment.amount" hx-preserve value="typed-value">
</div>

On Chrome (moveBefore path): swap replaces the content, handlePreservedElements stashes the input in the pantry via getElementById (works fine — getElementById treats id as opaque), then restorePreservedElements tries querySelector('#payment.amount') → no match → TypeError on .parentNode. The preserved element is stranded in the pantry div and all subsequent swaps on the page may break.

On Firefox/Safari (replaceChild path): handlePreservedElements takes the else branch and calls replaceChild directly — restorePreservedElements is never called, so the bug is latent.

Corresponding issues: the same bug class as #1537 (oobSwap) and the settle-lookup in #3752, applied to the three remaining unescaped sites.

Testing

  • Added handles hx-preserve on elements with dotted IDs to test/attributes/hx-preserve.js
  • Added handles elements with IDs containing dots in hx-select-oob to test/attributes/hx-select-oob.js
  • Full test suite: 849 passed, 0 failed, 3 skipped (Chrome)

Checklist

  • I have read the contribution guidelines
  • I have targeted this PR against the correct branch (dev for source changes)
  • This is a bugfix
  • I ran the test suite locally (npm run test:chrome) and verified that it succeeded

…roll

Three call sites build a CSS selector from a raw element id without
CSS.escape(), causing failures when the id contains CSS-special characters
(dots, slashes, colons, etc.).

- restorePreservedElements: find('#' + preservedElt.id) breaks on the
  moveBefore path (Chrome) with dotted ids — the selector parses as
  id + class, matches nothing, throws TypeError on .parentNode.
- selectOOB: fragment.querySelector('#' + id) — same pattern.
- anchor scroll: resolveTarget('#' + swapOptions.anchor) — same pattern.

The sibling call sites in oobSwap (fixed by bigskysoftware#3304) and handleAttributes
(fixed by bigskysoftware#3752) already use CSS.escape() correctly.
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.

1 participant