DynamicForm.NestedForms (DynamicForm v1.0.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 list_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

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.

Normalizes a paneldynamic value to a list of entries.

Builds one child changeset per entry of a paneldynamic question.

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 paneldynamic questions without initial data with panelCount fresh entries (at least minPanelCount), mirroring SurveyJS. Every entry is seeded separately so each gets its own id.

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

Walks the params tree along an entry path and updates the entry list at its end with fun. Path segments alternate between map keys (question names) and list indexes (entries).

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

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.

list_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.

list_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.Component) 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).

new_entry(question)

The initial params for a newly added entry.

Template questions' defaultValues seed the entry, overridden by the question's defaultPanelValue. The entry's dynamic_form_id is seeded separately by seed_entry_ids/2, which runs over the changeset's params — generating one here would leak into initial_data and reset the form on every parent re-render.

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_entries(params, questions)

Seeds paneldynamic questions without initial data with panelCount fresh entries (at least minPanelCount), mirroring SurveyJS. Every entry is seeded separately so each gets its own id.

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.

update_entry_list(params, list, fun)

Walks the params tree along an entry path and updates the entry list at its end with fun. Path segments alternate between map keys (question names) and list indexes (entries).

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.