FS FormScript
Package@formscript/render-html
Specification7.12, 13, 13.4, 13.6, 15.2, 15.3
Depends ona11y, ast, engine, render, validation

@formscript/render-html

The server-side HTML binding of FormScript render intents, per §15.2 and §15.3 of the specification.

@formscript/render computes which widget a field needs; this package turns that intent into HTML markup a server writes. It is the counterpart of @formscript/render-dom: the same intents, the same obligations, and no browser in the loop. A server renders a form, the person fills it in, the browser posts it back, the server evaluates it and renders the answer.

Rendering a form

ts
import { renderFormHtml } from '@formscript/render-html';

const html = renderFormHtml({
  document: parsed.document,
  engine,
  action: '/loan-application',
  method: 'post',
  submitted: true,
  result,
});

The result is a <form> element and nothing else: the markup a host puts on a page. The engine keeps the values, the validity and the conditions; the renderer only presents them.

Reading a posted body

ts
import { readPostedForm } from '@formscript/render-html';

const posted = readPostedForm(body, engine);
engine.applyInput(posted.assignments);
if (posted.repeat !== null) engine.mutateRepeat(posted.repeat.path, posted.repeat);

readPostedForm reads the body against the model of the engine, so a checkbox that was not ticked still writes false, and a multiple choice that chose nothing still writes an empty list. posted.repeat is the change a repeat button asked for, if the body asked for one.

The widgets

Intent Widget
RenderTextInput <input type=text>
RenderNumberInput <input type=number>
RenderChoice a radio group, or a <select> above six options (§7.12)
RenderMultiSelect a checkbox group, or a <select multiple> above six options
RenderDate <input type=date>, or datetime-local for a datetime
RenderToggle <input type=checkbox> with a hidden answer beside it
RenderFile <input type=file>
RenderSignature a text control
RenderLocation a pair of number controls
RenderGroup <fieldset> with a <legend> (§13.6)
RenderRepeat a repeated <fieldset> with add and remove buttons
RenderProgress <progress>

Every intent of §15.2 has a widget, as Spec-15.2 requires. A field whose declaration selects no intent is not dropped silently: the renderer says so in the form.

The obligations

  • A labelled control for every active field (Spec-15.4). A field with no label shows its name. A group, a repeat and a radio group carry their label in a <legend>, so the label is never a floating text.
  • The field state is exposed (Spec-15.4, §13). The control carries aria-required, aria-invalid and aria-describedby; the hint, the help and the error message are the elements it points at.
  • A value never loses its label (Spec-15.4). The label is outside the control, so no value is ever rendered without one.
  • A failed submit renders the summary of §13.4: every failing field is listed at the top of the form, each entry links to its field, and the same message is rendered inline at the field.

Judgement calls

The specification does not say how a server-side renderer binds these intents to HTML. The choices this package makes are recorded here.

  • The identity of a control is its path. The id of a control is the path the engine addresses the field by, joined with dots: household.0.income. The name it posts under is the same path, so the body a browser sends needs no translation beyond the one readPostedForm does.
  • A choice above six options becomes a select (§7.12), a radio group below it. The threshold is the one @formscript/a11y uses for the accessible tree, so the markup and the accessibility tree agree.
  • A toggle carries a hidden answer. A box that is not ticked posts nothing, so a hidden false stands in for it and the field is never left with the value it had before.
  • A repeat is changed by the server. The add and remove buttons are submit buttons, not script: they post __fs_repeat and the server calls mutateRepeat. A form served without JavaScript can still change the shape of a repeat.
  • A signature field holds typed text. A server has no canvas to draw on, so the signature is typed; a browser host uses @formscript/render-dom instead.
  • A location field holds { latitude, longitude }, with either part null when it was not given. The two parts post under their own names.
  • A when gate removes the field from the form (Spec-8.3). The renderer reads the applicability the engine holds, so a field that comes back keeps its place in the document order.
  • The browser does not judge the form. The form carries novalidate, so the only messages a person sees are the ones the specification defines, in the words the error catalogue holds.

Public API

17 exported symbols.

attributes function

Builds an attribute list; an attribute of no value is written on its own.

ControlContext interface

Everything the control renderer needs to write one control.

displayText function

The text a control shows for the value the engine holds.

escapeHtml function

Escapes text so that it can be placed in HTML text or in an attribute value.

join function

Joins the parts of a markup fragment.

PostedBody interface

The body a browser posts; a name may carry one value or several.

PostedForm interface

What a posted body asks the engine to do.

postedModelPaths re-export
readPostedForm re-export
readRepeatAction re-export
renderControl function

Builds the control an intent maps to (§15.2), as markup.

RenderedControl interface

The markup of a control, and the way its label is attached.

renderFormHtml function

Renders a FormScript document as a form, as markup a server writes.

RenderFormHtmlOptions interface

Everything the server-side renderer needs to write a form.

REPEAT_FIELD re-export
RepeatAction interface

The change a repeat button asks for.

ResolvedOption interface

An option of a choice set, with the label a person reads.