StatifierBlocks.Editor.ConfigForm (StatifierBlocks v0.7.0)

Copy Markdown View Source

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:

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

config_form(assigns)

One block's form: unrouted findings, then a control per schema field.

Attributes

  • node (StatifierBlocks.ViewModel.Node) (required)

  • target (:any) (required)

  • class (:string) - Defaults to nil.

  • expression_component (:any) - Defaults to nil.

  • 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 [].

decode(fields, params, base \\ %{})

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_config command 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 makes config_schema/1 a 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.