feat(ssr): migrate location-select to a Selmer template (Phase 2 — Selmer validated)
First interactive Transaction Edit component rendered from a Selmer template instead of
Hiccup/com/select, proving the render-file path + the Hiccup<->Selmer interop bridge on
real, e2e-covered markup.
- resources/templates/components/location-select.html: plain-HTML <select> with a
{% for %} over option maps + {% if opt.selected %}.
- location-select*: build options/selected/classes in Clojure (reusing
inputs/default-input-classes so styling can't drift), render via
sel/render->hiccup, and embed the fragment back into the still-Hiccup account row.
- skill: finalize selmer-conventions.md from this validated example (was a stub); add the
cookbook entry; scorecard marks the first Selmer component.
Verified on a fresh server: full suite 38 pass / 1 unrelated fail, swap 6/6,
transaction-edit 8/8 -- the Shared Location test selects through the Selmer <select>,
saves, and spreads Shared -> DT. Verified by string-match + e2e (not byte-parity:
hh/add-class is set-based so class order differs, CSS is order-independent).
Scope note: the modal's remaining attribute-heavy components delegate to the shared
com/typeahead / com/select / com/button-group-button; converting those is the
cross-cutting Phase 11 Selmer sweep, not a single-modal change (Open decision 2).
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
# Selmer template conventions
|
||||
|
||||
> **Status: STUB — validated in Phase 2.** This file describes the target. The Selmer
|
||||
> dependency, render helper, and interop bridge are added in Phase 2 (Transaction Edit);
|
||||
> rewrite this file from the *real, verified* example once that lands, and record each
|
||||
> converted component in `component-cookbook.md`.
|
||||
> **Validated** in the Transaction Edit migration: `location-select*` now renders from
|
||||
> `resources/templates/components/location-select.html` via the interop bridge, embedded
|
||||
> back into the Hiccup account row. Verified: swap spec 6/6, transaction-edit 8/8 (the
|
||||
> Shared Location test selects through the Selmer `<select>`, 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:''}">
|
||||
<span x-text="value.label"></span>
|
||||
</a>
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
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]|"@' <migrated-template> # → 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.)
|
||||
|
||||
Reference in New Issue
Block a user