Skip to content

fix: lvt-form:action routing, lvt-nav:no-intercept, unreserve action field#45

Merged
adnaan merged 2 commits intomainfrom
action-fix
Apr 4, 2026
Merged

fix: lvt-form:action routing, lvt-nav:no-intercept, unreserve action field#45
adnaan merged 2 commits intomainfrom
action-fix

Conversation

@adnaan
Copy link
Copy Markdown
Contributor

@adnaan adnaan commented Apr 4, 2026

Summary

Addresses 4 feedback items from the Phase 1A attribute reduction:

1. action field name conflict (data loss fix)

action was a reserved routing field excluded from ActionData. This silently dropped any legitimate form field named action — common in e-commerce checkouts, workflow forms, and multi-step wizards. Now action flows through as normal form data.

2. lvt-form:action — explicit routing attribute

New lvt-form:action attribute on <form> elements for explicit action routing (highest priority in resolution order). Replaces the old implicit action field reservation.

Action resolution order (updated):

  1. lvt-form:action attribute on form element (NEW)
  2. lvt-action hidden field (legacy progressive enhancement)
  3. Button name routing: submitter button's name
  4. Form name attribute
  5. Empty → defaults to "submit"

3. lvt-nav:no-intercept — semantic link interception

lvt-form:no-intercept on <a> tags was semantically wrong. Now:

  • lvt-form:no-intercept → forms only
  • lvt-nav:no-intercept → links only

4. lvt-key clarification

lvt-key stays as-is (not lvt-mod:key): it's an event filter (determines whether an event fires), not a timing modifier like debounce/throttle.

Changes

  • dom/event-delegation.ts: Add lvt-form:action check, stop excluding action from data
  • dom/link-interceptor.ts: lvt-no-interceptlvt-nav:no-intercept
  • state/change-auto-wirer.ts: lvt-no-interceptlvt-form:no-intercept (forms)
  • Tests: 2 new tests + 4 renamed test attributes

Testing

All 285 tests pass across 18 suites.

Companion PR: livetemplate/livetemplate (server-side action field fix)

- Add lvt-form:action attribute for explicit form routing (highest priority)
- Stop excluding 'action' from form data — it's no longer reserved
- Rename lvt-no-intercept → lvt-nav:no-intercept on links (semantic separation)
- Keep lvt-form:no-intercept for forms (already correct)
- Update change-auto-wirer: lvt-no-intercept → lvt-form:no-intercept
- Add tests for lvt-form:action routing and action field preservation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

Review: lvt-form:action routing, lvt-nav:no-intercept, unreserve action field

The changes are clean and the intent is clear. A few things worth flagging:

Breaking changes with no backward compatibility

Both attribute renames are silent breaking changes:

  • lvt-no-interceptlvt-nav:no-intercept (links)
  • lvt-no-interceptlvt-form:no-intercept (forms)

Any existing HTML using lvt-no-intercept will silently stop working — forms/links that should be excluded will start getting intercepted. If this is a pre-1.0 / internal-only library, that's fine to call out explicitly in release notes. If users have existing templates, consider a deprecation shim or a warning in the console when the old attribute is detected.

Same applies to the action field behavior change — existing forms with <input name="action"> that previously had it excluded will now have it forwarded as data. This is the correct fix, but it's a silent behavioral change.

PR description references lvt-action hidden field

The action resolution order in the PR body lists lvt-action hidden field as step 2, but the diff doesn't show that being implemented or tested. Is that already in the codebase, or is it missing from this PR?

Minor: empty lvt-form:action

if (explicitAction) will skip an empty string attribute (lvt-form:action=""), falling through to submitter name resolution. That's probably the right behavior, but worth a comment since it's a subtle edge case.

Overall the logic is correct and the tests cover the new cases well.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates LiveTemplate’s client-side routing/interception behavior to avoid silent data loss for <input name="action"> while introducing an explicit form routing attribute and splitting the “no-intercept” opt-out semantics between forms and links.

Changes:

  • Add lvt-form:action as the highest-priority form action routing source and stop reserving/excluding "action" from submitted form data.
  • Rename interception opt-out attributes to be semantic: lvt-form:no-intercept (forms) and lvt-nav:no-intercept (links).
  • Update/extend tests to cover lvt-form:action priority and "action" field preservation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
dom/event-delegation.ts Adds lvt-form:action routing and preserves "action" as normal form data.
dom/link-interceptor.ts Switches link opt-out attribute to lvt-nav:no-intercept.
state/change-auto-wirer.ts Switches form opt-out attribute to lvt-form:no-intercept for change auto-wiring.
tests/event-delegation.test.ts Adds tests for lvt-form:action routing priority and "action" data preservation.
tests/change-auto-wirer.test.ts Renames tests to use lvt-form:no-intercept for form opt-out coverage.
Comments suppressed due to low confidence (1)

dom/link-interceptor.ts:66

  • Link interception opt-out behavior changed from the old attribute to lvt-nav:no-intercept, but there are no automated tests covering LinkInterceptor.shouldSkip() / click interception. Adding a small unit test for same-origin links with/without lvt-nav:no-intercept (and optionally backward-compat if intended) would help prevent regressions.
  private shouldSkip(link: HTMLAnchorElement): boolean {
    // External links
    if (link.origin !== window.location.origin) return true;
    // target="_blank" or other targets
    if (link.target && link.target !== "_self") return true;
    // Download links
    if (link.hasAttribute("download")) return true;
    // Opt-out attribute for link interception
    if (link.hasAttribute("lvt-nav:no-intercept")) return true;
    // Hash-only links (scroll anchors)
    if (link.pathname === window.location.pathname && link.hash) return true;
    // mailto/tel/javascript
    const protocol = link.protocol;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +154 to +173
// Action resolution order:
// 1. lvt-form:action attribute (explicit routing)
// 2. submitter.name (button name = action)
// 3. form.name (form name = action)
// 4. "" (server defaults to Submit())
if (!action && eventType === "submit" && element instanceof HTMLFormElement) {
if (!element.hasAttribute("lvt-form:no-intercept")) {
// Check for explicit routing attribute first
const explicitAction = element.getAttribute("lvt-form:action");
const submitter = (e as SubmitEvent).submitter;
if (submitter instanceof HTMLButtonElement && submitter.name) {
action = submitter.name;
} else if (element.name) {
action = element.name;
if (explicitAction) {
action = explicitAction;
} else {
action = "submit";
if (submitter instanceof HTMLButtonElement && submitter.name) {
action = submitter.name;
} else if (element.name) {
action = element.name;
} else {
action = "submit";
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The action resolution comment says the final fallback is an empty string (""), but the implementation sets action = "submit". Update the comment (or the fallback value) so docs match behavior; the PR description also states the default should be "submit".

Copilot uses AI. Check for mistakes.
Comment on lines +154 to 166
// Action resolution order:
// 1. lvt-form:action attribute (explicit routing)
// 2. submitter.name (button name = action)
// 3. form.name (form name = action)
// 4. "" (server defaults to Submit())
if (!action && eventType === "submit" && element instanceof HTMLFormElement) {
if (!element.hasAttribute("lvt-form:no-intercept")) {
// Check for explicit routing attribute first
const explicitAction = element.getAttribute("lvt-form:action");
const submitter = (e as SubmitEvent).submitter;
if (submitter instanceof HTMLButtonElement && submitter.name) {
action = submitter.name;
} else if (element.name) {
action = element.name;
if (explicitAction) {
action = explicitAction;
} else {
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

PR description mentions a legacy lvt-action hidden field in the action resolution order, but this client code doesn't read any such field (and it isn't referenced elsewhere in the repo). Either implement that resolution step here or adjust the PR description to avoid advertising unsupported behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +250
// Determine the action routing key to exclude from form data.
// Only the submitter button's name is excluded (it's the routing key, not data).
// "action" is NOT excluded — it's a normal data field.
const submitterForData = (targetElement as any).__lvtSubmitter as HTMLButtonElement | undefined;
const actionFieldName = submitterForData?.name || "action";
const actionFieldName = submitterForData?.name;

formData.forEach((value, key) => {
if (key === actionFieldName || key === "action") return;
if (actionFieldName && key === actionFieldName) return;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The comment says the submitter button’s name is excluded because it’s the “routing key”. With lvt-form:action overriding button-name routing, the submitter name may no longer be the routing key, but it’s still being excluded from message.data. Consider clarifying the comment to reflect the actual reason (e.g., always omit the successful submitter control to avoid noisy payloads), or conditionally exclude it only when it’s used for routing.

Copilot uses AI. Check for mistakes.
- Fix action resolution comment: fallback is 'submit' not ''
- Add note: lvt-action is server-side only, client doesn't read it
- Add comment: empty lvt-form:action='' falls through to submitter
- Clarify submitter exclusion: always excluded to avoid noisy payloads

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

Review

Breaking attribute renames (needs migration note)

The rename of lvt-no-interceptlvt-nav:no-intercept (links) and lvt-form:no-intercept (forms) is a breaking change with no backward compatibility shim. Any existing HTML using lvt-no-intercept on links or forms will silently stop opting out — links/forms will be intercepted again without any error or warning. Same for the action field being unreserved: existing code that relied on action being stripped will now receive it in ActionData.

These are reasonable breaking changes, but the PR description should call them out explicitly, and if there's a changelog/migration guide it should be updated before merge.

Minor logic note

In the data exclusion filter:

// Before
const actionFieldName = submitterForData?.name || "action";
if (key === actionFieldName || key === "action") return;

// After
const actionFieldName = submitterForData?.name;
if (actionFieldName && key === actionFieldName) return;

This also changes behavior for forms with no submitter button name: previously the action field was excluded regardless; now it flows through. The "action field in form data is preserved" test confirms this is intentional — just worth verifying the server-side companion PR handles receiving action in ActionData without conflict.

No bugs found

Logic, type safety, and test coverage look solid. The explicitAction empty-string fallthrough is handled correctly (if (explicitAction) treats "" as falsy). The two new tests cover the key scenarios well.

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.

2 participants