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

86 lines
3.5 KiB
Markdown

# 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
```clojure
(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
```clojure
;; 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):
```clojure
;; 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:
```clojure
(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)
```bash
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.