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

Note:: the Public API is what is referenced below. There may be additional functions marked as public within modules. These are for internal use and are subject to change. If you find yourself needing to use one of these functions, please open a GitHub issue so we can explore if there's a better solution or a way to surface it as a stable public function moving forward.

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, parsed via Parser.FromData.parse!/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
change_debounce_in_msintegernilMilliseconds of quiet before a change runs on_change and sends its :change message. nil and 0 run both on every change
on_submitfunctionnil1-arity (payload) -> payload, on every submit — valid or not
on_successfunctionnil1-arity (payload), on every valid submission — replaces the :success message
send_message_onlist[:success]Lifecycle events that message the parent: any of [:success, :change, :submit]
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, change_debounce_in_ms, on_submit, on_success, send_message_on, data, form_name, validation_summary) and file upload questions — both raise.

DynamicForm.Renderer.LiveComponent (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
labelanyquestions, imageQuestion title / image alt text (→ title). Blank (nil, false, "") renders no label and no required marker; omitting it falls back to the capitalized name
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)
choices_fromstringchoice typesCarry forward: build choices from a <:nested> form's entries (→ choicesFromQuestion). Excludes options
choice_textstringchoice typesLabel for each carried choice: a member field name, or a template — "{min} - {max}", "{panelIndex}" (→ choiceTextsFromQuestion). Required with choices_from
choice_valuestringchoice typesValue for each carried choice (→ choiceValuesFromQuestion; default: the entry's dynamic_form_id). Nested sources only
choices_modestringchoice typesCarrying forward from another choice field: "all" (default), "selected", "unselected" (→ choicesFromQuestionMode)
no_choices_textstringchoice typesShown in place of the control while a carried-forward source has no entries (→ noChoicesText)
requiredbooleanquestionsRequired field (→ isRequired). Renders the HTML required attribute on the control, plus a mark beside the label
required_labelanyquestionsThe mark beside a required label (default "*"); blank (nil, false, "") shows none while the field stays required (→ requiredLabel)
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
titleanyPanel heading. Blank (nil, false, "") renders none
typestringLayout: "horizontal" (default, members share a row and wrap) or "vertical", or a type your components module defines (→ groupType)
visible_ifstringConditional visibility expression
enable_ifstringConditional enablement (disables all contained questions when false)
groupstringPlace this group inside another <:group> panel; it must declare the same nested scope as that parent
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 / descriptionany / stringSection heading and help text, rendered above the entries with the add button opposite them. Ordinary values, so title={gettext("Age groups")} works; a blank title (nil, false, "") renders no heading
entry_titleanyPer-entry heading; {panelIndex} interpolates the 1-based number (→ templateTitle). Blank renders none
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). Remove is an icon button, so its label becomes the tooltip and screen-reader name
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)
generate_idsbooleanSeed each entry with a stable dynamic_form_id (default true; → generateIds)
defaultlistInitial value: list of entry maps (→ defaultValue)
default_entrymapValues seeded into each newly added entry (→ defaultPanelValue)
requiredbooleanAt least one entry required (→ isRequired)
required_labelanyMark beside the section heading when required (default "*"); blank shows none (→ requiredLabel)
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 both modes. groupType picks the layout: "horizontal" (default) or "vertical"
image<img>src required
customSlot bodyFromComponent-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, carrying the lifecycle event and the payload:

{:dynamic_form, event, %DynamicForm.Payload{}}
EventSentIn send_message_on by default?
:successOn a valid submission (replaced by on_success when defined)yes
:changeOn every change, after the built-in validations and on_changeno
:submitOn every submit — valid or not — after on_submitno

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:

FieldValue
idThe form component's id
changesetThe form's Ecto.Changeset; its valid? flag is the source of truth for validity (always true for :success)
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 (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:

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
dynamic_form_group/1groups (panels)no — 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.Parser.FromData.parse!/1JSON string or map → Instance struct
DynamicForm.Instance.strip_slots/1Copy of an instance without slot bodies (definition-only comparison)
DynamicForm.Parser.FromComponent.parse!/1Slot entries → Instance (used by DynamicForm.form/1)
DynamicForm.Changeset.create_changeset/2Instance + params → Ecto changeset
DynamicForm.Changeset.list_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.form_data/1Inside a <:field> slot body: the whole form's current values, same shape as payload.data
DynamicForm.submit_button/1Submit button component usable outside the form element