Files
integreat/.claude/skills/ssr-form-migration/reference/selmer-conventions.md
Bryce 3ecd115f76 docs(skill): distil ssr-form-migration skill from transaction-edit reference (Phase 1)
Capture the proven whole-form hx-select swap method as a reusable skill so every
later modal migration is cheaper and consistent. No app code changes.

- SKILL.md: the per-migration playbook (classify → baseline → characterize →
  consolidate render fns → templatize → wire HTMX → collapse routes → verify →
  commit → feed skill) + Growth contract + non-negotiables.
- reference/swap-doctrine.md: the four swap rules, focus invariant, Alpine-survives-
  swap hardening, target-selector strategy — worked from the real edit.clj swaps
  (memo no-request, account→location targeted cell, amount→totals sibling-tbody,
  vendor/mode/row whole-form). 0 OOB.
- reference/render-functions.md: explicit-data or top-rooted cursor; the MapCursor
  fake + transaction-account-row-no-cursor* twin as the smell to remove.
- reference/form-vs-wizard.md: classification + the data-driven session-backed
  (formtools SessionStorage) engine that replaces the snapshot round-trip + protocol.
- reference/selmer-conventions.md: STUB, validated in Phase 2.
- component-cookbook.md / gotchas.md / test-recipes.md / scorecard.md: seeded from
  what transaction-edit proves (7 cookbook entries, caret-survival + typeahead test
  recipes, scorecard baseline LOC 1608 / ~12 routes / 1 no-cursor twin / 2 faked
  roots / 0 OOB).

Scorecard (Transaction Edit baseline, before Phase 2): LOC 1608, routes ~12,
no-cursor twins 1, faked-cursor roots 2, snapshot merges ~75, OOB 0, mixed hx- 8.
2026-06-03 00:05:11 -07:00

2.6 KiB

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.

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:

;; 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

In a Selmer template the same markup is unambiguous plain 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.

Render helper + interop bridge (the Phase 2 foundation)

(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))]

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.

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.

Scope (Open decision 2)

Hybrid: convert interactive/attribute-heavy components first; static markup may stay Hiccup. Revisit a fuller sweep in Phase 11.

Attribute-consistency scorecard (heuristic 8)

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.