Form
A form that checks what the person entered before it lets it through. On submit it runs your rules: a valid form fires submit with the values, an invalid one fires invalid with the errors and moves focus to the first field with one.
The preview needs JavaScript. The code is in the Code tab.
<script>
const values = state({ email: '', plan: '' })
const errors = state({})
const rules = {
email: (v) => (/^\S+@\S+\.\S+$/.test(v) ? '' : 'Enter an email such as ada@example.com.'),
plan: (v) => (v ? '' : 'Choose a plan.'),
}
const plans = [
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
]
function typed(value) {
values.value = { ...values.value, email: value }
}
function chose(detail) {
values.value = { ...values.value, plan: detail.value }
}
function invalid(detail) {
errors.value = detail.errors
}
function sent() {
errors.value = {}
}
</script>
<template>
<Form :values="values" :rules="rules" @invalid="invalid($event)" @submit="sent()">
<TextField :error="errors.email" @input="typed($event)">
<span slot="label">Email</span>
</TextField>
<RadioGroup :options="plans" :error="errors.plan" @choose="chose($event)">
<span slot="label">Plan</span>
</RadioGroup>
<Button type="submit">Sign up</Button>
</Form>
</template>
Installation
npx @pixelscriptio/tessera add Formpnpm exec tessera add Formyarn tessera add Formbunx @pixelscriptio/tessera add FormCopy and paste the code into your project
Put it in
system/components. Add Button, TextField the same way.system/components/Form.tess <contract> { "name": "Form", "role": "form", "description": "A form that checks what the person entered before it lets it through. On submit it runs your rules: a valid form fires submit with the values, an invalid one fires invalid with the errors and moves focus to the first field with one.", "since": "0.8.0", "parts": ["root"], "slots": { "default": "the fields and the buttons" }, "events": { "submit": "fired with { values } when the form is submitted and every rule passes", "invalid": "fired with { errors } when it is submitted and a rule fails: errors maps a field name to its message" }, "preview": { "slots": { "default": [ { "component": "TextField", "slots": { "label": "Email", "hint": "We only use it to sign you in." } }, { "component": "Button", "props": { "type": "submit" }, "slots": { "default": "Sign up" } } ] } } } </contract> <script> // `values` is the form's data, by field name. `rules` maps a field name to a function that takes the field's value and all // the values, and returns a message when it is wrong, or an empty string (or nothing) when it is fine. const { values, rules, busy } = props({ values: {}, rules: {}, busy: false }) const formEl = state(null) // The first field marked invalid gets focus, once the page has shown the errors. function focusFirstInvalid() { const root = formEl.value if (!root) return const first = root.querySelector('[aria-invalid="true"]') if (!first) return const target = first.matches('input, textarea, select, button') ? first : first.querySelector('input, textarea, select, button') if (target) target.focus() } function submit(event) { event.preventDefault() const data = values.value ?? {} const errors = {} for (const [name, rule] of Object.entries(rules.value ?? {})) { const message = rule(data[name], data) if (message) errors[name] = message } if (Object.keys(errors).length) { emit('invalid', { errors }) setTimeout(focusFirstInvalid, 0) } else { emit('submit', { values: data }) } } </script> <template> <form class="root" role="form" novalidate :aria-busy="busy ? 'true' : null" ref="formEl" @submit="submit($event)"> <slot /> </form> </template> <style> .root { display: flex; flex-direction: column; gap: $space.4; font-family: var(--t-font-family-sans); color: $color.text.default; } </style>Check your design tokens
Your design system needs these.
tessera addreports the ones that are missing; by hand, runnpx @pixelscriptio/tessera check.color.text.defaultspace.4
Usage
Using Form
A form that checks what was entered before it lets it through.
You give it the form's values and its rules. When the person submits (a button of type submit, or Enter in a field), it runs each rule. If every rule passes, it fires submit with the values. If one fails, it fires invalid with the errors, and moves focus to the first field marked invalid, so a keyboard or screen reader user lands on the first thing to fix.
- A rule is a function from a field's value (and all the values) to a message. Return an empty string, or nothing, when the value is fine.
Formdoes not draw errors. You keep them in state and hand each to its field'serrorprop (TextField, Textarea, RadioGroup, FormField). Theinvalidevent gives you all of them at once.- Browser validation is off (
novalidate), so the messages are yours and say what you want in the words you want. - Say what is wrong and how to fix it: "Enter an email such as ada@example.com", not "Invalid". Show an error only after the person has tried to submit or left the field, not while they are still typing.
busymarks the form as busy for assistive technology while it is being sent. Disable the submit button yourself.
The preview needs JavaScript. The code is in the Code tab.
<script>
const values = state({ email: '', plan: '' })
const errors = state({})
const rules = {
email: (v) => (/^\S+@\S+\.\S+$/.test(v) ? '' : 'Enter an email such as ada@example.com.'),
plan: (v) => (v ? '' : 'Choose a plan.'),
}
const plans = [
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
]
function typed(value) {
values.value = { ...values.value, email: value }
}
function chose(detail) {
values.value = { ...values.value, plan: detail.value }
}
function invalid(detail) {
errors.value = detail.errors
}
function sent() {
errors.value = {}
}
</script>
<template>
<Form :values="values" :rules="rules" @invalid="invalid($event)" @submit="sent()">
<TextField :error="errors.email" @input="typed($event)">
<span slot="label">Email</span>
</TextField>
<RadioGroup :options="plans" :error="errors.plan" @choose="chose($event)">
<span slot="label">Plan</span>
</RadioGroup>
<Button type="submit">Sign up</Button>
</Form>
</template>
API reference
Slots
| Slot | Content |
|---|---|
default | the fields and the buttons |
Events
| Event | When |
|---|---|
submit | fired with { values } when the form is submitted and every rule passes |
invalid | fired with { errors } when it is submitted and a rule fails: errors maps a field name to its message |
Anatomy
| Role | form |
| Parts | root |
| Since | 0.8.0 |
Design tokens it uses
Your design system needs these. tessera add checks that it has them, and can add the ones it lacks.
color.text.default space.4
Checksum
4dc86bb89d48253bd912cebd7cde9199f5982b6019bf12d62a5422db9f30cc46