<bmx-form>
A form drawn from JSON: fields, sections, repeating sections and pages, with conditions, worked-out values and checks written in a small, safe expression language - and the library's own fields to draw them, so every input, select, date picker and upload behaves as it does anywhere else.
18 properties · 7 events · 17 methods · 19 parts
Example
Show markup
<bmx-form id="ex-form" autosave="example-order" locale="en-GB" style="--bmx-form-max-width: 46rem"></bmx-form>
<div class="row" style="margin-block-start: 1rem">
<span class="note" id="ex-form-out">
A form in three steps, drawn from JSON. The address shows only for delivery, each line works out its amount, the
totals follow, and postage is free from £50. Leave a field to see its check; send the form to see the summary of
problems. Answers are kept as a draft in this browser.
</span>
</div>
<script type="module">
await customElements.whenDefined('bmx-form');
const form = document.getElementById('ex-form');
const out = document.getElementById('ex-form-out');
form.schema = {
title: 'Order stationery',
description: 'Tell us who you are, what you need, and where it should go.',
review: { title: 'Check your order', description: 'Change anything that is not right, then send it.' },
submitLabel: 'Send order',
successMessage: 'Thank you, {{ name }}. Your order for {{ TEXT(total, 2) }} pounds is on its way.',
pages: [
{
title: 'Your details',
fields: [
{ type: 'text', name: 'name', label: 'Full name', required: true, autocomplete: 'name', width: 'half' },
{ type: 'email', name: 'email', label: 'Email', required: true, width: 'half' },
{
type: 'radio', name: 'contact', label: 'How should we contact you?', orientation: 'horizontal',
options: [{ value: 'email', label: 'Email' }, { value: 'phone', label: 'Phone' }], defaultValue: 'email',
},
{ type: 'tel', name: 'phone', label: 'Phone number', requiredIf: "contact = 'phone'", visibleIf: "contact = 'phone'", width: 'half' },
{ type: 'select', name: 'team', label: 'Team', width: 'half', options: ['Finance', 'Operations', 'Sales', 'Support'] },
],
},
{
title: 'Your order',
fields: [
{
type: 'repeat', name: 'lines', label: 'Items', itemLabel: 'Item {{ $row }}', layout: 'table', minItems: 1, maxItems: 8, addLabel: 'Add another item',
fields: [
{
type: 'select', name: 'item', label: 'Item', required: true, width: 'half',
options: [
{ value: 'pens', label: 'Pens, box of 10 (£4.50)' },
{ value: 'pads', label: 'A4 pads, pack of 5 (£7.20)' },
{ value: 'folders', label: 'Folders, pack of 20 (£9.90)' },
{ value: 'stapler', label: 'Stapler (£12.00)' },
],
},
{ type: 'number', name: 'qty', label: 'Quantity', min: 1, max: 50, integer: true, defaultValue: 1, required: true, width: 'quarter' },
{
type: 'calculated', name: 'amount', label: 'Amount', format: 'currency', currency: 'GBP', width: 'quarter',
value: "qty * IFS(item = 'pens', 4.5, item = 'pads', 7.2, item = 'folders', 9.9, item = 'stapler', 12, true, 0)",
},
],
},
{ type: 'radio', name: 'delivery', label: 'Delivery', orientation: 'horizontal', defaultValue: 'collect',
options: [{ value: 'collect', label: 'Collect from reception' }, { value: 'post', label: 'Post it to me' }] },
{
type: 'section', name: 'address', label: 'Delivery address', visibleIf: "delivery = 'post'",
fields: [
{ type: 'text', name: 'line1', label: 'Address', required: true, autocomplete: 'address-line1' },
{ type: 'text', name: 'town', label: 'Town or city', required: true, width: 'half', autocomplete: 'address-level2' },
{ type: 'text', name: 'postcode', label: 'Postcode', required: true, width: 'half', autocomplete: 'postal-code',
pattern: '[A-Za-z]{1,2}\\d[A-Za-z\\d]? ?\\d[A-Za-z]{2}', messages: { pattern: 'Enter a postcode, like SW1A 1AA.' } },
{ type: 'date', name: 'date', label: 'Deliver on or after', min: '=ADDDAYS(TODAY(), 2)', width: 'half',
description: 'We need two days to pack an order.' },
],
},
{ type: 'calculated', name: 'subtotal', label: 'Items', value: 'SUM(lines.amount)', format: 'currency', width: 'third' },
{ type: 'calculated', name: 'postage', label: 'Postage', value: "IF(delivery = 'post' and subtotal < 50, 3.95, 0)", format: 'currency', width: 'third',
description: 'Free from £50.' },
{ type: 'calculated', name: 'total', label: 'Total', value: 'subtotal + postage', format: 'currency', width: 'third' },
],
},
{
title: 'Finish',
fields: [
{ type: 'rating', name: 'rating', label: 'How easy was this form?', scaleMax: 5 },
{ type: 'textarea', name: 'notes', label: 'Anything else?', maxLength: 300, visibleIf: 'rating > 0 and rating < 4',
description: 'Sorry it was not easier - tell us what would help.' },
{ type: 'signature', name: 'signature', label: 'Signed for by', required: true },
{ type: 'checkbox', name: 'agree', label: 'I will collect or receive this order for {{ COALESCE(team, "my team") }}', required: true },
],
},
],
};
form.addEventListener('bmxFormSubmit', event => {
out.textContent = `bmxFormSubmit - ${JSON.stringify(event.detail.data).slice(0, 220)}…`;
});
form.addEventListener('bmxFormInvalid', event => {
out.textContent = `bmxFormInvalid - ${event.detail.errors.length} problem(s).`;
});
</script>
THE SCHEMA
schema is { title, fields: [...] }, or { title, pages: [{ title, fields }] } for a form in steps. Each field has a type (text, email,
number, select, radio, checkboxes, checkbox, date, file,
signature, calculated, section, repeat and more), a name to file
its answer under, a label, and whatever its type takes. width lays
fields side by side (half, third); a narrow form stacks them.
LOGIC
visibleIf, enabledIf, requiredIf and readonlyIf are expressions:
contact = 'phone', age >= 18 and country in ['UK', 'IE']. value makes
a field hold a result (qty * price, SUM(lines.amount)), and options can
have a visibleIf of their own, or come from optionsUrl, read again when
the answers it names change. Labels and content take {{ placeholders }}.
Hidden answers count as blank and are not sent.
CHECKS
required, lengths, ranges (numbers or dates, fixed or =TODAY()),
patterns, email and web addresses, list sizes, file types and sizes, and
rules of your own ({ expression: "end >= start", message }). Messages
appear as each field is left, and all at once - with a summary that links
to each - when the form is sent or a page is left. validators add the
page's own checks, which may ask a server. The engine is a plain module, so
a server can check a submission against the same schema.
SENDING
bmxFormSubmit carries the answers; cancel it to send them yourself. With
action, the form posts them as JSON (or as multipart form data when there
are files) and shows the server's field errors - { errors: { path: message } } - against the fields. With name, the answers also go into an
enclosing <form> as JSON. autosave keeps a draft in the browser and
fills it back in.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
action |
action |
string |
— | A URL to send the answers to. Without one, the page handles bmxFormSubmit. |
autosave |
autosave |
string |
— | A name to keep a draft of the answers under in this browser, filled back in next time. |
disabled |
disabled |
boolean |
false |
Turns every field off. |
functions |
property only | Record<string, BmxFormFunction> |
— | Functions for expressions, by name: { VAT: (n) => n * 0.2 }. A property. |
hideActions |
hide-actions |
boolean |
false |
Leaves out the buttons, for a page that drives the form from script. |
label |
label |
string |
— | The form's accessible name, when it has no title. |
locale |
locale |
string |
— | The language numbers and dates are shown in. The reader's, by default. |
method |
method |
string |
'POST' |
The method action is sent with. |
name |
name |
string |
— | With a name, the answers go into an enclosing <form> as JSON under it. |
page |
page |
number |
0 |
The page on show, from 0. |
readonly |
readonly |
boolean |
false |
Shows the answers as text rather than as fields. |
requestInit |
request-init |
RequestInit | string |
— | Options for the request to action and to optionsUrl (headers, credentials), as a property or JSON. |
schema |
schema |
BmxFormSchema | string |
— | The form: { title, fields } or { title, pages }, as a property or JSON. |
src |
src |
string |
— | A URL to read the schema from as JSON. |
strings |
strings |
Partial<BmxFormStrings> | string |
— | Replacements for the form's own wording, as a property or JSON. |
validateOn |
validate-on |
'blur' | 'input' | 'submit' |
'blur' |
When a field shows its problem: once it is left (blur), as it is typed in (input), or only when the form is sent (submit). |
validators |
property only | Record<string, BmxFormValidator> |
— | The page's own checks, by field (email, or lines.qty for a column of rows). A property. |
value |
value |
Record<string, unknown> | string |
{} |
The answers, as a property or JSON. Changes write back here. |
Events
| Event | Detail | Description |
|---|---|---|
bmxFormChange |
BmxFormChangeDetail |
An answer changed. |
bmxFormInvalid |
BmxFormInvalidDetail |
The form was sent, or a page left, with problems. |
bmxFormPageChange |
BmxFormPageChangeDetail |
Another page is on show. |
bmxFormReset |
void |
The form was emptied back to its defaults. |
bmxFormSubmit |
BmxFormSubmitDetail |
The form is valid and about to be sent. Cancel it to send the answers yourself. |
bmxFormSubmitError |
BmxFormSubmitErrorDetail |
action refused the answers, or could not be reached. |
bmxFormSubmitted |
BmxFormSubmittedDetail |
action accepted the answers. |
Methods
| Method | Signature | Description |
|---|---|---|
clearErrors |
clearErrors() => Promise<void> |
Clears the messages set by setErrors. |
focusField |
focusField(path: string) => Promise<void> |
Moves focus to a field, by path, going to its page first. |
getData |
getData(all?: boolean) => Promise<Record<string, unknown>> |
The answers: by default as they would be sent (hidden ones left out); with all, every answer kept. |
getSummary |
getSummary() => Promise<BmxFormSummaryItem[]> |
The answers as a reader checks them: each field on show with its answer as text. |
getValue |
getValue(path: string) => Promise<unknown> |
One answer, by path (email, lines[0].qty). |
goToPage |
goToPage(page: number) => Promise<void> |
Goes to a page, from 0, without checking the one on show. |
load |
load(url: string) => Promise<void> |
Reads the schema from a URL. |
next |
next() => Promise<boolean> |
Checks the page on show and goes to the next one; false if the page has problems. |
previous |
previous() => Promise<void> |
Goes back a page. |
reportValidity |
reportValidity() => Promise<boolean> |
Checks the whole form and shows its problems; true when there are none. |
reset |
reset() => Promise<void> |
Empties the form back to its defaults, on its first page. |
setData |
setData(data: Record<string, unknown>, merge?: boolean) => Promise<void> |
Replaces the answers, or with merge adds to them. |
setErrors |
setErrors(errors: Record<string, string>) => Promise<void> |
Shows messages against fields, by path - a server's answer, typically. They clear as each field is changed. |
setValue |
setValue(path: string, value: unknown) => Promise<void> |
Sets one answer, by path, as though it had been typed. |
submit |
submit() => Promise<boolean> |
Checks the form and, when it passes, sends it: bmxFormSubmit, then action if there is one. |
toFormData |
toFormData() => Promise<FormData> |
The answers to send, as FormData (lines[0][qty]), files included. |
validate |
validate(page?: number) => Promise<BmxFormError[]> |
The problems, without showing them: the whole form's, or one page's. Waits for the page's own validators. |
CSS shadow parts
| Part | Description |
|---|---|
actions |
the buttons under the form. |
content |
text from the schema. |
error |
a message under a field the form draws itself. |
error-summary |
the list of problems shown when the form is sent. |
field |
the cell around one field. |
form |
the form itself. |
header |
the title and description. |
notice |
the "restored" notice and the sending error. |
output |
a worked-out value. |
page |
the fields of the page on show. |
progress |
the steps of a form in pages. |
repeat |
a repeating section. |
review |
the answers on the "check your answers" step. |
row |
one row of a repeating section. |
row-label |
a row's label. |
section |
a section. |
signature |
a signature box. |
step |
one step. Also step-current, step-done. |
success |
what shows once the form is sent. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-form-accent |
The progress steps and links. |
--bmx-form-border |
Lines around sections, rows and the summary. |
--bmx-form-error-color |
Messages the form draws itself, and the summary's edge. |
--bmx-form-gap |
The space between fields. |
--bmx-form-max-width |
The widest the form grows. |
--bmx-form-output-background |
Behind a worked-out value. |
--bmx-form-radius |
Corners of sections, rows and boxes. |
--bmx-form-section-background |
Behind a section and a repeating row. |