Demo app
The /examples directory contains a full Phoenix demo app exercising every
feature:
cd examples/demo
mix setup && iex -S mix phx.serverThe demo is generated, following the same pattern as Slab:
examples/overlay/— the version-controlled demo code: LiveViews, form definitions, layout tweaks, and tests. This is where edits go.examples/demo/— the generated app (committed, disposable). After editing the overlay, copy it over:cp -R overlay/. demo/, then runmix formatinsidedemo/and copy any reformatted files back to the overlay so the two stay identical.examples/regenerate.sh— rebuildsdemo/from scratch with a pinnedphx.newrelease, applies DynamicForm-specific edits (path dependency, routes, Tailwind@source, a stub uploader for the direct-upload demo), and copies the overlay on top. Run it whenever the skeleton drifts out of date. Never edit generated files directly.
The /slot-forms page doubles as a manual test bed: each section shows the
definition above the rendered form, and the "Input Preservation Test" button
re-renders the parent LiveView to verify in-progress input survives (see the
update guard below).
Architecture notes
Both definition modes converge on DynamicForm.Instance before any stateful
code runs:
JSON / stored map ──▶ Instance.Decoder ────┐
├──▶ %Instance{} ──▶ RendererLive ──▶ Renderer
<:field> slots ─────▶ Instance.FromSlots ──┘Instance.Decodernormalizes untrusted external data (string keys, safe atom conversion);Instance.FromSlotsnormalizes compiler-produced slot entries (atom keys) and holds all declarative-mode validation. Conversion runs in theDynamicForm.form/1function component — the LiveComponent's contract stays "give me an Instance".- Slot carriage: elements defined with a slot body keep the raw slot
entry (including its
inner_blockclosure) in their:slotfield so the renderer can callrender_slot/2. The:slotfield is dropped from JSON encoding, andInstance.strip_slots/1removes it for definition-only comparison. - Update guard:
RendererLive.update/2fires on every parent re-render that touches its inputs and used to rebuild the changeset each time — wiping in-progress user input. It now skips re-initialization when the slot-stripped instance, initial params, and form name are unchanged, while still assigning the fresh instance so slot bodies re-render with current parent assigns. - The design rationale for the declarative mode lives in
heex_form_definition_exploration.md(UX options) andheex_form_backend_implementation.md(backend options) at the repo root.
Testing
Library tests (unit tests for conversion, changesets, visibility, rendering):
mix test
mix credo --all
mix format --check-formattedDemo app tests (end-to-end LiveView tests: slot definitions, conditional visibility, input preservation, submission handling, plus a smoke test over every route):
cd examples/demo
mix testDocumentation
mix docs generates API documentation including these guides. The README,
module docs, and guides should stay consistent — the README covers the
high-level pattern and installation, the guides cover depth, and module docs
cover per-function detail.