Skip to content

Mental model

MLForm keeps four questions apart. What does the form mean? is answered by the schema. Where does each part appear? by the layout. Where do submitted values go? by the transport. What does it look like? by the design system. Changing one answer does not require changing the others: the same schema can render as one page or a wizard, submit to a test stub or a production model, and use any theme.

MLForm package layersNested rings: mlform/kit contains mlform/view, which contains mlform/runtime, which contains mlform/schema. Side packages: builtins supplies definitions to the runtime, primitives and design are used by the kit, and transport receives submissions from the runtime.mlform · mlform/kitmountForm(): built-in UI, layout hosts, lifecycle, designmlform/viewcreateFormView(): layout, navigation, descriptors, pluginsmlform/runtimecreateForm(): state, validation, conditions, submitmlform/schemaFormSchema, ids, mappedTo, registryThe contract every other layer readsmlform/builtins12 field kinds, 2 report kindsdefinitionsmlform/primitivesWeb Components for descriptorsrenderersmlform/designthemes, recipes, tokensstylesmlform/transportcontract, fanout, errorssubmit()
Each ring builds on the rings inside it. Start at the outermost ring that still gives you the control you need.
Surface Start here when you want Main entry
mlform (mlform/kit) A complete form in an element, with built-in UI and lifecycle. mountForm()
mlform/view Your own markup or framework, with MLForm’s layout, navigation, and descriptors. createFormView()
mlform/runtime State, validation, and submission with no UI at all. createForm()
mlform/schema Schema types, normalization, JSON Schema, and registries for tooling. validateSchema()
mlform/builtins The built-in field and report definitions as a registry. createBuiltinMlRegistry()
mlform/primitives MLForm’s Web Components inside your own mounting code. mountPrimitiveForm()
mlform/design Themes, recipes, and tokens applied to any element. attachDesignSystem()
mlform/transport The transport contract and helpers such as fan-out. createFanoutTransport()

The kit is built from the view, which is built from the runtime. Moving inward never changes field meaning, validation, or submitted values; you only take over more of the rendering. The Headless view and Runtime without UI pages show the inner rings.

What happens when a form is submittedTop row, left to right: field values keyed by runtime id are validated, turned into submission records with displayValues and modelValues, and passed to transport.submit. Bottom row, right to left: the backend response with report envelopes is routed by backend and mappedTo, report states settle, and reports render.RUNTIMEField valueskeyed by runtime id,normalized by definitionsRUNTIMEValidateconstraints, field andform validatorsSNAPSHOTRecordsdisplayValuesmodelValuesTRANSPORTsubmit(request)your HTTP client,worker, or local modelVIEW · KITRenderbuilt-in reports,plugins, or your UIRUNTIMEReport statesready · skipped ·error, per reportRUNTIMERoute resultsexact backend andmappedTo, never idsBACKENDResponse{ reports: [ … ] }explicit envelopes
A validation failure stops before the transport runs. A transport error puts every report of that submission in the error state.
  1. Values live in field controllers, keyed by runtime id and normalized by each kind’s definition: a number field holds a number, a date field a Date.
  2. Validation runs required checks, kind constraints, field validators, and form validators. Invalid forms stop here and the transport is never called.
  3. Submission records are built for active fields: inputs (one record per field), displayValues (keyed by displayKey), and modelValues (keyed by mappedTo).
  4. The transport receives those records in a frozen request and returns a response. It is the only part that talks to your backend.
  5. Routing gives each report the envelope whose backend and mappedTo match its own configuration. Ids and labels are never used to guess a match.
  6. Report states settle independently: one report can be ready while another is skipped. Reports covers the full state model.
Three identities on one fieldA field config with id customer-age, displayKey Customer age and mappedTo age_years. The id addresses runtime state and layout, the display key names the value in displayValues, and the mapped target names it in modelValues. The label is user-facing copy and is never used as a key.FIELD CONFIG{ id: "customer-age", displayKey: "Customer age", mappedTo: "age_years", label: "Age", kind: "number",}Runtime idform.getField("customer-age")layout refs, errors, focus, state, testsDisplay keydisplayValues["Customer age"] = 42review, persistence, export; omitted if unsetMapped targetmodelValues.age_years = 42what the transport sends to the modellabel is copy for people. It can change without touching any contract.
MLForm never derives one identity from another. A field without mappedTo or valuePath sends no model value.

Each field can carry three names, and each has one job:

  • Runtime id (id): the handle inside MLForm. Layout nodes, validation errors, focus, form.getField(id), and tests use it. Explicit ids are preserved exactly; when omitted, MLForm generates a lowercase kebab-case id from the label. Set explicit ids for anything you reference. See Schema.
  • Display key (displayKey): the name of the value in displayValues, for review screens, persistence, or export. Fields without one are left out of displayValues.
  • Mapped target (mappedTo): the name, numeric position, or per-backend map the model expects. Fields without mappedTo or valuePath send no model value.

Reports follow the same rule: a report’s id addresses it in MLForm, and its mappedTo selects the backend output it shows. Renaming a model feature means changing mappedTo, not the id that your layout and tests depend on.

Term Meaning
Schema The declarative description of fields, reports, conditions, and mappings.
Kind The registry key, such as number or classifier, that selects a definition.
Definition Registered behavior for one kind: config schema, value normalization, validation, serialization.
Registry The set of definitions a form can use. Built-ins and plugins register into it the same way.
Runtime Headless state, validation, conditions, report state, and submission.
View Headless layout, navigation, visibility, and render descriptors.
Descriptor A render-ready description of one field or report, produced by a presenter.
Primitive A built-in Web Component that renders a descriptor.
Kit mountForm: the view rendered with primitives, plus design system and lifecycle.
Transport The submit(request) boundary between MLForm and your backend.
Envelope One report result: { backend, mappedTo, status, payload? }.

Next, connect a real service in First backend.