diff --git a/.claude/skills/ssr-form-migration/reference/component-cookbook.md b/.claude/skills/ssr-form-migration/reference/component-cookbook.md index f9332f10..dcfcddb1 100644 --- a/.claude/skills/ssr-form-migration/reference/component-cookbook.md +++ b/.claude/skills/ssr-form-migration/reference/component-cookbook.md @@ -90,6 +90,25 @@ replaces the amount input (caret survives). :placeholder "Optional note"}) ; no hx-* — rides along to save ``` +## location-select — first Selmer-migrated component (validated) + +The account row's location ``, saves, and spreads to DT). ## Why Selmer for interactive components In Hiccup the same Alpine/HTMX attribute is sometimes a keyword and sometimes a string in -the same file — there's no rule a reader (or an LLM) can rely on: +the same file — there's no rule a reader (or an LLM) can rely on. The real +`com/typeahead-` mixes them in one map: ```clojure -;; All of these appear in one component today: -:x-ref "input" "x-ref" "hidden" -:x-model "value.value" "x-model" "search" -"@keydown.down.prevent.stop" "tippy.show();" ; handlers MUST be strings -:x-init "..." ; structural attrs are keywords +:x-modelable "value.value" ; keyword key +"x-ref" "hidden" ; string key +"@keydown.down.prevent.stop" "tippy?.show();" ; handlers MUST be strings +:x-init "..." ; structural attrs are keywords ``` In a Selmer template the same markup is unambiguous plain HTML: @@ -28,36 +28,66 @@ In a Selmer template the same markup is unambiguous plain HTML: @keydown.backspace="tippy?.hide(); value = {value:'', label:''}"> - ... ``` Note the `tippy?.` null-guard carried over from the swap doctrine — Selmer doesn't change the Alpine-survives-swap requirement. -## Render helper + interop bridge (the Phase 2 foundation) +## The render helper + interop bridge (`auto-ap.ssr.selmer`) ```clojure -(defn render [tpl ctx] (selmer/render-file tpl ctx)) -(defn hiccup->html [h] (hiccup/html h)) ; embed hiccup inside selmer via {{ frag|safe }} -;; selmer fragment inside hiccup: [:div (hiccup/raw (render "..." ctx))] +(sel/render template ctx) ; selmer/render-file -> HTML string (classpath-relative path) +(sel/render-str template ctx) ; render from a string (tests/REPL) +(sel/hiccup->html h) ; Hiccup -> string, for {{ frag|safe }} inside a template +(sel/raw html-string) ; wrap a rendered string so hiccup2 emits it verbatim +(sel/render->hiccup template ctx); render + raw, ready to drop into a Hiccup tree ``` -The bridge must work **both ways** during the strangler transition: a Hiccup component -renders inside a Selmer template (pass `(hiccup->html h)` into the context, render with -`|safe`), and a Selmer fragment renders inside a Hiccup tree (`(hiccup/raw (render ...))`). -Prove both in Phase 2 before broad use. +The bridge works **both ways** (proven in `selmer_test`): a Hiccup component renders inside +a Selmer template (`hiccup->html` + `|safe`), and a Selmer fragment renders inside a Hiccup +tree (`render->hiccup`, which `raw`-wraps so hiccup2 doesn't double-escape). + +## The worked example — `location-select*` + +Template (`resources/templates/components/location-select.html`): plain HTML, an +`{% for %}` over option maps, `{% if opt.selected %}`. + +```clojure +;; Clojure side: build the data, compute classes (reuse inputs/default-input-classes so +;; styling can't drift), render, and return a Hiccup-embeddable fragment. +(defn location-select* [{:keys [name client-locations value ...]}] + (let [options (cond ...) ; [[value label] ...] + selected (or value (ffirst options)) + classes (str/join " " (conj (vec inputs/default-input-classes) "w-full"))] + (sel/render->hiccup "templates/components/location-select.html" + {:name name :classes classes + :options (for [[v l] options] {:value v :label l :selected (= v selected)})}))) +``` + +Lessons: +- **Pass computed values in, don't hard-code.** Reuse the Clojure source of truth + (`inputs/default-input-classes`) as a context value rather than copying class strings + into the template — otherwise styling drifts from the shared components. +- **Verify by string-match + e2e, not byte-parity.** `hh/add-class` is set-based, so class + *order* differs from the old `com/select` output; CSS is order-independent and the e2e + proves behavior. (`testing-conventions`: don't assert on exact markup.) +- **Embed via the bridge.** `render->hiccup` lets the Selmer fragment sit inside the + still-Hiccup row (`com/validated-field` wraps it) — strangler, one component at a time. ## Composition -Selmer composes via `{% include %}` and `{% block %}`. Prefer small per-component -templates that the cookbook references by path. Keep `|safe` to values the server fully -controls (rendered Hiccup, JSON for `x-data`), never raw user input. +Selmer composes via `{% include %}` and `{% block %}`. Prefer small per-component templates +referenced by classpath path. Keep `|safe` to values the server fully controls (rendered +Hiccup, JSON for `x-data`), never raw user input. ## Scope (Open decision 2) -Hybrid: convert interactive/attribute-heavy components first; static markup may stay -Hiccup. Revisit a fuller sweep in Phase 11. +Hybrid, and the boundary is real: **the modal's attribute-heavy components delegate to the +shared `com/typeahead` / `com/select` / `com/button-group-button`.** Converting those is a +*cross-cutting* change (every modal uses them), so it belongs to the Phase 11 Selmer sweep, +not a single modal. `location-select*` is the first, self-contained proof; the shared +components follow when the sweep promotes them to Selmer partials. ## Attribute-consistency scorecard (heuristic 8) @@ -65,4 +95,5 @@ Hiccup. Revisit a fuller sweep in Phase 11. grep -cE '"x-[a-z]|"hx-[a-z]|"@' # → 0 mixed encodings in Selmer ``` A migrated Selmer template has no mixed `:x-`/`"x-"` encodings because everything is plain -HTML. +HTML. (The Hiccup `"@click"`/`":class"` offenders that remain in `edit.clj` live in the +shared-component call sites — they clear when those components move to Selmer.) diff --git a/resources/templates/components/location-select.html b/resources/templates/components/location-select.html new file mode 100644 index 00000000..c96755d7 --- /dev/null +++ b/resources/templates/components/location-select.html @@ -0,0 +1,8 @@ +{# Location + {% for opt in options %} + + {% endfor %} + diff --git a/src/clj/auto_ap/ssr/transaction/edit.clj b/src/clj/auto_ap/ssr/transaction/edit.clj index 8c754ff4..fdd84fc8 100644 --- a/src/clj/auto_ap/ssr/transaction/edit.clj +++ b/src/clj/auto_ap/ssr/transaction/edit.clj @@ -21,11 +21,13 @@ [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] [auto-ap.ssr.components :as com] + [auto-ap.ssr.components.inputs :as inputs] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.transaction.common :refer [grid-page]] [auto-ap.ssr.components.multi-modal :as mm] [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.selmer :as sel] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [->db-id apply-middleware-to-all-handlers check-allowance @@ -36,6 +38,7 @@ [bidi.bidi :as bidi] [clj-time.coerce :as coerce] [clojure.edn :as edn] + [clojure.string :as str] [datomic.api :as dc] [hiccup.util :as hu] [iol-ion.query :refer [dollars=]] @@ -153,20 +156,29 @@ (:vendor/default-account clientized)))) (defn location-select* + "The location