Demo app

The /examples directory contains a full Phoenix demo app exercising every feature:

cd examples/demo
mix setup && iex -S mix phx.server

The 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 run mix format inside demo/ and copy any reformatted files back to the overlay so the two stay identical.
  • examples/regenerate.sh — rebuilds demo/ from scratch with a pinned phx.new release, 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.Decoder normalizes untrusted external data (string keys, safe atom conversion); Instance.FromSlots normalizes compiler-produced slot entries (atom keys) and holds all declarative-mode validation. Conversion runs in the DynamicForm.form/1 function 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_block closure) in their :slot field so the renderer can call render_slot/2. The :slot field is dropped from JSON encoding, and Instance.strip_slots/1 removes it for definition-only comparison.
  • Update guard: RendererLive.update/2 fires 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) and heex_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-formatted

Demo 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 test

Documentation

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.