Current section
Files
Jump to
Current section
Files
dynamic_form
CHANGELOG.md
CHANGELOG.md
# Changelog
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 with `choices_from`, labelled
by `choice_text` — a member field's name or a template interpolating
several (`"{min} - {max}"`, `"{panelIndex}"`). Values default to the
entry's `dynamic_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_text` replaces 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's `choicesFromQuestionMode`).
- 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 defining
`dynamic_form_group/1` — it dispatches on `type` exactly like `input/1`.
### Breaking
- The components-module function wrapping a group is `dynamic_form_group/1`,
not `section/1`. It receives `type`, `title`, `name`, and `disabled`
alongside `inner_block`. As with `input/1`, a module that exports it owns
every group type, so end with a clause delegating to
`DynamicForm.CoreComponents.dynamic_form_group/1`.
- Groups arrange their members on one row by default. Existing groups that
should keep stacking need `type="vertical"`.
### Changed
- `label` and `title` accept 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: `required` still 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 `:any` rather than
`:string` to allow `false`.
- A nested form renders as a section: its `title` (now an `<h3>`) and
`description` on the left, its add button opposite them on the right rather
than below the entries. The title no longer routes through the components
module's `label/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/1` instead of raw markup, so it picks up the application's button
styling like the submit button does. It receives its `phx-click` in `rest`,
so a custom `button/1` must 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 a `hero-*` class,
so it renders in apps that don't vendor heroicons.
- A choice field no longer requires an `options` list when it has a slot body
rendering its own choices, or a `choices_from` source.
### 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/1` still 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_text` never
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 from
`data` with an `id` adopts it; one the user adds gets a generated id. The
field round-trips through a hidden input and appears in `payload.data` —
it is only stable across sessions if the application persists it and
passes it back in `data`. Opt out per nested form with
`generate_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_if` submits nothing, and the gap was
being filled from the original `data` — 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 by `enable_if` keeps
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. `readOnly`
rendered as an HTML `disabled` input, 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 render `readonly` (still submitted), and controls HTML has no
`readonly` for render disabled alongside a hidden input carrying the value.
Questions disabled by `enableIf` are 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 as `payload.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 read
`field.form`. That body receives the form itself, so the example raised
`KeyError` as written.
## 0.19.0
### Breaking
- Parent LiveView messages carry the lifecycle event:
`{:dynamic_form, payload}` is now `{:dynamic_form, event, payload}`. Update
existing handlers to `handle_info({:dynamic_form, :success, payload}, socket)` —
an unmatched message raises `FunctionClauseError` rather than failing quietly.
- `:change` and `:submit` payloads can be invalid. The previous guarantee that
the parent only ever hears about valid submissions holds for `:success` only.
### Added
- `send_message_on` attribute: the lifecycle events that message the parent
LiveView, any of `[:success, :change, :submit]` (default: `[:success]`).
Listing `:success` alongside `on_success` raises, since `on_success`
replaces that message.
- `change_debounce_in_ms` attribute: milliseconds of quiet before a change
runs `on_change` and sends its `:change` message. 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_change`
and the `:change` message — like any other change to the form's data.