All notable changes to this project are documented here. For releases before 0.19.0, see the git history.
0.22.0
Added
Carry forward (SurveyJS's
choicesFromQuestion): a choice field can build its options from a<:nested>form's entries withchoices_from, labelled bychoice_text— a member field's name or a template interpolating several ("{min} - {max}","{panelIndex}"). Values default to the entry'sdynamic_form_id, so a selection survives edits to the entry it points at. Source names resolve innermost-first, so a nested form inside the same entry wins over a form-level one of the same name.no_choices_textreplaces a carried-forward field's control while its source has no entries yet, so an empty checkbox group can say "Add an age group above to assign it here" instead of rendering nothing.Carry forward from another choice field, not just a nested form: its options carry over, narrowed by
choices_mode—"all"(default),"selected", or"unselected"(SurveyJS'schoicesFromQuestionMode).Values a carried-forward source no longer offers are cleared during validation — a deleted entry, an option removed from the definition, or the source emptied entirely. Nothing is cleared when the form can't observe the source: no such question in the definition, or a submission that carries no values for it (hidden by
visible_if, say).<:group type="...">("groupType"in JSON) picks a group's layout:"horizontal"(default) puts its members on one row, wrapping as needed, and"vertical"stacks them. Members are sized by their content rather than split into equal columns, so a<:field>slot body can set an exact width and the row honors it. Applications add their own types by definingdynamic_form_group/1— it dispatches ontypeexactly likeinput/1.
Breaking
- The components-module function wrapping a group is
dynamic_form_group/1, notsection/1. It receivestype,title,name, anddisabledalongsideinner_block. As withinput/1, a module that exports it owns every group type, so end with a clause delegating toDynamicForm.CoreComponents.dynamic_form_group/1. - Groups arrange their members on one row by default. Existing groups that
should keep stacking need
type="vertical".
Changed
labelandtitleaccept a blank value —nil,false, or""— meaning "render no label", so a template can compute one without special-casing (label={@compact && gettext("Street")}). A blank label suppresses the required marker with it:requiredstill validates, but an asterisk with nothing to sit beside isn't rendered. Applies to<:field label>,<:group title>,<:nested title>and<:nested entry_title>, in data mode as well as declarative. Omitting the attribute is unchanged and still falls back to the capitalized field name; the attrs are typed:anyrather than:stringto allowfalse.- A nested form renders as a section: its
title(now an<h3>) anddescriptionon the left, its add button opposite them on the right rather than below the entries. The title no longer routes through the components module'slabel/1— a repeating section is a heading, not a label for a single input. - A nested form's add button renders through the components module's
button/1instead of raw markup, so it picks up the application's button styling like the submit button does. It receives itsphx-clickinrest, so a custombutton/1must splat globals (<button {@rest}>). Both button dispatches now pass the same assigns:type,disabled,rest,inner_block. - A nested-form entry is laid out in two columns: its title and child fields
on the left, its remove control on the right, top-aligned with the first
field. The control is a trash icon rather than a text button, and its
column collapses when the entry can't be removed — so an untitled entry
spends no vertical space on a header row at all.
remove_text(removePanelText) still applies, now as the icon's tooltip and screen-reader name. The icon is an inline SVG rather than ahero-*class, so it renders in apps that don't vendor heroicons. - A choice field no longer requires an
optionslist when it has a slot body rendering its own choices, or achoices_fromsource.
Fixed
- A question's title and description are escaped when composed with the
library's own markup. Building the required marker and a checkbox's inline
description interpolated them into a raw HTML string, so a definition loaded
from storage could inject markup into the page. A title deliberately wrapped
in
Phoenix.HTML.raw/1still renders as markup, which is now the way to opt in. Every other title and description already went through HEEx and was never affected. - Fields inside a
<:group>render like fields anywhere else. The group's contents took a shortcut past the wrapper every other field goes through, so two things silently didn't work inside one:no_choices_textnever replaced an empty carried-forward control, and a read-only choice field lost the hidden input carrying its value — so it was dropped on the next change.
0.21.0
Added
- Nested form entries carry a stable
dynamic_form_id, so a value referencing an entry survives edits and reordering. An entry loaded fromdatawith anidadopts it; one the user adds gets a generated id. The field round-trips through a hidden input and appears inpayload.data— it is only stable across sessions if the application persists it and passes it back indata. Opt out per nested form withgenerate_ids={false}("generateIds": false).
Fixed
- Hiding a section no longer rewinds it to the data the form was loaded
with. A section hidden by
visible_ifsubmits nothing, and the gap was being filled from the originaldata— discarding edits made while it was visible, and its entries' ids along with them. The gap is now filled from what the form is currently holding, falling back to the loaded data only for keys the form has never held. A question disabled byenable_ifkeeps its last value for the same reason, rather than reverting. Still section by section: a question hidden inside a repeating entry loses its value. - Read-only values are no longer dropped inside nested entries.
readOnlyrendered as an HTMLdisabledinput, which browsers don't submit, and the initial-data merge that covered for that restores top-level keys only — so a read-only value inside a nested entry was lost on the first change. Text controls now renderreadonly(still submitted), and controls HTML has noreadonlyfor render disabled alongside a hidden input carrying the value. Questions disabled byenableIfare unaffected: they remain excluded from the params.
0.20.0
Added
DynamicForm.form_data/1: inside a<:field>slot body, the whole form's current values as a map — the same shape aspayload.data. Always form-level, so a control inside one nested form can read another's entries.
Fixed
- The Usage guide's
type="custom"example bound:let={field}and readfield.form. That body receives the form itself, so the example raisedKeyErroras written.
0.19.0
Breaking
- Parent LiveView messages carry the lifecycle event:
{:dynamic_form, payload}is now{:dynamic_form, event, payload}. Update existing handlers tohandle_info({:dynamic_form, :success, payload}, socket)— an unmatched message raisesFunctionClauseErrorrather than failing quietly. :changeand:submitpayloads can be invalid. The previous guarantee that the parent only ever hears about valid submissions holds for:successonly.
Added
send_message_onattribute: the lifecycle events that message the parent LiveView, any of[:success, :change, :submit](default:[:success]). Listing:successalongsideon_successraises, sinceon_successreplaces that message.change_debounce_in_msattribute: milliseconds of quiet before a change runson_changeand sends its:changemessage. The built-in validations still render on every change, and submitting always runs the change pass inline.
Changed
- Adding or removing a nested entry now runs the change pass —
on_changeand the:changemessage — like any other change to the form's data.