Skip to content

Fix client router intercepting JS-handled forms (chat/comments navigate on send)#151

Merged
vivek7405 merged 2 commits into
mainfrom
fix/router-submit-bubble
May 31, 2026
Merged

Fix client router intercepting JS-handled forms (chat/comments navigate on send)#151
vivek7405 merged 2 commits into
mainfrom
fix/router-submit-bubble

Conversation

@vivek7405

Copy link
Copy Markdown
Collaborator

Summary

Closes #150.

In the demo blog, sending a live chat message sent the message but then scrolled
the page to the top and the just-sent message vanished. Same class of bug for
the live comments thread.

Root cause: the client router's submit listener was on document in CAPTURE
phase (since e59f381, not a recent change), but a component's @submit is
bound per-element and runs at-target, AFTER document capture. So for a
JS-handled form (chat/comments e.preventDefault() and send over WebSocket /
fetch), the router's onSubmit ran first; its if (e.defaultPrevented) return
guard saw false (the component had not run yet), so it intercepted the form:
with no action it fell back to the current URL and navigated/swapped the page.
The message was sent AND the page swapped, scrolling to top and wiping the
WebSocket-driven chat state.

Fix: register submit in the BUBBLE phase (capture: false) so the component's
@submit runs first and its preventDefault makes the guard skip the form,
honoring the documented "forms that preventDefault in @submit are untouched"
contract. Plain progressive-enhancement forms (no @submit) still get
intercepted and SPA-submitted. Click stays capture. Mirrors hotwired/turbo,
which performs the submission in a bubble listener.

Test plan

  • New e2e regression: sending a chat message keeps you on the page and the
    message survives. Verified it FAILS on the reverted (capture) code
    ("the sent chat message must remain visible") and passes with the fix.
  • npm test green (1476)
  • e2e green (51) against the blog in dist mode (dist rebuilt with the fix)
  • Dogfood: blog e2e 51/51 (incl. the chat regression); website / docs /
    ui-website boot 200 in dist mode

Note: the unit env (linkedom) does not model capture/bubble ordering of a
document-level vs element-level submit listener faithfully, so the regression
lives at the e2e layer where real event phases apply.

After merge

Needs a Railway redeploy to reach the live blog (the deployed services auto-deploy
on push to main).

…apture

The client router's submit listener was on document in CAPTURE phase, but a
component's @submit handler is bound per-element and runs at-target, AFTER
document capture. So for a JS-handled form (the blog's live chat and comments
call e.preventDefault() and send over WebSocket/fetch), the router's onSubmit
fired first, its `if (e.defaultPrevented) return` guard saw false (the
component had not run yet), and it intercepted the form: with no action it fell
back to the current URL and navigated/swapped the page. Result: the message was
sent AND the page swapped, scrolling to the top and wiping the WebSocket-driven
chat state, so the just-sent message vanished.

Register submit in the bubble phase (capture false) so the component's @submit
runs first; its preventDefault then makes the guard skip the form, honoring the
documented "forms that preventDefault in @submit are untouched" contract. Plain
progressive-enhancement forms (no @submit) still get intercepted and
SPA-submitted. Click stays capture. Mirrors hotwired/turbo, which does the
submission work in a bubble listener.

Adds an e2e regression: sending a chat message keeps you on the page and the
message survives. Verified it fails on the reverted (capture) code.

Closes #150
@vivek7405 vivek7405 self-assigned this May 31, 2026
…epend the e2e on polling

Self-review fixes. The onSubmit JSDoc still claimed "capture phase so we run
before user @submit handlers", which is exactly backwards after the fix; rewrite
it to describe the bubble ordering that makes the defaultPrevented guard work.
Add a NOTE at the click registration that it stays capture and carries the same
latent @click-preventDefault hijack, deferred to its own change (#153). Replace
the chat e2e's fixed sleep with waitForFunction polling for the sentinel message
so a slow WS broadcast cannot flake it (still times out, and so fails, on the
buggy capture code).

Refs #150
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Self-review

Loop ran two rounds, last clean.

  • Round 1 found: the onSubmit JSDoc still said "capture phase so we run before user @submit handlers" (exactly backwards after the fix); the chat e2e used a fixed sleep that could flake; and click stays capture, carrying the same latent @click-preventDefault hijack. Fixed in c513948: rewrote the JSDoc to describe the bubble ordering, switched the e2e to waitForFunction polling (still times out and fails on the buggy code), and added a NOTE at the click registration deferring that case to its own change, filed as Client router click listener is capture-phase: same @click preventDefault hijack as the submit bug #153.
  • Round 2: clean. Verified the removeEventListener flags match (submit … false in both enable/disable), no other comment still calls submit capture, the click NOTE is factually accurate, and the e2e polling has teeth.

Verified: chat e2e passes with the fix and fails on the reverted (capture) code ("the sent chat message must remain visible"); node suite 1476; e2e 51; website / docs / ui-website boot 200 in dist mode.

Follow-up: #153 tracks moving the click listener to bubble for the same reason (out of scope here; doesn't affect the blog, whose nav is plain <a href>).

@vivek7405 vivek7405 merged commit 7d84355 into main May 31, 2026
5 checks passed
@vivek7405 vivek7405 deleted the fix/router-submit-bubble branch May 31, 2026 18:53
vivek7405 added a commit that referenced this pull request May 31, 2026
* chore: release @webjsdev/core 0.7.3 and @webjsdev/server 0.8.3

Release debt accumulated since the 0.7.2 / 0.8.2 release (#149):

core 0.7.3 (patch): the two client-router fixes from #151 and #157
(JS-handled forms and links were hijacked by the router despite
e.preventDefault).

server 0.8.3 (patch): #161 (modulepreload hints no longer point at
server-only or template-embedded files the auth gate 404s) and #156
(webjs check no longer leaks a git env var across worktrees).

Both stay in a single minor line; all in-repo dependents pin ^0.7.0 /
^0.8.0, so no dependent range edits are needed. Lockfile regenerated.

* chore: release @webjsdev/cli 0.10.0 (webjs vendor surface)

cli 0.9.1 shipped without the `webjs vendor` command surface that
#105 and #89 added (pin / unpin / list / audit / outdated / update,
the --from multi-CDN selector, and --download caching). That is
feature-level work, so a minor bump.

create-webjs and webjsdev are thin shims that delegate to cli; widen
their `@webjsdev/cli` range from ^0.9.0 to ^0.10.0 so the workspace
keeps linking the local cli (a minor bump falls outside the old caret).
They are not in the changelog auto-publish system, so they carry no
changelog entry of their own. Lockfile regenerated.
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.

Client router intercepts JS-handled forms (@submit preventDefault), navigating away

1 participant