Quick lookup tables. For narrative documentation see the Usage guide.
DynamicForm.form/1 attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
id | string | required | Component ID; also the instance id in declarative mode |
instance | any | nil | Data mode: Instance struct, JSON string, or map |
json | string | nil | Data mode: SurveyJS-compatible JSON string, decoded via Instance.decode!/1 |
title | string | nil | Instance title (declarative mode) |
description | string | nil | Instance description (declarative mode) |
on_change | function | nil | 1-arity (payload) -> payload, after built-in validations on every change and during the submit validation pass |
change_debounce_in_ms | integer | nil | Milliseconds of quiet before a change runs on_change and sends its :change message. nil and 0 run both on every change |
on_submit | function | nil | 1-arity (payload) -> payload, on every submit — valid or not |
on_success | function | nil | 1-arity (payload), on every valid submission — replaces the :success message |
send_message_on | list | [:success] | Lifecycle events that message the parent: any of [:success, :change, :submit] |
data | map | %{} | Initial form data for edit mode — existing record values; a payload's data round-trips directly |
form_name | string | "dynamic_form" | Form namespace for params |
submit_text | string | "Submit" | Submit button text |
hide_submit | boolean | false | Hide the built-in submit button |
gettext | atom | DynamicForm.Gettext | Gettext backend for translations |
components | atom | nil | Custom components module; falls back to the :dynamic_form, :components config, then the built-ins per function |
custom_field_types | map | nil | Custom field types (%{"name" => ecto_type}), merged over the :dynamic_form, :custom_field_types config |
validation_summary | string | nil | Errors at top of form: nil, "simple", or "detailed" |
render_only | boolean | false | Render markup only: events go to the parent LiveView's handle_event/3; requires form |
form | Phoenix.HTML.Form | nil | Render-only mode: the parent-owned form to render against |
phx_change | string | "validate" | Render-only mode: change event name |
phx_submit | string | "submit" | Render-only mode: submit event name |
Exactly one of instance, json, or <:field> slots must be provided.
render_only excludes the lifecycle attributes (on_change,
change_debounce_in_ms, on_submit, on_success, send_message_on,
data, form_name, validation_summary) and file upload questions — both
raise.
DynamicForm.RendererLive (used directly via <.live_component>) accepts
id, instance, and the same optional attributes from data down.
<:field> attributes
| Attribute | Type | Applies to | Description |
|---|---|---|---|
type | string | all | Required. One of the question or element types below |
name | string | all | Field name. Required for question types; auto-generated for html/image/custom |
label | string | questions, image | Question title / image alt text (→ title) |
placeholder | string | text inputs | Input placeholder |
description | string | questions | Help text shown below the input |
input_type | string | text | HTML input type pass-through (email, number, ...) |
default | any | questions | Default value seeded into the form params (→ defaultValue) |
options | list | choice types | Choices: [{"Label", "value"}, ...] or ["value", ...] (→ choices) |
required | boolean | questions | Required field (→ isRequired) |
required_if | string | questions | Conditional requirement expression (→ requiredIf) |
visible_if | string | all | Conditional visibility expression (→ visibleIf) |
enable_if | string | all | Conditional enablement expression (→ enableIf) |
read_only | boolean | questions | Display value without allowing edits (→ readOnly) |
group | string | all | Collect this field into the <:group> panel with this name |
nested | string | all except file | Data scope: collect this field into the <:nested> form with this name — see the Nested Forms guide |
rate_min / rate_max / rate_step | integer | rating | Rating scale (defaults 1–5, step 1) |
min_length / max_length | integer | text | Builds a text validator |
min / max | number | numeric | Builds a numeric validator |
pattern | string | text | Builds a regex validator |
format | string | text | Format validator; supported: "email" |
validators | list | questions | Escape hatch: Instance.Validator structs or atom-keyed maps |
html | string | html | Raw HTML content (alternative to a slot body) |
src | string | image | Image URL (→ imageLink); required |
width / height / fit | string | image | Image sizing (→ imageWidth/imageHeight/imageFit) |
metadata | map | all | Metadata map (upload config, radiogroup style, ...) |
Slot bodies: any question type accepts a body receiving its
Phoenix.HTML.FormField via :let; html accepts a plain body;
custom requires a body receiving the Phoenix form.
<:group> attributes
| Attribute | Type | Description |
|---|---|---|
name | string | Required. Referenced by <:field group="...">; also the panel's name |
title | string | Panel title |
visible_if | string | Conditional visibility expression |
enable_if | string | Conditional enablement (disables all contained questions when false) |
nested | string | Data scope this group lives in; every member field must declare the identical scope |
<:nested> attributes
Declares a repeating child form (→ a SurveyJS paneldynamic question);
fields join it with <:field nested="...">. See the
Nested Forms guide.
| Attribute | Type | Description |
|---|---|---|
name | string | Required. Data key — the value is a list of entry maps |
title / description | string | Heading and help text |
entry_title | string | Per-entry heading; {panelIndex} interpolates the 1-based number (→ templateTitle) |
entries | integer | Entries seeded on a fresh form (→ panelCount) |
min_entries / max_entries | integer | Count limits: buttons hide, and submit validates (→ minPanelCount/maxPanelCount) |
add_text / remove_text | string | Button labels (→ addPanelText/removePanelText) |
no_entries_text | string | Shown at zero entries (→ noEntriesText) |
confirm_delete / confirm_text | boolean / string | Confirmation before removing (→ confirmDelete/confirmDeleteText) |
key / key_error | string | Member field unique across entries + error message (→ keyName/keyDuplicationError) |
generate_ids | boolean | Seed each entry with a stable dynamic_form_id (default true; → generateIds) |
default | list | Initial value: list of entry maps (→ defaultValue) |
default_entry | map | Values seeded into each newly added entry (→ defaultPanelValue) |
required | boolean | At least one entry required (→ isRequired) |
visible_if / enable_if | string | Conditional expressions |
nested | string | Place this nested form inside another <:nested> form |
group | string | Place this nested form inside a <:group> panel |
Question types
| Type | Renders as | Notes |
|---|---|---|
text | <input> | input_type passes through (email, number, ...); number casts to decimal |
comment | <textarea> | |
dropdown | <select> | Requires options |
radiogroup | Radio buttons | Requires options; metadata "style": "vertical"/"horizontal" |
checkbox | Checkbox group | Array-valued; requires options |
tagbox | Multi-select | Array-valued; requires options |
boolean | Single checkbox | |
rating | Numeric radio row | rate_min/rate_max/rate_step; casts to integer |
file | Direct upload | Presigner + uploader required — see Usage: File uploads |
paneldynamic | Repeating child form | Casts to a list of maps, validated per entry; <:nested> in declarative mode — see Nested Forms |
Element types
| Type | Renders as | Notes |
|---|---|---|
html | Raw HTML or slot body | String attr goes through Phoenix.HTML.raw/1; slot bodies are escaped HEEx |
panel | Titled container | Declared via <:group> in declarative mode; nestable in data mode |
image | <img> | src required |
custom | Slot body | Declarative-only; body receives the Phoenix form |
Validators
Data-mode JSON validator objects (built automatically by the flattened attrs
in declarative mode). Each accepts a custom error message via text:
| Type | Fields | Flattened attrs |
|---|---|---|
text | minLength, maxLength | min_length, max_length |
numeric | minValue, maxValue | min, max |
email | — | format="email" |
regex | regex | pattern |
input_type="email" also applies email format validation automatically.
Conditional expression operators
Used by visible_if, required_if, and enable_if:
| Category | Operators |
|---|---|
| Comparison | =, ==, <>, !=, >, <, >=, <= |
| Presence | empty, notempty |
| Membership | contains, notcontains, anyof, allof, noneof |
| Combinators | and, or, parentheses |
Field references use braces: {field_name}. Literals: 'strings', numbers,
true/false, ['lists', 'of', 'values'].
Messages
Sent to the parent LiveView, carrying the lifecycle event and the payload:
{:dynamic_form, event, %DynamicForm.Payload{}}| Event | Sent | In send_message_on by default? |
|---|---|---|
:success | On a valid submission (replaced by on_success when defined) | yes |
:change | On every change, after the built-in validations and on_change | no |
:submit | On every submit — valid or not — after on_submit | no |
A valid submission with all three enabled delivers :change, :submit, and
:success, in that order. :change and :submit payloads are routinely
invalid — check DynamicForm.Payload.valid?/1.
%DynamicForm.Payload{} fields:
| Field | Value |
|---|---|
id | The form component's id |
changeset | The form's Ecto.Changeset; its valid? flag is the source of truth for validity (always true for :success) |
data | The applied changeset data |
extra | Empty map by default; written by on_submit via Payload.put_extra/3 |
Lifecycle callback contracts
on_change: (DynamicForm.Payload.t()) -> DynamicForm.Payload.t()
on_submit: (DynamicForm.Payload.t()) -> DynamicForm.Payload.t()
on_success: (DynamicForm.Payload.t()) -> any()on_change runs after built-in validations, on every change (including a
nested entry add/remove) and during the submit validation pass — after
change_debounce_in_ms milliseconds of quiet when that attribute is set,
except on submit, where it always runs inline. on_submit runs on every
submit — valid or not — so it can batch expensive checks with the built-in
errors into one complete error list. Both are validation hooks that run
alongside the built-in behavior: reject a submission with
DynamicForm.Payload.add_error/4 (validity lives on the changeset, so
adding an error marks the submission invalid); perform side effects in the
parent's handle_info/2 instead.
on_success runs on every valid submission and replaces the :success
message; its return value is ignored. Listing :success in
send_message_on alongside it raises.
Messages reach the LiveView process only. A LiveComponent parent uses the
callbacks with Phoenix.LiveView.send_update/2 instead — see the
Lifecycle guide.
Upload metadata keys
metadata map keys for type="file" questions:
| Key | Default | Description |
|---|---|---|
"max_entries" | 3 | Maximum number of files |
"max_file_size" | 10_000_000 | Maximum file size in bytes |
"accept" | :any | Accepted extensions/MIME types |
"bucket" | — | Cloud storage bucket |
"object_name_prefix" | "" | Prefix for stored object names |
"presigner" | — | %{"module" => ..., "function" => ...} returning a presigned URL |
Uploaded files are stored in the form data as maps with filename,
cloud_bucket, cloud_path, cloud_provider, and uploaded_on.
Custom field types
Registered as %{"type_name" => ecto_type} via the
:dynamic_form, :custom_field_types config and/or the custom_field_types
attribute (per-form entries win). The Ecto type drives casting ({:array, _}
types get checkbox-group-style param normalization); rendering dispatches to
the components module's input/1, matched by a
def input(%{type: "type_name"} = assigns) clause. Names colliding with
built-in types raise. Unregistered question types render nothing.
Components contract
Functions the renderer dispatches through the components module
(per-function fallback to DynamicForm.CoreComponents):
| Function | Renders | In Phoenix-generated CoreComponents? |
|---|---|---|
input/1 | text/email/number, textarea, select, checkbox controls | yes — works out of the box |
input_radio_group/1 | radiogroup and rating questions | no — built-in fallback |
input_checkbox_group/1 | multi-select checkbox groups | no — built-in fallback |
label/1, error/1 | around custom-control slot bodies | no — built-in fallback |
section/1 | panels | no — built-in fallback |
nested_entry/1 | the container around each repeating nested-form entry | no — built-in fallback |
button/1 | the submit button | yes — delegates |
translate_error/1 | error messages via the app's Gettext | yes — delegates |
Helper functions
| Function | Description |
|---|---|
DynamicForm.Instance.decode!/1 | JSON string or map → Instance struct |
DynamicForm.Instance.strip_slots/1 | Copy of an instance without slot bodies (definition-only comparison) |
DynamicForm.Instance.FromSlots.convert!/1 | Slot entries → Instance (used by DynamicForm.form/1) |
DynamicForm.Changeset.create_changeset/2 | Instance + params → Ecto changeset |
DynamicForm.Changeset.get_questions/1 | Flat list of questions, including nested panels |
DynamicForm.Payload.add_error/4 | Add a changeset error, marking the submission invalid |
DynamicForm.Payload.put_extra/3 | Stash derived data on the payload for the parent's handle_info/2 |
DynamicForm.form_data/1 | Inside a <:field> slot body: the whole form's current values, same shape as payload.data |
DynamicForm.submit_button/1 | Submit button component usable outside the form element |