Skip to main content

Form

You describe the fields; the Form builds them, holds the values, shows the errors and tells you when a submit was valid.

Full name ┌──────────────────────────┐
│ Ada Lovelace │
Email ├──────────────────────────┤
│ ada@example.com │
Plan ├──────────────────────────┤
│ Free ▾ │
What are you ├──────────────────────────┤
building? │ │
└──────────────────────────┘
Updates ☐ Email me when a release ships

Out of the box it renders those five sample fields, so you see what it is the moment you drop it on a page.

Feeding it your fields​

The Controls input is an array of plain objects, one per field:

PropertyWhat it does
TypeWhich control to build — Text Input (the default), Text Area, Drop Down, Checkbox, Single Choice, Multi Choice, Slider, Date Input
LabelThe words above the field
PropertyThe key this field reads and writes on the form's value object — this is what ties a field to your data
ValueWhat the field starts out holding. Descriptor keys are copied over the control last, so this wins over the form's stored value
PlaceholderText Input / Text Area
InputTypeText Input — text, email, number, password, url
TextCheckbox — the sentence beside the box (the label above it stays Label)
Options / LabelsDrop Down, Single Choice, Multi Choice — the values, and optionally the words to show for them
Min / Max / StepSlider

Two ways to supply it:

  1. Connect the Controls input to any array — a Static Data node of your own, a Function output, a schema you fetched. A non-empty connected array wins over the built-in samples.
  2. Edit the samples: inside the Form component, the Sample fields Static Data node holds the demo JSON.

Inputs and outputs​

InputWhat it does
ControlsThe field descriptors above.
Object IdAn existing object to edit. Unset, the Form creates one and reports its id, so the same graph does create and edit.
ErrorsAn array of { Property, Error }. A field only shows its error once it has been edited — until then the form is not scolding you for fields you have not reached. Submit shows all of them.
SubmitSignal. Validates against Errors and fires one of the two outputs below.
OutputWhen
Valid SubmitSubmit fired and no error matched a field on this form.
Have ErrorsSubmit fired and something did. Every error becomes visible at that moment.
Object IdThe id of the object holding the values — wire it at a Set Record / Create Record.
Value Changed / PropertyA field was edited, and which one.

Things worth knowing before you change it​

The fallback is a Function, not a default. Choose controls takes the connected array and the samples and picks: a connected array with anything in it wins, an empty or absent one falls through. That is what makes the samples vanish the instant your data arrives rather than fighting it.

Static Data hands over records, not plain objects. Choose controls flattens each row before passing it on, because Build controls copies properties off the descriptor by name and a Noodl record answers those through a proxy rather than as own keys. Both sources therefore reach that script in one shape.

A control's words live on the control. The Checkbox sets Enable Label and takes its sentence through the control's own Label port, so the words render as a real <label> wired to the input and tapping them toggles the box. A Text beside a checkbox is not a click target — the hit area is the 24px square, and that is WCAG 2.2 SC 2.5.8's floor and nothing more. Text Input and Text Area also have Enable Label on now; before that their Label was wired and invisible, and every text field in the form rendered unlabelled.

A Drop Down needs a declared height, and the symptom does not look like one. The Options control's content is a single <span> that is empty until something is selected, so under a content-driven size mode the wrapper measures zero — and because that control opts into a default solid 2px #000000 border (unlike Text Input, which defaults to none), the collapse renders as a 4px black rule across the field. It reads like a broken node or a missing style; it is a missing height. This one is Size Mode: Explicit with a real 40px, which is what the two Dropdowns elsewhere in this library already do.

A throw is reported against the node that emitted, not the node that threw. This form spent two measured rounds on one error:

JavaScriptFunction (/Form/Set Form Value): The script threw:
Cannot set properties of undefined (setting 'Name')

Nothing in Set Form Value was wrong. Noodl.Events.emit dispatches synchronously (events.js calls each listener with ReflectApply, it is not a queue), so the listener's stack frame sits inside the emitting script and its throw unwinds into that node's try. The actual write was Component.Object.HaveValues[ev.Property] = true in Receive Value Changed, a different component, and 'Name' was the first field's Property rather than anything about a form value.

HaveValues is created by the initialiser on the root Group's Did Mount — and a repeated control mounts before its parent Group does, so the first field's value reaches the listener while the map is still undefined. It is created lazily there now. Build controls had guarded this same property with || {} from the start; the listener was the copy that forgot, which is the usual shape — one defensive reader and one that trusts an ordering nobody wrote down.

The lesson for anyone reading a Noodl stack trace: the component in the message is where execution was, not necessarily where the bug is. Search for the written property, not for the reported node.

Set Form Value also runs during mount, because its Value input changes before anybody types — Do is an additional trigger, not a replacement for one. Component.RepeaterObject and Component.ParentObject are guarded there for that reason.

Adding a control type means adding a component under /Form named exactly for the Type string — the repeater's template script resolves './' + item.Type — giving it a Model2 for the properties it reads, and ending its edits at a Set Form Value.