Files
integreat/.claude/skills/ssr-form-migration/reference/render-functions.md
Bryce 70c178de83 refactor(ssr): full Selmer migration of Transaction Edit; remove the wizard
Squashed Phase-2 SSR work: migrate the Transaction Edit modal's render path
entirely to Selmer templates (zero Hiccup in the render path), rip out the
multi-step wizard abstraction (EditWizard/LinksStep records, MultiStepFormState,
step-params[...] field names, mm/* middleware) in favor of a plain form with
flat derived state, and promote shared UI components to reusable Selmer partials
under resources/templates/components/. Adds the Selmer interop bridge, the
auto-ap.ssr.components.selmer (sc) wrapper library, and the ssr-form-migration
skill capturing the learnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:36:29 -07:00

3.5 KiB

Render functions: explicit data, or a top-rooted cursor

One function, data in, markup out. The data can arrive as a plain map or via a cursor — as long as the cursor was rooted at the top of the form and walked down to here, never faked to start at this depth. The rule is about where the cursor starts, not whether you use one.

GOOD — explicit data, pure, testable without setup

(defn account-row [{:keys [account index client-id amount-mode]}]
  (com/data-grid-row
    (com/hidden {:name (str "accounts[" index "][db/id]")
                 :value (or (:db/id account) "")})
    (com/data-grid-cell
      (account-typeahead* {:value (:transaction-account/account account)
                           :name  (str "accounts[" index "][account]")
                           :client-id client-id}))
    ...))

ALSO FINE — a cursor that started at the form root and was advanced naturally

;; The top-level render walks the cursor; the row fn receives the dereferenced row
;; (or the advanced cursor). No rebinding of *current*/*prefix* to fake depth.
(defn account-rows [accounts-cursor]
  (for [row-cursor (fc/each accounts-cursor)]   ; advanced from the root, not faked
    (account-row {:account @row-cursor :index (fc/index row-cursor) ...})))

transaction/edit.clj's transaction-account-row* is the cursor form done right: the caller (account-grid-body*) holds a top-rooted cursor via fc/cursor-map and hands each row cursor to one render fn.


The SMELL this migration removes

1. Faking the cursor's starting position

A "form cursor" is fine. The pain is rebinding the dynamic root deeper in the tree so a deeply nested render fn can run against a fragment. Real example from transaction/edit.clj's simple-mode-fields* (the thing to delete):

;; SMELL: re-roots the cursor to a synthetic MapCursor pointed at accounts[0] so a
;; fragment can render "deep". Fragile, and the source of the *-no-cursor* twin below.
(fc/with-field :transaction/accounts
  (fc/with-cursor (let [cur fc/*current*]
                    (if (sequential? @cur)
                      (nth cur 0 nil)
                      (auto_ap.cursor.MapCursor. {} (cursor/state cur)
                                                 (conj (cursor/path cur) 0))))
    ...))

Target: the cursor begins at the top level of what the form consumes and walks down naturally. Because the whole form is re-rendered each time (swap doctrine), there is no longer any reason to fake a deep starting position.

2. The *-no-cursor* twin

Faking the deep cursor forces a second copy of the same markup — one that reads the faked cursor and one that takes plain params for the cases where the fake can't be set up. transaction/edit.clj has exactly this pair:

(defn transaction-account-row*           [{:keys [value index client-id ...]}] ...) ; cursor form
(defn transaction-account-row-no-cursor* [{:keys [account index client-id ...]}] ...) ; duplicate markup

Fix: keep one render fn. If a caller already holds a top-rooted cursor, advance it and hand the row data (or the advanced cursor) to that one fn. Delete the *-no-cursor* copy. Heuristic 1 targets grep -c 'defn.*-no-cursor' → 0 and faked-cursor re-roots → 0.

Scorecard hooks (heuristics 1, 2)

grep -c 'defn.*-no-cursor' $F           # → 0
grep -cE 'with-cursor|MapCursor\.' $F   # faked re-roots → 0 (top-rooted cursors are fine)

Top-rooted cursors do not count against heuristic 1 — only re-roots that fake depth and the *-no-cursor* twins do.