Getting started
Below you will write a definition for a small sign-up form, check it, and render it with the Vue or React adapter. If you haven't yet, install the packages for your framework first.
Write a definition
A definition describes a form as JSON: its version, which is always "1" for now, and its fields. Each field has a name, which is its key in the form's value, and a type:
{
"version": "1",
"fields": [
{
"name": "email",
"type": "email",
"label": "Email",
"required": true
},
{
"name": "password",
"type": "password",
"label": "Password",
"required": true,
"rules": [
{
"rule": "minLength",
"value": 10,
"message": "At least 10 characters"
}
]
},
{
"name": "newsletter",
"type": "checkbox",
"label": "Send me the newsletter"
}
]
}The email type checks that the value is an email address. required makes a field required, and rules adds checks of your own, each with an optional message. The define your form guide goes through every key.
Check it
A definition that comes from outside your code, like one from a database, can have mistakes: a misspelled type, a rule that doesn't fit the field, two fields with the same name. validate() never throws. It returns every problem it finds, so you can check a definition before you use it:
const builder = createSchemaBuilder()
function load(json: unknown): JsonForm {
const result = builder.validate(json)
if (!result.success) {
throw new Error(result.issues.map((issue) => issue.message).join('\n'))
}
return json as JsonForm
}The adapters build the form with build(), which runs the same checks and throws a DefinitionError with every problem.
Add field components
The adapters are headless: they connect every field to Formisch, but render nothing of their own. You give them a component for each field type, so the form looks like the rest of your app. A text input looks like this:
<script setup lang="ts">
import type { ValueFieldProps } from '@whatsaaaa/form-builder-vue'
defineProps<ValueFieldProps>()
// The native input for each type. The rest are text.
const inputTypes: Record<string, string> = {
email: 'email',
password: 'password',
number: 'number',
url: 'url',
tel: 'tel',
date: 'date',
time: 'time',
datetime: 'datetime-local',
}
</script>
<template>
<label>
{{ field.label }}
<input
v-bind="inputProps"
:type="inputTypes[field.type] ?? 'text'"
:value="input ?? ''"
:placeholder="field.placeholder"
:required="required"
@input="setInput(($event.target as HTMLInputElement).value)"
/>
<small v-if="errors">{{ errors[0] }}</small>
</label>
</template>import type { ValueFieldProps } from '@whatsaaaa/form-builder-react'
// The native input for each type. The rest are text.
const inputTypes: Record<string, string> = {
email: 'email',
password: 'password',
number: 'number',
url: 'url',
tel: 'tel',
date: 'date',
time: 'time',
datetime: 'datetime-local',
}
export function TextInput({
field,
input,
errors,
required,
inputProps,
setInput,
}: ValueFieldProps) {
return (
<label>
{field.label}
<input
{...inputProps}
type={inputTypes[field.type] ?? 'text'}
value={String(input ?? '')}
placeholder={field.placeholder}
required={required}
onChange={(event) => setInput(event.target.value)}
/>
{errors && <small>{errors[0]}</small>}
</label>
)
}Spread inputProps on the input element, so Formisch knows its name and can focus it. Set the value with setInput. Then map each field type the definition uses to a component:
import type { FieldComponents } from '@whatsaaaa/form-builder-vue'
import CheckboxInput from './CheckboxInput.vue'
import TextInput from './TextInput.vue'
export const components: FieldComponents = {
text: TextInput,
email: TextInput,
password: TextInput,
checkbox: CheckboxInput,
}import type { FieldComponents } from '@whatsaaaa/form-builder-react'
import { CheckboxInput } from './CheckboxInput'
import { TextInput } from './TextInput'
export const components: FieldComponents = {
text: TextInput,
email: TextInput,
password: TextInput,
checkbox: CheckboxInput,
}Render the form
useJsonForm builds the schema and the initial input, and creates the Formisch form. <JsonFields> renders its fields inside Formisch's <Form>. The form element and the submit button are yours:
<script setup lang="ts">
import { Form } from '@formisch/vue'
import type { JsonForm } from '@whatsaaaa/form-builder-schema'
import { JsonFields, useJsonForm } from '@whatsaaaa/form-builder-vue'
import { components } from './fields'
const { definition } = defineProps<{ definition: JsonForm }>()
const jsonForm = useJsonForm(definition, { components })
function save(output: unknown) {
// { email: 'jane@example.com', password: 'correct horse', newsletter: true }
console.log(output)
}
</script>
<template>
<Form :of="jsonForm.form" @submit="save">
<JsonFields :of="jsonForm" />
<button type="submit">Sign up</button>
</Form>
</template>import { Form } from '@formisch/react'
import type { JsonForm } from '@whatsaaaa/form-builder-schema'
import { JsonFields, useJsonForm } from '@whatsaaaa/form-builder-react'
import { components } from './fields'
export function SignUpForm({ definition }: { definition: JsonForm }) {
const jsonForm = useJsonForm(definition, { components })
function save(output: unknown) {
// { email: 'jane@example.com', password: 'correct horse', newsletter: true }
console.log(output)
}
return (
<Form of={jsonForm.form} onSubmit={save}>
<JsonFields of={jsonForm} />
<button type="submit">Sign up</button>
</Form>
)
}When the user submits, Formisch validates the whole form against the schema. It shows the errors on the fields and focuses the first one, or calls your submit handler with the output.
Handle the output
The output is what the schema gives for the input: text as typed, numbers as numbers, and optional fields that are empty left out. Rules that normalize, like trim, have already run. Its type is unknown, because the definition is data that TypeScript can't see. When you know the shape, give useJsonForm your types: see TypeScript.
Next steps
You now have a working form. To learn what a definition can describe, continue with define your form. For the components of every field type, see field components.