The selected block's config form (ADR-0005 decisions 9 and 13).
The form is a projection of current config, never a stateful mirror of
it: StatifierBlocks.ViewModel re-derives config_schema/1 against the
block's current config on every build, so a branch that gains an arm gains
a field on the very next render with nothing here to invalidate.
Two things this component does not do, both deliberate:
- It does not decide what reaches the document. Decision 9's gate -
an
:update_configis applied only whenvalidate_config/1returns:ok- lives inStatifierBlocks.Edit.check_config/3, whichStatifierBlocks.Edit.History.commit/4calls. This component posts params;StatifierBlocks.Editordecodes them and offers the command. - It does not render an unresolvable block's config. There is no
config_schema/1for one, and inventing a form would be guessing (decision 12), soStatifierBlocks.Editor.BlockNodeshows canonical JSON read-only instead.
form.unrouted renders at the head. That bucket exists because
Core.Branch.config_schema/1 keys one field per arm by the arm's own
slot name while validate_config/1 also emits findings keyed "arms" -
a key matching no field, because adding or removing an arm is a document
edit, not a form value. Rendering it here is what stops such a finding
from silently having nowhere to go.
Summary
Functions
One block's form: unrouted findings, then a control per schema field.
Decodes a phx-change payload into a config map, one field at a time
through StatifierBlocks.Editor.Field.decode/2.
Functions
One block's form: unrouted findings, then a control per schema field.
Attributes
node(StatifierBlocks.ViewModel.Node) (required)target(:any) (required)class(:string) - Defaults tonil.expression_component(:any) - Defaults tonil.
@spec decode( [StatifierBlocks.ViewModel.Field.t()], map(), StatifierBlocks.Block.config() ) :: StatifierBlocks.Block.config()
Decodes a phx-change payload into a config map, one field at a time
through StatifierBlocks.Editor.Field.decode/2.
Three properties, each of which is a bug if it is missing:
- It starts from
base, the config the block already carries. An:update_configcommand replaces a block's whole config, so a decode that built a fresh map from the schema alone would delete every key the schema does not name. ADR-0002 decision 7 makesconfig_schema/1a rendering hint rather than the authority, and ADR-0001 decision 9's principle - leave alone what you do not understand - applies to config keys as much as to block types. - It is keyed off the schema, not off the params. A param naming something the schema does not is ignored, so a crafted payload cannot inject config keys.
- A field whose control did not post keeps the value it had. A partially rendered form does not blank out the fields it did not show.
Where a decoded value is written
A field's key names its control; where the value goes is the field's
value_path (ADR-0002 decision 7, amended 2026-08-27), which is [key]
unless the block type said otherwise. Core.Branch is the one core type
that says otherwise: its per-arm condition fields keep their slot-name
keys and declare ["arms", i, "cond"], so a branch's conditions are
editable here without this component ever inferring anything from the
shape of a key. Writing through the path is also what stops a top-level
config["arm_approved"] from accumulating beside the arm the author
actually edited.