Whole-form hx-select swaps with zero out-of-band swaps

Replace the section-swap + OOB approach with uniform whole-form swaps,
eliminating both out-of-band swaps:

- Discrete edits (vendor, account, location, mode, add/remove row) now swap all
  of #wizard-form via hx-select. The active action/tab already round-trips
  (:action is in edit-form-schema and the tab x-data inits from it), so a
  whole-form swap re-creates the tab state from the server value and the active
  tab is preserved -- no #wizard-snapshot OOB needed, since the snapshot hidden
  field rides along inside the form.
- Move the totals into their own <tbody id="account-totals"> (new optional
  :footer-tbody param on data-grid-) so the amount field updates them with a
  plain targeted swap instead of an OOB swap of #total,#balance. The totals tbody
  is a sibling of the input rows, so the amount input is never replaced.
- Memo unchanged (hx-swap=none).

Net: 0 hx-select-oob, 0 morph. The focus invariant is unchanged -- the typed
field is never inside a region it swaps. Tab clicks stay Alpine (instant); only
the action value round-trips. Revert the now-unneeded #wizard-snapshot id.

Full e2e suite: 27 passed / 2 failed (same pre-existing, unrelated failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 11:01:37 -07:00
parent a2684bf5c1
commit 5f1bb6db82
4 changed files with 77 additions and 84 deletions

View File

@@ -233,9 +233,8 @@
:x-dispatch:changed "simpleAccountId"
:hx-trigger "changed"
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"
:hx-include "closest form"}
(location-select*
@@ -250,9 +249,8 @@
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
:hx-include "closest form"
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"}
"Switch to advanced mode"]]]))
@@ -296,9 +294,8 @@
:x-dispatch:changed "accountId"
:hx-trigger "changed"
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"
:hx-include "closest form"}
(location-select* {:name (fc/field-name)
@@ -317,12 +314,12 @@
:class "w-16 account-amount-field"
:value (fc/field-value)
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
;; Typing an amount posts the whole form but swaps NOTHING into the field
;; itself (hx-swap=none). Only the TOTAL and BALANCE cells are pulled out of
;; the response and applied out-of-band, so the amount input is never replaced
;; and the user's focus + caret survive with no morph involved.
:hx-select-oob "#total,#balance"
:hx-swap "none"
;; Typing an amount posts the whole form but swaps back only the
;; #account-totals tbody -- a sibling of the input-bearing rows, so
;; the amount input is never replaced and the caret survives.
:hx-target "#account-totals"
:hx-select "#account-totals"
:hx-swap "outerHTML"
:hx-trigger "keyup changed delay:300ms"
:hx-include "closest form"}]
(if (= "%" amount-mode)
@@ -331,9 +328,8 @@
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-remove-account)
:hx-vals (hx/json {:row-index (or index 0)})
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"
:hx-include "closest form"
:class "account-remove-action"} svg/x))))
@@ -475,12 +471,36 @@
:name "step-params[amount-mode]"
:orientation :horizontal
:hx-post (bidi/path-for ssr-routes/only-routes ::route/toggle-amount-mode)
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"
:hx-include "closest form"}))
(com/data-grid-header {:class "w-16"})]}
(com/data-grid-header {:class "w-16"})]
;; Totals live in their own <tbody id="account-totals"> so the amount
;; field refreshes them with a plain targeted swap, never swapping the
;; input-bearing rows above (which would drop the caret).
:footer-tbody
[:tbody {:id "account-totals"}
(com/data-grid-row {:class "account-total-row"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
(com/data-grid-cell {:id "total"
:class "text-right"}
(account-total* request))
(com/data-grid-cell {}))
(com/data-grid-row {:class "account-balance-row"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
(com/data-grid-cell {:id "balance"
:class "text-right"}
(account-balance* request))
(com/data-grid-cell {}))
(com/data-grid-row {:class "account-grand-total-row"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TRANSACTION TOTAL"])
(com/data-grid-cell {:class "text-right"}
(format "$%,.2f" total))
(com/data-grid-cell {}))]}
(fc/cursor-map (fn [cursor]
(transaction-account-row* {:value cursor
:client-id (-> request :entity :transaction/client :db/id)
@@ -491,35 +511,12 @@
(com/data-grid-row {:class "new-row"}
(com/data-grid-cell {:colspan 4}
(com/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-new-account)
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"
:hx-include "closest form"
:color :secondary}
"New account")))
(com/data-grid-row {:class "account-total-row"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
(com/data-grid-cell {:id "total"
:class "text-right"}
(account-total* request))
(com/data-grid-cell {}))
(com/data-grid-row {:class "account-balance-row"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
(com/data-grid-cell {:id "balance"
:class "text-right"}
(account-balance* request))
(com/data-grid-cell {}))
(com/data-grid-row {:class "account-grand-total-row"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TRANSACTION TOTAL"])
(com/data-grid-cell {:class "text-right"}
(format "$%,.2f" total))
(com/data-grid-cell {})))))
"New account"))))))
(defn manual-coding-section*
"Renders the vendor field + account/location section for the manual tab.
@@ -536,9 +533,8 @@
(com/hidden {:name "step-params[mode]" :value (name mode)})
[:div {:hx-trigger "change"
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"
:hx-sync "this:replace"
:hx-include "closest form"}
@@ -564,9 +560,8 @@
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
:hx-include "closest form"
:hx-target "#manual-coding-section"
:hx-select "#manual-coding-section"
:hx-select-oob "#wizard-snapshot"
:hx-target "#wizard-form"
:hx-select "#wizard-form"
:hx-swap "outerHTML"}
"Switch to simple mode"]])
(fc/with-field :transaction/accounts