# Selmer template conventions

> **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 real
`com/typeahead-` mixes them in one map:

```clojure
: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:

```html
{# templates/components/typeahead.html #}
<div class="relative" x-data="{{ x_data|safe }}" x-model="{{ x_model }}">
  <a class="{{ classes }}" x-ref="input" tabindex="0"
     @keydown.down.prevent.stop="tippy?.show()"
     @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.

## The render helper + interop bridge (`auto-ap.ssr.selmer`)

```clojure
(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 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 — verified mechanics (selmer 1.12.61)

Proven by REPL before the full migration (do the same before relying on any of these):

- **`{% include %}` works in FILE renders only.** `sel/render` = `selmer/render-file`, and
  include/extends/block are *parse-stage* tags. Rendering a template **string** that
  contains `{% include %}` throws `unrecognized tag: :include` (the runtime expr-tag has a
  nil handler). So includes only work from a `.html` file, never from `render-str`.
- **`{% include %}` sees `{% for %}` loop bindings.** Inside `{% for row in rows %}…{% include "components/x.html" %}…{% endfor %}` the partial can read `row.*`. Good for repeated
  rows — though Clojure-composing the rows (below) is usually simpler.
- **`{% include … with k=v %}` is IGNORED** in 1.12.61 (the `with` clause is dropped). To
  parametrize, either wrap with the block tag `{% with k=v %}{% include … %}{% endwith %}`
  (works), or — preferred — let a Clojure wrapper render the partial with an explicit ctx.

## The Clojure-wrapper composition pattern (`auto-ap.ssr.components.selmer` / `sc`)

Because `{% include with %}` can't pass args and the server computes most values anyway,
each shared component is a **thin Clojure wrapper that renders its own partial** (the
proven `location-select*` shape, generalised). The element *structure* lives 100% in the
`.html`; the only Clojure is data assembly. The modal's render fns compose these wrappers
and assemble a view-model, interpolating the pre-rendered fragments as `{{ frag|safe }}`.

```clojure
(sc/hidden {:name … :value …})            ; -> render "components/hidden.html"
(sc/validated-field {:label … :errors …} body…)
(sc/typeahead {:name … :url … :value … :content-fn …})   ; resolves label server-side
(sc/data-grid {:headers […] :footer-tbody …} rows…)
```

### `attrs->str` — the dynamic-attribute bridge

HTMX/Alpine attributes vary per call site, so a fixed template can't enumerate them.
`sc/attrs->str` serialises an attribute *map* to an HTML attribute *string* injected with
`{{ attrs|safe }}`: `<input type="hidden"{{ attrs|safe }}>`. Rules mirror hiccup2 — nil/false
dropped, `true` → bare boolean attr, else `name="escaped"` (via `hu/escape-html`, so JSON
`x-data` and `x-init` quotes become `&quot;`/`&apos;` and the browser decodes them back).
Keyword keys keep their full name incl. namespaced/colon forms (`:x-hx-val:account-id` →
`x-hx-val:account-id`). This keeps templates free of per-attribute `{% if %}` ladders while
still emitting 100% Selmer markup — `attrs->str` serialises data, it does not build Hiccup.

### Reuse the real class helpers

Wrappers reuse `inputs/default-input-classes`, `inputs/use-size`, `hh/add-class`,
`btn/bg-colors` so output matches the Hiccup component **modulo Tailwind class order**.
Verify by class-**set** equality + e2e, never byte-parity (see the cookbook entries).

### Trivial wrapper divs

A bare `<div class="w-72">…</div>` around a fragment is composed with a `wrap-div` string
helper (or put the class in the parent template), not a Hiccup vector — string composition
of a structural wrapper is not Hiccup and avoids a micro-template per div.

Keep `|safe` to values the server fully controls (rendered fragments, JSON for `x-data`),
never raw user input.

## Scope (Open decision 2)

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)

```bash
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. (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.)
