# Component cookbook GROWS every migration. Each entry: what it is, the swap rule it uses, and the canonical snippet. Reuse these before writing anything new; the success signal is *more reuse each migration*. Seeded from `transaction/edit.clj` (Hiccup form — Selmer versions land in Phase 2). --- ## typeahead (account / vendor) — Alpine + tippy, survives swaps Used for account and vendor selection. Click-to-select (not a live text caret), so a whole-form swap on change is safe. Null-guard `tippy?`/`$refs.input?`. ```clojure (defn account-typeahead* [{:keys [name value client-id x-model]}] [:div.flex.flex-col (com/typeahead {:name name :placeholder "Search..." :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) (cond-> {:purpose "transaction"} client-id (assoc :client-id client-id))) :id name :x-model x-model ; binds selected value into the row's Alpine scope :value value :content-fn (fn [v] (:account/name (d-accounts/clientize ... v client-id)))})]) ``` Reuse note: `:x-model` lets the *parent* row read the selected id (e.g. `accountId`) to gate a targeted location swap. See account-row. ## account-row — cursor render fn + per-row targeted location swap + whole-form remove The canonical "row in a repeated grid" pattern. One render fn, top-rooted cursor. - account typeahead binds `accountId` into row Alpine scope; - **location cell** swaps *only itself* (`#account-location-`) on `changed` (swap-doctrine Rule 2); - **amount cell** swaps *only* `#account-totals` (Rule 4, sibling tbody); - **remove** swaps the whole form (Rule 3). ```clojure (defn transaction-account-row* [{:keys [value client-id amount-mode index]}] (com/data-grid-row (-> {:class "account-row" :id (str "account-row-" index) :x-data (hx/json {:show ... :accountId (fc/field-value (:transaction-account/account value))}) :data-key "show" :x-ref "p"} hx/alpine-mount-then-appear) (fc/with-field :db/id (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :transaction-account/account (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} (account-typeahead* {:value (fc/field-value) :client-id client-id :name (fc/field-name) :x-model "accountId"})))) (fc/with-field :transaction-account/location (com/data-grid-cell {:id (str "account-location-" index)} ...Rule 2 targeted swap...)) (fc/with-field :transaction-account/amount (com/data-grid-cell {} ...Rule 4 totals swap...)) (com/data-grid-cell {:class "align-top"} ...Rule 3 whole-form remove...))) ``` TODO Phase 2: drop the `transaction-account-row-no-cursor*` twin; this is the only kept form. ## totals in a sibling `` — Rule 4 instead of OOB Running totals live in their own ``, a sibling of the input-bearing rows, so an amount edit refreshes them with a plain targeted swap and never replaces the amount input (caret survives). ```clojure (com/data-grid {:footer-tbody [:tbody {:id "account-totals"} (com/data-grid-row {:class "account-total-row"} ... (account-total* request) ...) (com/data-grid-row {:class "account-balance-row"} ... (account-balance* request) ...)]} ...input rows...) ``` ## money-input / text-input amount field — Rule 4 targeted totals swap ```clojure (com/money-input {:name (fc/field-name) :id (str "account-amount-" index) :class "w-16 account-amount-field" :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) :hx-target "#account-totals" :hx-select "#account-totals" :hx-swap "outerHTML" :hx-trigger "keyup changed delay:300ms" :hx-include "closest form"}) ``` `%` mode swaps to `com/text-input {:type "number" :step "0.01"}` with the same swap attrs. ## memo field — Rule 1, no request ```clojure (com/text-input {:value (fc/field-value) :name (fc/field-name) :id "edit-memo" :placeholder "Optional note"}) ; no hx-* — rides along to save ``` ## location-select — first Selmer-migrated component (validated) The account row's location ``; `options [[value label] …]`, `:value` (string/keyword) marks selected, extra hx-/x- attrs ride through. `location-select.html` generalized — reach for this before `com/select`. Added for the bulk-code status field. | ## inline click-to-edit cell (Phase 4) — targeted `.account-cell` swap, not a whole-form op A "display value + pencil → edit-in-place → check/cancel" cell. Three tiny **stateless** routes, each swapping just the cell (`hx-target="closest .account-cell"`, `outerHTML`): a `display` cell (value + pencil `hx-get edit`), an `edit` cell (typeahead + check `hx-put save` / cancel `hx-get cancel`). State rides in the request (item index + current value via `hx-vals`), so no server-side "which cell is editing" flag is needed. Keep it as its own routes — it is a distinct feature, *not* folded into the whole-form `form-changed` dispatcher (that would lose the targeted swap and re-render the whole modal on every pencil click). The cells are assembled with `sc/*` + `sel/raw` strings (like `edit.clj`'s `footer*`); SVGs ride in as `svg/*` Hiccup via the `sc/a-icon-button` body (no `[:svg]` literal lands in the modal file). ## db/id-keyed item merge (Phase 4) — for rows the form posts only partially When a row renders some fields read-only (so they aren't posted) but the entity holds them (sales-summary auto items post only db/id/category/account — not ledger-side/amount), the flat `wrap-derive-state` must **overlay posted items onto the persisted items by `:db/id`** so the unposted fields survive a re-render: `(merge (by-id (:db/id posted)) posted)`. New rows (temp `:db/id` not in the entity) ride through as-is. This is the row-level analog of edit's "entity-only fields always from the entity"; without it, a re-render drops ledger-side/amount and the debit/credit split + totals break. | `sc/validated-field` | `validated-field.html` | label + body + always-present error `

`; pass-through attrs land on the wrapping div (the per-row location cell hangs its swap wiring here) | | `sc/button` / `sc/a-button` / `sc/a-icon-button` | `button`/`a-button`/`a-icon-button`.html | spinner via `{% include "spinner.html" %}`; class via `btn/bg-colors` | | `sc/badge` / `sc/link` | `badge`/`link`.html | | | `sc/button-group` / `sc/button-group-button` | `button-group(+button).html` | the group does **not** mutate children's classes (the Hiccup `group-` added rounded-l/r) — add rounding in the caller/template (tabs do) | | `sc/radio-card` | `radio-card.html` | reproduces the `select-keys [:hx-post :hx-target :hx-swap :hx-include :hx-trigger]` filter (drops `:hx-vals`/`:hx-select`) **and** the dangling-`[:h3]` quirk: only the `