Forms
Forms allow users to fill and submit data to your application, sometimes they are given feedback along the way to help them achieve that.
Assuming you’ve followed the other guides and have created a few input components, you can now use them to build a form. Formwerk builds on the native form
element but it is not required.
Features
Section titled “Features”- Value tracking and submission handling.
- Nested fields and arrays support.
- Controlled and uncontrolled fields.
- Multi-layered validation with both HTML attributes or Standard Schemas.
- Aggregated state for validation, dirty, touched, and more.
- Type safety for form data and submitted data.
- Scrolling to the first invalid field after submission.
- Submitted data can be consumed as a plain object, JSON, or
FormData
object.
useForm
Section titled “useForm”You will be using the useForm
composable to create a form context in the current component. This effectively marks the component as a form, meaning you can only use useForm
once per component.
This is the most basic form you can create with Formwerk:
import { useForm } from '@formwerk/core';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => { console.log(data);});
It doesn’t look like much, but already a lot is being done for you behind the scenes. The useForm
composable creates a reactive form context that does the following among other things:
- Tracks and collects the values of all input fields within the form.
- Tracks the validity of each field and the overall form validity.
- Provides a
handleSubmit
function that you can use to submit the form.
Here is an example with some input fields we already created from the previous guides:
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import Checkbox from './Checkbox.vue';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<Checkbox label="Remember me" name="rememberMe" />
<button type="submit">Submit</button> </form></template>
Controlled Fields
Section titled “Controlled Fields”You may have noticed that we passed the name
prop to the input fields in the previous example. This is because Formwerk uses the name
prop to identify the fields in the form and uses it to build the form data object that will eventually be submitted.
Passing the name
prop to the field marks it as “controlled”, as in it is being tracked by the form and contributes its state and value to the form data object.
Now if you want to do the opposite, which is to not have the field be tracked by the form, then you can simply skip passing the name
prop. This matches the behavior of FormData
objects and native form submission behavior.
Here is an example where a non-controlled field can be useful. In this example, we toggle the visibility of the billingAddress
field based on the value of the sameAsShipping
field, but we don’t want to submit the latter.
<script setup lang="ts">import { ref } from 'vue';import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import Checkbox from './Checkbox.vue';
const { handleSubmit } = useForm();const isSameAsBilling = ref(false);
const onSubmit = handleSubmit((data) => { const json = data.toObject(); if (isSameAsBilling.value) { json.billingAddress = json.shippingAddress; }
alert(JSON.stringify(json, null, 2));});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="shippingAddress" label="Shipping Address" required />
<TextField v-if="!isSameAsBilling" name="billingAddress" label="Billing Address" required /> <Checkbox label="Same as shipping" v-model="isSameAsBilling" />
<button type="submit">Submit</button> </form></template>
Nested Fields
Section titled “Nested Fields”Formwerk supports nested fields by using the .
character in the name
prop. This allows you to create nested objects in the form data object to structure your data however you need. Having numeric path names will result in arrays being created instead of objects.
Here is an example with both nested fields and arrays:
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import Checkbox from './Checkbox.vue';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="socials.github" label="GitHub " type="url" required /> <TextField name="socials.twitter" label="Twitter " type="url" required /> <TextField name="socials.discord" label="Discord" type="url" required />
<TextField name="customLinks.0" label="Custom link 1" type="url" /> <TextField name="customLinks.1" label="Custom Link 2" type="url" />
<button type="submit">Submit</button> </form></template>
Submitting Forms
Section titled “Submitting Forms”You noticed that the values of the form are collected and passed for you in the data
object in the previous examples.
handleSubmit
Section titled “handleSubmit”The previous examples used the handleSubmit
function to submit the form. This function doesn’t require you to use a form
element nor does it require you to use it with a submit
event. You can use it with any event or even call it directly.
The handleSubmit
function takes a callback function that will be called with the data
object when the form is submitted. The callback is run only if the form is valid; otherwise, it does nothing.
Here is an example where we just call the submission handler directly:
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});
function onClick() { onSubmit();}</script>
<template> <TextField name="field" label="Your field" value="Press 👇" />
<button @click="onClick">Press me</button></template>
toObject
Section titled “toObject”The most common way to get the form data is to call the toObject
method on the data
object. This method returns a plain JavaScript object with the form data as you’ve seen in the previous examples.
import { useForm } from '@formwerk/core';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => { data.toObject(); // { email: '...', password: '...', rememberMe: true }});
toJSON
Section titled “toJSON”The toJSON
method returns a JSON-serializable object with the form data. While this often matches the structure from toObject
, JSON has some limitations compared to JavaScript objects. For example, JSON cannot represent:
undefined
values- Date objects (they get converted to strings)
- File objects
- Functions
- BigInt values
- Symbol values
The toJSON
method handles converting these values into JSON-safe equivalents automatically.
import { useForm } from '@formwerk/core';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit(async (data) => { const response = await fetch('https://example.org/post', { body: JSON.stringify(data.toJSON()), });});
If you plan to stringify the form data for use with something like fetch
or axios
, then you can omit the toJSON
and call JSON.stringify
directly on the data
object, which will call toJSON
under the hood.
const onSubmit = handleSubmit(async (data) => { const response = await fetch('https://example.org/post', { body: JSON.stringify(data.toJSON()), body: JSON.stringify(data), });});
The toJSON
method is fully typed to match the JSON-serialized structure of your form data. The types will automatically handle non-serializable values like undefined
, Date
objects, and File
objects in the type system, ensuring the types match what actually gets serialized to JSON.
toFormData
Section titled “toFormData”If you need to submit the form data as a FormData
object, you can call the toFormData
method instead.
This method returns a FormData
object that you can use to submit the form data to traditional form endpoints or APIs. It becomes especially useful when submitting files since they cannot be transported in JSON.
import { useForm } from '@formwerk/core';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => { data.toFormData(); // FormData});
formProps
Section titled “formProps”If you prefer to not handle submissions with JavaScript and instead want to rely on native form submissions, which is common with non-JS server-rendered applications like Rails (Ruby) or Laravel (PHP) applications, you can use the formProps
object that is returned by the useForm
composable to bind the form props to the form
element. It will enhance the native submission cycle with the same features as with the handleSubmit
function.
When you submit a form bound to the formProps
object:
- The form submit event will be prevented.
- The form data will be collected and validated.
- If invalid, the form will not be submitted, and the flow ends.
- If valid, the form will be submitted using the native form submission cycle.
Here is an example of how to use formProps
. The example will submit the data to another page that will list the submitted values. Typically, your backend endpoint would be handling the form submission.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import Checkbox from './Checkbox.vue';
const { formProps } = useForm();</script>
<template> <form v-bind="formProps" target="_blank" action="/form-d"> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required /> <Checkbox label="Remember me" name="rememberMe" />
<button type="submit">Submit</button> </form></template>
Submit State
Section titled “Submit State”Forms and Fields expose several properties related to the submission state. They can be useful to build certain UI behaviors like showing submit progress spinners or disabling the submit button while the form is being submitted with an async handler.
isSubmitting
Section titled “isSubmitting”You can check the submission status with the isSubmitting
property. This property is true
when the form is being submitted and false
otherwise. This is useful when you want to show a loading spinner or disable the submit button while the form is being submitted with an async handler.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit, isSubmitting } = useForm();
const onSubmit = handleSubmit(async (data) => { await new Promise((resolve) => setTimeout(resolve, 2000));
alert(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<button :disabled="isSubmitting" type="submit">Submit</button> </form></template>
wasSubmitted
Section titled “wasSubmitted”The wasSubmitted
property is true
if the form was submitted and the handler was called without any errors thrown. This is useful when you want to show a success message or perform some custom logic after the form has been submitted.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit, wasSubmitted } = useForm();
const onSubmit = handleSubmit(async (data) => { console.log(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <form v-if="!wasSubmitted" @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<button type="submit">Submit</button> </form>
<p v-else>Form was submitted</p></template>
This state is reset when the form is reset.
submitAttemptsCount
Section titled “submitAttemptsCount”The submitAttemptsCount
property returns the number of times the form has been submitted regardless of whether it was valid or not.
This can be useful when you want to disable certain UI elements, or show some feedback to the user. You might even want to gather analytics! maybe your form is too hard?
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit, submitAttemptsCount } = useForm();
const onSubmit = handleSubmit(async (data) => { console.log(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <p>Submit attempts: {{ submitAttemptsCount }}</p>
<form @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<button type="submit">Submit</button> </form></template>
This state is reset when the form is reset.
isSubmitAttempted
Section titled “isSubmitAttempted”The isSubmitAttempted
property is true
if the form was submitted, but unlike wasSubmitted
, it turns true
even if the form is invalid.
import { useForm } from '@formwerk/core';
const { handleSubmit, isSubmitAttempted } = useForm();
const onSubmit = handleSubmit((data) => { console.log(JSON.stringify(data.toObject(), null, 2));});
Scrolling to invalid fields
Section titled “Scrolling to invalid fields”By default, Formwerk will scroll to the first invalid field when the form is submitted.
The scrolling is performed with the Element.scrollIntoView
method with smooth behavior by default. You can override that by passing ScrollViewOptions
to the scrollToInvalidFieldOnSubmit
option.
import { useForm } from '@formwerk/core';
useForm({ scrollToInvalidFieldOnSubmit: { behavior: 'instant', // default is 'smooth' block: 'center', // default is 'center' inline: 'start', // default is 'start' },});
You can see it in action in the following example. Scroll all the way down to see the form submit button and then click it to see the invalid field being scrolled into view.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit, isSubmitting } = useForm();
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<button :disabled="isSubmitting" type="submit">Submit</button> </form></template>
<style>form { display: flex; flex-direction: column; /* This is to make the view scrollable */ gap: 500px;}</style>
This behavior can be disabled by setting the scrollToInvalidFieldOnSubmit
option to false
when creating the form.
import { useForm } from '@formwerk/core';
useForm({ scrollToInvalidFieldOnSubmit: false,});
Touched Fields
Section titled “Touched Fields”Forms track the touched state of each field in the form. A field is considered touched when the user interacts with it, which means if they have focused and blurred the field at least once. In addition to blurring, whenever the form is submitted, all fields are marked as touched.
The form also tracks the overall touched state of the form, which is true
if any field in the form has been touched.
Each field composable returns an isTouched
property. useForm
exposes its own isTouched
method that you can use to check if the form has been interacted with or if a field has been touched.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit, isTouched } = useForm();
const onSubmit = handleSubmit((data) => { console.log('All fields should be touched now');});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" /> <TextField name="password" label="Password" type="password" />
<pre>Email Touched: {{ isTouched('email') }}</pre> <pre>Password Touched: {{ isTouched('password') }}</pre> <pre>Form Touched: {{ isTouched() }}</pre>
<button type="submit">Submit</button> </form></template>
You can set the touched state of fields manually with the setTouched
function.
import { useForm } from '@formwerk/core';
const { setTouched } = useForm();
function onFieldBlur() { setTouched('email', true); // Or set all fields to touched setTouched(true);}
Dirty Fields
Section titled “Dirty Fields”Forms also track the dirty state of each field in the form. A field is considered dirty when its value has changed from the initial value. The form also tracks the overall dirty state of the form, which is true
if any field in the form has been modified.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { isDirty } = useForm();</script>
<template> <TextField name="email" label="Email" type="email" /> <TextField name="password" label="Password" type="password" />
<pre>Email Dirty: {{ isDirty('email') }}</pre> <pre>Password Dirty: {{ isDirty('password') }}</pre> <pre>Form Dirty: {{ isDirty() }}</pre></template>
The dirty state is computed. There is no way to set it manually, but you can reset the form to its initial values or a new set of values to influence the dirty state.
Validation
Section titled “Validation”As you’ve seen from field guides, many fields can be validated with either HTML constraints via attributes like required
, min
, max
, etc., or with Standard Schema objects.
HTML Constraints
Section titled “HTML Constraints”HTML constraints are always field-level. They are useful for dynamic fields, but at the same time, they are more accessible to users, which is why it is recommended to use them whenever possible for basic validations.
For advanced cases, you can use Standard Schemas, which can be both field-level or form-level.
If you want to completely disable HTML constraints for the form, you can pass the disableHtmlValidation
option to useForm
:
import { useForm } from '@formwerk/core';
useForm({ disableHtmlValidation: true,});
Form-level Validation with Standard Schemas
Section titled “Form-level Validation with Standard Schemas”But you can also provide a form-level Standard Schema to useForm
to validate the entire form as a whole. Form-level schemas are useful for forms where the fields are known beforehand.
Here is an example of a form with a Standard Schema:
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import { z } from 'zod';
const { handleSubmit } = useForm({ schema: z.object({ email: z.string().email(), password: z.string().min(8), }),});
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="email" label="Email" /> <TextField name="password" label="Password" type="password" />
<button type="submit">Submit</button> </form></template>
For more information on Standard Schemas and which libraries are supported, visit the project’s GitHub page.
Mixing validation sources
Section titled “Mixing validation sources”Let’s say you have a mix of validations in place. You have a field with some HTML constraints and a schema that validates that field at the same time. Let’s throw in a form-level schema that validates the form, including that field.
That field now has three sources of validation. How does that work?
Formwerk prioritizes the validation sources in the following order:
- HTML Constraints are checked first. Only if they are valid, continue to the next step.
- Field-level Standard Schema is checked next. Only if it is valid, continue to the next step.
- Form-level Standard Schema is checked last.
This keeps the validation process consistent and predictable. At the same time, it is also efficient, as you won’t have to re-validate the whole form if a field-level validation fails for that field. You can think of it as a merged validation approach, but it is more of a cascading validation behavior where it cascades upwards to the form level.
The only thing you need to be careful of is to not have conflicting validations between the different sources, as this can cause the field to never be valid.
Here is an example for a field with all validation sources:
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import { z } from 'zod';
const { handleSubmit } = useForm({ schema: z.object({ field: z.string().max(8), }),});
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="field" label="Field" min-length="3" required />
<button type="submit">Submit</button> </form></template>
You can even have a fourth source of validation with Form Groups.
Displaying Errors
Section titled “Displaying Errors”If you have followed the field guides, you know that fields are responsible for displaying their own errors. But what if you want to display the form errors in a single place, or maybe you just need access to errors to perform some custom logic?
There are three ways to access errors with useForm
:
getError
to get the error of a specific field.getErrors
to get all errors in the form grouped by field.displayError
to display the error of a specific field if it has been touched.
The getError
function returns the error of a specific field. If the field has no error, it returns undefined
.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import { z } from 'zod';
const { handleSubmit, getError } = useForm({ schema: z.object({ url: z.string().url().max(8), email: z.string().email(), }),});</script>
<template> <TextField name="url" label="URL" required /> <TextField name="email" label="Email" required />
<ul> <li>URL Error: {{ getError('url') }}</li> <li>Email Error: {{ getError('email') }}</li> </ul></template>
The getErrors
function returns all errors in the form as an array of error groups. Each group contains a field error message.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import { z } from 'zod';
const { handleSubmit, getErrors } = useForm({ schema: z.object({ url: z.string().url().max(8), email: z.string().email(), }),});</script>
<template> <TextField name="url" label="URL" required /> <TextField name="email" label="Email" required />
<ul> <li v-for="error in getErrors()">{{ error.path }}: {{ error.messages }}</li> </ul></template>
The displayError
function is similar to getError
, but it only displays the error if the field has been touched. This is useful when you want to show errors only after the user has interacted with the field.
You can alternatively use CSS with the :user-invalid
pseudo-class to show errors only when the field is invalid and has been interacted with. More info on that in the Styling guide.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import { z } from 'zod';
const { handleSubmit, displayError } = useForm({ schema: z.object({ url: z.string().url().max(8), email: z.string().email(), }),});</script>
<template> <TextField name="url" label="URL" required /> <TextField name="email" label="Email" required />
<ul> <li>URL Error: {{ displayError('url') }}</li> <li>Email Error: {{ displayError('email') }}</li> </ul></template>
Submit Errors
Section titled “Submit Errors”Unlike validation errors which are mostly “live” and react to the values changing regardless of when you display them, submit errors are only populated when the form is submitted. This is useful if you want to only show errors after submits rather than live.
Each field exposes submitErrorMessage
and submitErrors
, using these will only display errors after the form is submitted.
import { useTextField } from '@formwerk/core';
const { submitErrorMessage, submitErrors } = useTextField({ // ...});
Forms also expose getSubmitError
and getSubmitErrors
to get the submit error of a specific field or all fields respectively, if you need access to them on the form level.
Resetting Forms
Section titled “Resetting Forms”with reset()
Section titled “with reset()”Form state can be reset with the reset
function. Calling this function will reset the current values back to the initial values, revert the touched state for all fields back to false
, and clear any custom errors.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';import Checkbox from './Checkbox.vue';
const { handleSubmit, reset } = useForm();
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});
function onResetClick() { reset();}</script>
<template> <form @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<Checkbox label="Remember me" name="rememberMe" />
<button type="submit">Submit</button> <button type="button" @click="onResetClick">Reset</button> </form></template>
Notice that even though we called reset, the errors are still displayed. By default, reset re-validates the form afterward. This is to ensure that the validation state of the fields matches their actual validity.
You should consider using isTouched
or displayError
to show errors only when the field has been interacted with, which would eliminate this caveat. Alternatively, you can disable this behavior by passing revalidate: false
to the reset
function.
import { useForm } from '@formwerk/core';
const { reset } = useForm();
function onReset() { reset({ revalidate: false });}
You can also reset the form to a specific state by passing a ResetState
object to the reset
function. This object can contain the following properties:
- values: The new form values.
- touched: The new touched state for each field.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit, reset } = useForm();
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});
function onResetClick() { reset({ value: { email: 'hello@formwerk.dev', password: 'p@$$w0rd', }, touched: { email: true, password: false, }, });}</script>
<template> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<button @click="onResetClick">Reset</button></template>
Lastly, the value setting behavior by default uses a replace
strategy. This means that the values are replaced with the new values, and any unspecified values will be considered undefined
.
If you want to merge the new values with the existing initial values, you can pass behavior: 'merge'
to the reset
function as the second argument.
import { useForm } from '@formwerk/core';
const { reset } = useForm();
function onReset() { reset( { // values and stuff... }, { behavior: 'merge' }, );}
event handler with handleReset()
Section titled “event handler with handleReset()”You can also use handleReset()
to create an event handler that responds to the native reset
event.
<script setup lang="ts">import { useForm } from '@formwerk/core';import TextField from './TextField.vue';
const { handleSubmit, handleReset } = useForm();
const onSubmit = handleSubmit((data) => { alert(JSON.stringify(data.toObject(), null, 2));});
const onReset = handleReset(() => { alert('after reset');});</script>
<template> <form @reset="onReset" @submit="onSubmit" novalidate> <TextField name="email" label="Email" type="email" required /> <TextField name="password" label="Password" type="password" required />
<button type="submit">Submit</button> <button type="reset">Reset</button> </form></template>
Form Types
Section titled “Form Types”Forms support typing your form values with TypeScript. This is done through a couple of generic type parameters.
The useForm
composable signature is roughly typed as:
function useForm< TSchema extends StandardSchema<FormObject>, TInput extends FormObject = FormObject, TOutput extends FormObject = TInput,>();
Let’s break down the generic type parameters:
TSchema
: The type of the form schema if a Standard Schema is used.TInput
: The type of the form input values. These represent the current values of the form fields without any validation or transformations applied. We also refer to those as “input” types.TOutput
: The type of the form output values. These represent the values that would be submitted. That means validation and transformations have already been applied.
The distinction between input and output types is important because it helps you avoid re-checking values that have already been validated in runtime to satisfy TypeScript.
Inferring Types with initialValues
Section titled “Inferring Types with initialValues”You can type a form by either providing initialValues
to infer the input type.
const { values, handleSubmit } = useForm({ initialValues: { email: '' },});
values; // { email?: string | undefined }
However, this does not provide you with output types, meaning when submitting the form, the email
field would still be typed as string | undefined
.
In order to get output types, we export a utility type called FormSchema
that you can use to type both the input and output values. By default, the input type is assumed to be a partial of the output type.
import { type FormSchema, useForm } from '@formwerk/core';
// Input type is assumed to be a partial of the output typetype LoginForm = FormSchema<{ email: string }>;
const { handleSubmit, values } = useForm<LoginForm>();
values; // { email: string | undefined }
const onSubmit = handleSubmit((data) => { console.log(data.toObject()); // { email: string }});
If you want to explicitly define both the input and output types, you can do so by passing a second generic argument to the FormSchema
type. The first being the input type and the second being the output type.
import { type FormSchema, useForm } from '@formwerk/core';
type LoginForm = FormSchema< { email: string }, { email: string; token: string }>;
const { handleSubmit, values } = useForm<LoginForm>();
values; // { email: string | undefined; }
const onSubmit = handleSubmit((data) => { console.log(data.toObject()); // { email: string; token: string }});
We only recommend using this approach for simple forms with a few fields, or if the types are automatically generated from an API schema like GraphQL or OpenAPI specs.
Inferring Types with Standard Schema
Section titled “Inferring Types with Standard Schema”By providing a Standard Schema to the schema
prop, the form will infer both the input and output types automatically.
import { z } from 'zod';const { values, handleSubmit } = useForm({ schema: z.object({ email: z.string().email() }),});
values; // { email: string | undefined }
const onSubmit = handleSubmit((data) => { data.toObject(); // { email: string }});
For getting the most out of type safety, it is recommended to use Standard Schemas over manually providing types via the initialValues
prop.
Getting access to the form context
Section titled “Getting access to the form context”You may need to access the form context from within a component that is a child of the form. Common examples are button components that may need to be aware of the submitting state or the dirty state.
To do this, you can use the useFormContext
composable.
import { useFormContext } from '@formwerk/core';
const { isSubmitting } = useFormContext();
These are the properties that can be passed to the useForm
composable.
Name | Type | Description |
---|---|---|
disabled | Whether the form is disabled. | |
disableHtmlValidation | Whether HTML5 validation should be disabled for this form. | |
id | The form's unique identifier. | |
initialDirty | The initial dirty state for form fields. | |
initialTouched | The initial touched state for form fields. | |
initialValues | ||
schema | The validation schema for the form. | |
scrollToInvalidFieldOnSubmit | Whether the form should scroll to the first invalid field on invalid submission. |
Returns
Section titled “Returns”These are the properties in the object returned by the useForm
composable.
Name | Type | Description |
---|---|---|
context | The form context object, for internal use. | |
displayError | Displays the errors for a form field. | |
formProps | The form props. | |
getError | Get the error for a form field. | |
getErrors | Get the errors for a form field, or the | |
getIssues | Get the issues for a form field. | |
getSubmitError | Get the submit error for a form field. | |
getSubmitErrors | Get the submit errors for a form field. | |
getValue | TPath extends Path<TInput>>(path: TPath) => PathValue<TInput, TPath | Gets the value of a field or a path. |
handleReset | (afterReset?: () => any) => (e?: Event) => Promise<void> | Handle form reset. |
handleSubmit | (onSubmit: (values: ConsumableData<TInput>) => any) => (e?: Event) => Promise<void> | Handle form submission. |
isDirty | Checks if the form is dirty, which is true if any field's value has changed from the initial values. Accepts an optional path to check if a specific form path is dirty. | |
isDisabled | Whether the form is disabled. | |
isSubmitAttempted | Whether the form was submitted, wether the validity or the submission was successful or not. | |
isSubmitting | Whether the form is submitting. | |
isTouched | Checks if the form is touched, which is true if any field has been touched. Accepts an optional path to check if a specific form path is touched. | |
isValid | Checks if the form is valid, or if a form path is valid. Validity is defined as the absence of errors. | |
reset | { (): Promise<void>; (state: Partial<ResetState<TInput>>, opts?: SetValueOptions): Promise<void>; <TPath>(path: TPath): Promise<void>; <TPath extends Path<...>>(path: TPath, state: Partial<...>, opts?: SetValueOptions): Promise<...>; } | Reset the form to its initial values |
setErrors | { <TPath>(path: TPath, message: Arrayable$1<string>): void; <TPath extends Path<TInput>>(issues: {}): void; } | Sets the errors for a form path. |
setTouched | { (value: boolean): void; <TPath>(path: TPath, value: boolean): void; } | Sets the touched state of the form. Alternatively, pass a path to set the touched state of a specific form path. |
setValue | <TPath>(path: TPath, value: PathValue<TInput, TPath>) => void | Set a form field value by name. |
setValues | (values: PartialDeep<TInput>, opts?: SetValueOptions) => void | Set the value for a form field. |
submitAttemptsCount | The number of times the form has been submitted, regardless of the form's validity. | |
validate | () => Promise<FormValidationResult<TOutput>> | Validate the form. |
values | The current values of the form. | |
wasSubmitted | Whether the form was submitted, which is true if the form was submitted and the submission was successful. |