DynamicForm.NestedForms (DynamicForm v0.21.0)

Copy Markdown View Source

Nested/repeating child forms — the SurveyJS paneldynamic question type.

A paneldynamic question holds a repeating template (templateElements); its value is a list of entries, one map per repetition. This module owns the entry machinery shared by validation and rendering:

Entry changesets are ordinary schemaless changesets built recursively via DynamicForm.Changeset.create_changeset/3 — there is no Ecto relation (no cast_embed/inputs_for) involved. Because entries aren't tracked inside the parent changeset, they are derived state: a pure function of the question and the parent's raw params. Validation and rendering both call entry_changesets/3, so the errors they see are always identical.

SurveyJS vocabulary ("panel") appears only at the boundary — the question type string, the Instance.Question fields, and {panel.field} expression scoping. Internally these are entries of a nested form.

Summary

Functions

Normalizes a paneldynamic value to a list of entries.

Builds one child changeset per entry of a paneldynamic question.

Resolves the paneldynamic question at a dot-separated entry path.

Whether a paneldynamic question seeds entry ids. On unless generateIds is explicitly false.

The entry field carrying a stable id, when generateIds is on.

The initial params for a newly added entry.

Converts indexed-map values (as submitted by the browser) into ordered entry lists for every paneldynamic question, so the {:array, :map} cast succeeds and changeset.params holds a stable shape.

Seeds dynamic_form_id on every entry of the given questions' values.

Validates every paneldynamic question on an already-cast parent changeset: entry changesets (validity propagates to the parent), isRequired, minPanelCount/maxPanelCount, and replaces the raw cast value with each entry's applied data.

Functions

entries(value)

Normalizes a paneldynamic value to a list of entries.

Browser submissions arrive as an indexed map (%{"0" => %{...}, "1" => %{...}}, possibly with non-integer bookkeeping keys such as the always- present __empty__ hidden input); programmatic values are already lists.

entry_changesets(question, parent_params, opts \\ [])

Builds one child changeset per entry of a paneldynamic question.

Each entry of the question's value is validated against the question's templateElements with the same rules as a top-level form (casting, required fields, validators, conditional expressions), recursively — so nested paneldynamic questions work too. Conditional expressions inside the template can reference sibling values as {panel.field} (or plain {field}), and form-level values by their names.

When the question defines keyName, entries duplicating another entry's value for that field get an error (message: keyDuplicationError).

Both validation (DynamicForm.Changeset.create_changeset/3) and rendering (DynamicForm.Renderer) call this with the parent's raw params, so the changesets — and their errors — are identical in both places.

parent_params is the parent changeset's params map; the entry list is read from it under the question's name (either a list or a %{"0" => ...}-indexed map as submitted by the browser).

find_question(elements, path)

Resolves the paneldynamic question at a dot-separated entry path.

Path segments alternate question names and entry indexes ("addresses", or "contacts.0.phones" when nested). Each name resolves within its own scope — the top-level elements for the first segment, then each matched question's templateElements — so questions in different scopes may share a name. Returns nil when the path doesn't resolve.

generate_ids?(question)

Whether a paneldynamic question seeds entry ids. On unless generateIds is explicitly false.

id_field()

The entry field carrying a stable id, when generateIds is on.

Entries are otherwise positional, so a value referencing one (a checkbox listing another nested form's entries, say) has nothing durable to point at: a member field changes when the user edits it, and an index changes when entries are added or removed. This field is seeded once — copied from the entry's id when the data came from a stored record, generated otherwise — and round-trips through a hidden input.

It is only stable across sessions if the application persists it and passes it back in data.

new_entry(question)

The initial params for a newly added entry.

Template questions' defaultValues seed the entry, overridden by the question's defaultPanelValue, plus a generated dynamic_form_id unless the question sets generateIds to false.

normalize_params(params, questions)

Converts indexed-map values (as submitted by the browser) into ordered entry lists for every paneldynamic question, so the {:array, :map} cast succeeds and changeset.params holds a stable shape.

seed_entry_ids(params, questions)

Seeds dynamic_form_id on every entry of the given questions' values.

Runs over string-keyed params when a form is built or reset, recursing into nested templates. An entry that already carries an id keeps it; one carrying an id from a stored record adopts that; anything else — an entry the user created this session — gets a generated one.

Generation happens here rather than at render time on purpose: a fresh id per render would be worse than no id at all.

validate(changeset, questions, opts)

Validates every paneldynamic question on an already-cast parent changeset: entry changesets (validity propagates to the parent), isRequired, minPanelCount/maxPanelCount, and replaces the raw cast value with each entry's applied data.