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.invoke_types(:list) - Passed through toStatifierBlocks.Editor.Field; see its moduledoc. Defaults to[].path_candidates(:list) - Passed through toStatifierBlocks.Editor.Field; see its moduledoc. Defaults to[].pending(:list) - The fields whose typed value is not in the document, in schema order. Empty when the block has no outstanding draft.Defaults to
[].
@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.
The one value that is not written at all
An empty :duration omits its key rather than storing ""
(ADR-0005 decision 9, amended 2026-08-29). A cleared field and a
never-set field are the same value - there is no PT0S and no third
state for "the author touched this and then did not finish" - so the
two have to produce the same config, and the only config an absent key
can produce is one without the key.
This is the only place that can perform it. Field.decode/2 returns a
value and every value it could return is a value the key would then
hold; whether a key exists is a property of the map, which is this
function's to write. The omission is still the ordinary path in every
other respect: the resulting config goes to the same whole-config gate
through StatifierBlocks.Edit.check_config/3, and a required duration
left empty is refused there rather than here.