Renderers
FormScript does not describe a page. It describes a form, and a renderer turns that form into whatever the platform wants: a web page, a phone screen, a kiosk, a printable document.
The rule that makes this work is in §15.2: FormScript emits intents, never
markup. A runtime produces a tree of intents, and a renderer decides what
each one becomes. The language never says "a <div>"; it says "a group".
The intents
There are fourteen of them, and they are the whole interface between a runtime and a renderer.
| Intent | What it is |
|---|---|
RenderTextInput |
free text |
RenderNumberInput |
a number, formatted for the locale |
RenderChoice |
a constrained selection |
RenderMultiSelect |
several of many |
RenderDate |
a date |
RenderToggle |
a boolean |
RenderFile |
an attachment |
RenderSignature |
a signature |
RenderLocation |
a place |
RenderGroup |
a labelled composite |
RenderRepeat |
a list that can grow |
RenderProgress |
how far through |
RenderErrorSummary |
why the submit failed |
RenderMessage |
an inline message |
A renderer must map every intent it receives. An unmapped intent is a
conformance failure, and the runtime surfaces it as a warning rather than
silently dropping the field — a form that quietly loses a question is worse
than one that complains. checkIntentCoverage is what checks this.
A renderer may map an intent to a different widget, as long as the mapping
preserves the intent's accessibility, validation and keyboard semantics. One
mapping is forbidden outright: RenderChoice must not become a control that
cannot be operated by the keyboard alone.
The obligations
Spec-15.4 says a Level 2+ renderer must:
- maintain a labelled control for every active field;
- expose field state to assistive technology per §13;
- never render a value in a way that omits its label;
- handle the
pendingsource state without trapping focus; - preserve field identity across re-renders, so focus is not lost mid-typing.
Spec-15.5 adds the rule that catches the usual mistake: a renderer must preserve input focus and cursor position across a state propagation. A renderer that re-renders the whole form on every keystroke and loses focus is non-conforming.
checkRenderObligations checks a rendered tree against these.
The renderers that exist
| Package | What it does | The API |
|---|---|---|
@formscript/render |
The intents themselves: how a field becomes one, and the checks above | computeRenderIntent, findRenderNode, checkIntentCoverage, checkRenderObligations |
@formscript/render-dom |
The browser binding. It owns the DOM, and it reconciles rather than redraws | renderForm, createWidget, reconcile, setControlValue |
@formscript/render-html |
The server-side binding: HTML for a form, and the posted form read back into a transaction | renderFormHtml, renderControl, readPostedForm, postedModelPaths |
The DOM binding is the one that powers the playground and the live form. The HTML binding is what makes a form work with no JavaScript at all — see server rendering.
Neither of them is "the" renderer. They are two answers to the same contract, which is the point of having a contract.
What does not exist yet
No renderer is written for PDF, for printing, for React Native, for Android, or for email. §15.1 names a web renderer and React Native as what level 2 looks like in practice, and only the web one is here.
Writing another one is a bounded job: compute the intents, map each one to a widget, and honour the obligations. The language does not change, and neither does the runtime — a renderer is a consumer of the runtime, not a part of it.
See also
- Rendering — the same contract, as the course teaches it
- Server rendering — a form with no JavaScript
- Integrations — what a host must supply around a renderer
- The packages — every package, with its API