Quick lookup tables. For narrative documentation see the Usage guide.

DynamicForm.form/1 attributes

AttributeTypeDefaultDescription
idstringrequiredComponent ID; also the instance id in declarative mode
instanceanynilData mode: Instance struct, JSON string, or map
jsonstringnilData mode: SurveyJS-compatible JSON string, decoded via Instance.decode!/1
titlestringnilInstance title (declarative mode)
descriptionstringnilInstance description (declarative mode)
on_changefunctionnil1-arity (payload) -> payload, after built-in validations on every change and during the submit validation pass
on_submitfunctionnil1-arity (payload) -> payload, on every submit — valid or not
on_successfunctionnil1-arity (payload), on every valid submission — replaces the default {:dynamic_form, payload} message
datamap%{}Initial form data for edit mode — existing record values; a payload's data round-trips directly
form_namestring"dynamic_form"Form namespace for params
submit_textstring"Submit"Submit button text
hide_submitbooleanfalseHide the built-in submit button
gettextatomDynamicForm.GettextGettext backend for translations
componentsatomnilCustom components module; falls back to the :dynamic_form, :components config, then the built-ins per function
custom_field_typesmapnilCustom field types (%{"name" => ecto_type}), merged over the :dynamic_form, :custom_field_types config
validation_summarystringnilErrors at top of form: nil, "simple", or "detailed"
render_onlybooleanfalseRender markup only: events go to the parent LiveView's handle_event/3; requires form
formPhoenix.HTML.FormnilRender-only mode: the parent-owned form to render against
phx_changestring"validate"Render-only mode: change event name
phx_submitstring"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, on_submit, on_success, 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

AttributeTypeApplies toDescription
typestringallRequired. One of the question or element types below
namestringallField name. Required for question types; auto-generated for html/image/custom
labelstringquestions, imageQuestion title / image alt text (→ title)
placeholderstringtext inputsInput placeholder
descriptionstringquestionsHelp text shown below the input
input_typestringtextHTML input type pass-through (email, number, ...)
defaultanyquestionsDefault value seeded into the form params (→ defaultValue)
optionslistchoice typesChoices: [{"Label", "value"}, ...] or ["value", ...] (→ choices)
requiredbooleanquestionsRequired field (→ isRequired)
required_ifstringquestionsConditional requirement expression (→ requiredIf)
visible_ifstringallConditional visibility expression (→ visibleIf)
enable_ifstringallConditional enablement expression (→ enableIf)
read_onlybooleanquestionsDisplay value without allowing edits (→ readOnly)
groupstringallCollect this field into the <:group> panel with this name
nestedstringall except fileData scope: collect this field into the <:nested> form with this name — see the Nested Forms guide
rate_min / rate_max / rate_stepintegerratingRating scale (defaults 1–5, step 1)
min_length / max_lengthintegertextBuilds a text validator
min / maxnumbernumericBuilds a numeric validator
patternstringtextBuilds a regex validator
formatstringtextFormat validator; supported: "email"
validatorslistquestionsEscape hatch: Instance.Validator structs or atom-keyed maps
htmlstringhtmlRaw HTML content (alternative to a slot body)
srcstringimageImage URL (→ imageLink); required
width / height / fitstringimageImage sizing (→ imageWidth/imageHeight/imageFit)
metadatamapallMetadata 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

AttributeTypeDescription
namestringRequired. Referenced by <:field group="...">; also the panel's name
titlestringPanel title
visible_ifstringConditional visibility expression
enable_ifstringConditional enablement (disables all contained questions when false)
nestedstringData 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.

AttributeTypeDescription
namestringRequired. Data key — the value is a list of entry maps
title / descriptionstringHeading and help text
entry_titlestringPer-entry heading; {panelIndex} interpolates the 1-based number (→ templateTitle)
entriesintegerEntries seeded on a fresh form (→ panelCount)
min_entries / max_entriesintegerCount limits: buttons hide, and submit validates (→ minPanelCount/maxPanelCount)
add_text / remove_textstringButton labels (→ addPanelText/removePanelText)
no_entries_textstringShown at zero entries (→ noEntriesText)
confirm_delete / confirm_textboolean / stringConfirmation before removing (→ confirmDelete/confirmDeleteText)
key / key_errorstringMember field unique across entries + error message (→ keyName/keyDuplicationError)
defaultlistInitial value: list of entry maps (→ defaultValue)
default_entrymapValues seeded into each newly added entry (→ defaultPanelValue)
requiredbooleanAt least one entry required (→ isRequired)
visible_if / enable_ifstringConditional expressions
nestedstringPlace this nested form inside another <:nested> form
groupstringPlace this nested form inside a <:group> panel

Question types

TypeRenders asNotes
text<input>input_type passes through (email, number, ...); number casts to decimal
comment<textarea>
dropdown<select>Requires options
radiogroupRadio buttonsRequires options; metadata "style": "vertical"/"horizontal"
checkboxCheckbox groupArray-valued; requires options
tagboxMulti-selectArray-valued; requires options
booleanSingle checkbox
ratingNumeric radio rowrate_min/rate_max/rate_step; casts to integer
fileDirect uploadPresigner + uploader required — see Usage: File uploads
paneldynamicRepeating child formCasts to a list of maps, validated per entry; <:nested> in declarative mode — see Nested Forms

Element types

TypeRenders asNotes
htmlRaw HTML or slot bodyString attr goes through Phoenix.HTML.raw/1; slot bodies are escaped HEEx
panelTitled containerDeclared via <:group> in declarative mode; nestable in data mode
image<img>src required
customSlot bodyDeclarative-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:

TypeFieldsFlattened attrs
textminLength, maxLengthmin_length, max_length
numericminValue, maxValuemin, max
emailformat="email"
regexregexpattern

input_type="email" also applies email format validation automatically.

Conditional expression operators

Used by visible_if, required_if, and enable_if:

CategoryOperators
Comparison=, ==, <>, !=, >, <, >=, <=
Presenceempty, notempty
Membershipcontains, notcontains, anyof, allof, noneof
Combinatorsand, or, parentheses

Field references use braces: {field_name}. Literals: 'strings', numbers, true/false, ['lists', 'of', 'values'].

Messages

Sent to the parent LiveView by default, on valid submissions only — invalid submissions render their errors inline and never message the parent. Defining on_success replaces the message with the callback:

{:dynamic_form, %DynamicForm.Payload{}}

%DynamicForm.Payload{} fields:

FieldValue
idThe form component's id
changesetThe form's final Ecto.Changeset; its valid? flag is the source of truth for validity (always true for delivered messages)
dataThe applied changeset data
extraEmpty 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 and during the submit validation pass. 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 default {:dynamic_form, payload} message; its return value is ignored.

Upload metadata keys

metadata map keys for type="file" questions:

KeyDefaultDescription
"max_entries"3Maximum number of files
"max_file_size"10_000_000Maximum file size in bytes
"accept":anyAccepted 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):

FunctionRendersIn Phoenix-generated CoreComponents?
input/1text/email/number, textarea, select, checkbox controlsyes — works out of the box
input_radio_group/1radiogroup and rating questionsno — built-in fallback
input_checkbox_group/1multi-select checkbox groupsno — built-in fallback
label/1, error/1around custom-control slot bodiesno — built-in fallback
section/1panelsno — built-in fallback
nested_entry/1the container around each repeating nested-form entryno — built-in fallback
button/1the submit buttonyes — delegates
translate_error/1error messages via the app's Gettextyes — delegates

Helper functions

FunctionDescription
DynamicForm.Instance.decode!/1JSON string or map → Instance struct
DynamicForm.Instance.strip_slots/1Copy of an instance without slot bodies (definition-only comparison)
DynamicForm.Instance.FromSlots.convert!/1Slot entries → Instance (used by DynamicForm.form/1)
DynamicForm.Changeset.create_changeset/2Instance + params → Ecto changeset
DynamicForm.Changeset.get_questions/1Flat list of questions, including nested panels
DynamicForm.Payload.add_error/4Add a changeset error, marking the submission invalid
DynamicForm.Payload.put_extra/3Stash derived data on the payload for the parent's handle_info/2
DynamicForm.submit_button/1Submit button component usable outside the form element