Releases: edmundhung/conform
v0.6.2
Improvements
- The
useForm
hook can now infer the shape directly based on the return type of theonValidate
hook (#149)
// e.g. using zod
const schema = z.object({
title: z.string().min(1, 'Title is required'),
// ...
})
// Before (Manual typing is needed)
function Example() {
const [form, fieldset] = useForm<z.infer<typeof schema>>({
onValidate({ formData }) {
return parse(formData, { schema });
}
})
console.log(fieldset.title);
// ^ FieldConfig<string>
}
// After (Directly inferred from the schema)
function Example() {
const [form, fieldset] = useForm({
onValidate({ formData }) {
return parse(formData, { schema });
}
})
console.log(fieldset.title);
// ^ FieldConfig<string>
}
- Added support to zod schema intersection when deriving validation attributes with
getFieldsetConstraints
(#148)
const constraint = getFieldsetConstraint(
z
.object({ a: z.string() })
.and(z.object({ a: z.string().optional(), b: z.string() }))
);
// This generates { a: { required: false }, b: { required: true } }
- Added union support on both schema and constraint type (#149)
- The
Submission
type is slightly adjusted for better accessibility (#149) - Fixed a bug which triggers validation when user unfocused a button
New Contributors
- @ollie-bud made their first contribution in #139
Full Changelog: v0.6.1...v0.6.2
v0.6.1
Improvements
- You can now customize when Conform should validate and revalidate (#127)
function Example() {
const [form, { email, password }] = useForm({
// This is now deprecated in favor of the new configs
initialReport: 'onBlur',
// Define when Conform should start validation. Default `onSubmit`.
shouldValidate: 'onBlur',
// Define when Conform should revalidate again. Default `onInput`.
shouldRevalidate: 'onBlur',
});
}
- The useForm hook now accepts an optional ref object. If it is not provided, conform will fallback to its own ref object instead. (#122)
function Example() {
const ref = useRef<HTMLFormElement>(null);
const [form, { email, password }] = useForm({
ref,
});
// `form.ref / form.props.ref` will now be the same as `ref`
return (
<form {...form.props}>
{/* ... */}
</form>
);
}
- The field config now generates an additional
descriptionId
to support accessible input hint. (#126)
function Example() {
const [form, { email }] = useForm();
return (
<form {...form.props}>
<label htmlFor={email.id}>Email</label>
<input {...conform.input(email, { type: "email", description: true })} />
{/* If invalid, it will set the aria-describedby to "${errorId} ${descriptionId}" */}
<div id={email.descriptionId}>
Email hint
</div>
<div id={email.errorId}>
{email.error}
</div>
</form>
)
}
- The defaultValue and initialError is no longer cached to simplify form reset. (#130)
Full Changelog: v0.6.0...v0.6.1
v0.6.0
Breaking Changes
- All properties on
field.config
is now merged with thefield
itself (#113)
function Example() {
const [form, { message }] = useForm();
return (
<form>
- <input {...conform.input(message.config)} />
+ <input {...conform.input(message)} />
{message.error}
</form>
);
- The
submission.intent
is now merged withsubmission.type
to align with the intent button approach that are common in Remix. (#91)
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData);
// The `submission.intent` is `submit` if the user click on the submit button with no specific intent (default)
if (!submission.value || submission.intent !== 'submit') {
return json(submission);
}
// ...
}
- The
validate
andformatError
helpers are replaced by a newparse
helper, which can be used on both client and server side: (#92)
// The new `parse` helper can be treat as a replacement of the parse helper from `@conform-to/react`
import { parse } from '@conform-to/zod'; // or `@conform-to/yup`
const schema = z.object({ /* ... */ });
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, {
schema,
// If you need to run async validation on the server
async: true,
});
// `submission.value` is defined only if no error
if (!submission.value || submission.intent !== 'submit') {
return json(submission);
}
// ...
}
export default function Example() {
const [form, fieldset] = useForm({
onValidate({ formData }) {
return parse(formData, { schema });
},
// ...
});
// ...
}
- Redesigned the async validation setup with the
VALIDATION_SKIPPED
andVALIDATION_UNDEFINED
message (#100)
import { conform, useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { z } from 'zod';
// Instead of sharing a schema, we prepare a schema creator
function createSchema(
intent: string,
// Note: the constraints parameter is optional
constraints: {
isEmailUnique?: (email: string) => Promise<boolean>;
} = {},
) {
return z.object({
name: z
.string()
.min(1, 'Name is required'),
email: z
.string()
.min(1, 'Email is required')
.email('Email is invalid')
// We use `.superRefine` instead of `.refine` for better control
.superRefine((value, ctx) => {
if (intent !== 'validate/email' && intent !== 'submit') {
// Validate only when necessary
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: conform.VALIDATION_SKIPPED,
});
} else if (typeof constraints.isEmailUnique === 'undefined') {
// Validate only if the constraint is defined
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: conform.VALIDATION_UNDEFINED,
});
} else {
// Tell zod this is an async validation by returning the promise
return constraints.isEmailUnique(value).then((isUnique) => {
if (isUnique) {
return;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Email is already used',
});
});
}
}),
title: z
.string().min(1, 'Title is required')
.max(20, 'Title is too long'),
});
}
export let action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const submission = await parse(formData, {
schema: (intent) =>
// create the zod schema with the intent and constraint
createSchema(intent, {
async isEmailUnique(email) {
// ...
},
}),
async: true,
});
return json(submission);
};
export default function EmployeeForm() {
const lastSubmission = useActionData();
const [form, { name, email, title }] = useForm({
lastSubmission,
onValidate({ formData }) {
return parse(formData, {
// Create the schema without any constraint defined
schema: (intent) => createSchema(intent),
});
},
});
return (
<Form method="post" {...form.props}>
{/* ... */}
</Form>
);
}
- The validation
mode
option is removed. Conform will now decide the validation mode based on whetheronValidate
is defined or not. (#95)
export default function Example() {
const [form, fieldset] = useForm({
// Server validation will be enabled unless the next 3 lines are uncommented
// onValidate({ formData }) {
// return parse(formData, { schema });
// },
});
// ...
}
- The
state
option is now calledlastSubmission
on the useForm hook (#115) - The
useControlledInput
hook is removed, please use useInputEvent (#97) - The
getFormElements
andrequestSubmit
API are also removed (#110)
Improvements
- Added multiple errors support (#96)
import { parse } from '@conform-to/zod';
import { z } from 'zod';
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, {
schema: z.object({
// ...
password: z
.string()
.min(10, 'The password should have minimum 10 characters')
.refine(password => password.toLowerCase() === password, 'The password should have at least 1 uppercase character')
.refine(password => password.toUpperCase() === password, 'The password should have at least 1 lowercase character')
}),
// Default to false if not specified
acceptMultipleErrors({ name }) {
return name === 'password';
}
});
// ...
}
export default function Example() {
const lastSubmission = useActionData();
const [form, { password }] = useForm({
lastSubmission,
});
return (
<Form {...form.props}>
{ /* ... */ }
<div>
<label>Password</label>
<input {...conform.input(password, { type: 'password' })} />
<ul>
{password.errors?.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
{ /* ... */ }
</Form>
)
}
- Simplified access to form attributes within the
onSubmit
handler (#99)
export default function Login() {
const submit = useSubmit();
const [form] = useForm({
async onSubmit(event, { formData, method, action, encType }) {
event.preventDefault();
formData.set("captcha", await captcha());
// Both method, action, encType are properly typed
// to fullfill the types required by the submit function
// with awareness on submitter attributes
submit(formData, { method, action, encType });
},
});
// ...
}
- Introduced a new validateConstraint helper to fully utilize browser validation. Best suited for application built with react-router. (#89)
import { useForm, validateConstraint } from '@conform-to/react';
import { Form } from 'react-router-dom';
export default function SignupForm() {
const [form, { email, password, confirmPassword }] = useForm({
onValidate(context) {
// This enables validating each field based on the validity state and custom cosntraint if defined
return validateConstraint(
...context,
constraint: {
// Define custom constraint
match(value, { formData, attributeValue }) {
// Check if the value of the field match the value of another field
return value === formData.get(attributeValue);
},
});
}
});
return (
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<input
name="email"
type="email"
required
pattern="[^@]+@[^@]+\\.[^@]+"
/>
{email.error === 'required' ? (
<div>Email is required</div>
) : email.error === 'type' ? (
<div>Email is invalid</div>
) : null}
</div>
<div>
<label>Password</label>
<input
name="password"
type="password"
required
/>
{password.error === 'required' ? (
<div>Password is required</div>
) : null}
</div>
<div>
<label>Confirm Pas...
v0.5.1
What's Changed
- The
useControlledInput
API is now deprecated and replaced with the newuseInputEvent
hook. (#90)
Please check the new integration guide for details.
Full Changelog: v0.5.0...v0.5.1
v0.5.0
Hey! I am glad you are here. There are many exciting changes on v0.5. Here is what's changed and a brief migration guide. If you would like to learn more about the new features, please checkout the new guides on the website.
Breaking Changes
- The
useForm
hook now returns the fieldset together as a tuple (#78)
// Before
export default function LoginForm() {
const form = useForm();
const { email, password } = useFieldset(form.ref, form.config);
return (
<form {...form.props}>
{/* ... */}
</form>
);
}
// After the changes
export default function LoginForm() {
const [form, { email, password }] = useForm();
return (
<form {...form.props}>
{/* ... */}
</form>
);
}
Tips: As there is no change on the form
objects. You can also do this to simplify the migration and fix the rest gradually:
export default function LoginForm() {
// Just find and replace `form = useForm` with `[form] = useForm`
const [form] = useForm();
const { email, password } = useFieldset(form.ref, form.config);
return (
<form {...form.props}>
{/* ... */}
</form>
);
}
- The
useFieldList
hook now returns a list of field error and config only. The command is available as an additional API instead. (#70)
// Before
import { useForm, useFieldset, useFieldList, conform } from '@conform-to/react';
function Example(config) {
const form = useForm();
const { tasks } = useFieldset(form.ref, form.config);
const [taskList, command] = useFieldList(form.ref, tasks.config);
return (
<form {...form.props}>
{taskList.map((task, index) => (
<div key={task.key}>
<input {...conform.input(task.config)} />
<button {...command.remove({ index })}>Add</button>
</div>
))}
<button {...command.append()}>Add</button>
</form>
)
}
// After
import { useForm, useFieldList, list, conform } from '@conform-to/react';
function Example(config) {
const [form, { tasks }] = useForm();
const taskList = useFieldList(form.ref, tasks.config);
return (
<form {...form.props}>
{taskList.map((task, index) => (
<div key={task.key}>
<input {...conform.input(task.config)} />
<button {...list.remove(tasks.config.name, { index })}>
Delete
</button>
</div>
))}
{/* All `list` commands require the name now, i.e. 'tasks' */}
<button {...list.append(tasks.config.name)}>Add</button>
</form>
)
}
Tips: The list
command builder can be used anywhere as long as you know about the name of the list.
import { useForm, useFieldList, list, conform } from '@conform-to/react';
function Example(config) {
const [form, { tasks }] = useForm();
const taskList = useFieldList(form.ref, tasks.config);
return (
<form {...form.props} id="todos">
{/* Nothing changed from above*/}
</form>
)
}
// On the sidebar (outside of the form)
function Sidebar() {
return (
<button {...list.append('tasks')} form="todos">Add Task</button>
);
}
Improvements
-
Conform now inserts placeholder buttons for error that have no matching elements, e.g. form error. This will not break any existing form with placeholder buttons, e.g.
<button name="..." hidden />
and could be removed gradually. (#69) -
File Upload is now supported natively including multiple file input. More details can be found here (#72)
-
The
useForm
API now accepts an optional form id which will be used to derive aria-attributes. More details can be found here. (#77) -
The
useFieldList
API now captures and returns the error for each item. (#71)
import { useForm, useFieldList, list, conform } from '@conform-to/react';
function Example(config) {
const [form, { tasks }] = useForm();
const taskList = useFieldList(form.ref, tasks.config);
return (
<form {...form.props}>
{taskList.map((task, index) => (
<div key={task.key}>
<input {...conform.input(task.config)} />
{/* Error of each task */}
<div>{task.error}</div>
</div>
))}
</form>
)
}
- Introduced a new API to execute command imperatively: requestCommand. (#70)
import {
useForm,
useFieldList,
conform,
list,
requestCommand,
} from '@conform-to/react';
import DragAndDrop from 'awesome-dnd-example';
export default function Todos() {
const [form, { tasks }] = useForm();
const taskList = useFieldList(form.ref, tasks.config);
// Execute a command with a form element and a list command
const handleDrop = (from, to) =>
requestCommand(form.ref.current, list.reorder({ from, to }));
return (
<form {...form.props}>
<DragAndDrop onDrop={handleDrop}>
{taskList.map((task, index) => (WW
<div key={task.key}>
<input {...conform.input(task.config)} />
</div>
))}
</DragAndDrop>
<button>Save</button>
</form>
);
}
- Similar to the new list command builder, the internal validate command builder is also exported now, which could be used to trigger validation manually. More details can be found here. (#84)
Full Changelog: v0.4.1...v0.5.0
v0.5.0-pre.0
What's Changed
- chore(conform-guide): link to chakra-ui example in #66
- test(conform-yup,conform-zod): setup unit tests in #67
- feat(conform-dom,conform-react): automatic button creation for form-level error in #69
- feat(conform-react): capture list item error in #71
- feat(conform-dom,conform-react): multiple file support in #72
Full Changelog: v0.4.1...v0.5.0-pre.0
v0.4.1
Merry Christmas 🎄
Improvements
- Fixed a case with form error not populating caused by non-submit button (#63)
- The shadow input configured with
useControlledInput
should now be hidden from the accessibility API. (#60) - The return type of the conform helpers is now restricted to the relevant properties only. (#59)
- The
FieldConfig
type is now re-exported from@conform-to/react
to simplify integration with custom inputs. (#57)
Docs
- The material-ui example is updated covering more input type. (#53)
- Examples for headless-ui and chakra-ui is added. (#51, #52, #54)
Full Changelog: v0.4.0...v0.4.1
v0.4.0
What's Changed
Breaking Changes
Conform has undergone a massive redesign in its validation mechanism. This includes replacing some of the high level abstractions with a new set of APIs. Revisitng the updated docs are strongly recommended. Changes include:
- The
useForm
hook is updated with new config and return type:
// Before
import { useForm } from '@conform-to/react';
function ExampleForm() {
const formProps = useForm({
// ...
});
return <form {...formProps}>{/* ... */}</form>
}
// Now
import { useForm } from '@conform-to/react';
function ExampleForm() {
// If you are using remix:
const state = useActionData();
/**
* The hook now returns a `form` object with
* - `form.props` equivalent to the previous `formProps`
* - `form.ref` which is just a shortcut of `form.props.ref`
* - `form.config` which wraps `defaultValue` and `initialError`
* bases on the new `defaultValue` and `state` config
* - `form.error` which represent the form-level error
*/
const form = useForm({
/**
* New validation mode config, default to `client-only`
* Please check the new validation guide for details
*/
mode: 'client-only',
/**
* Default value of the form. Used to serve the `form.config`.
*/
defaultValue: undefined,
/**
* Last submission state. Used to serve the `form.config`
*/
state,
/**
* The `validate` config is renamed to `onValidate`
*/
onValidate({ form, formData }) {
// ...
},
// ... the rest of the config remains the same
})
const fieldset = useFieldset(form.ref, form.config);
return <form {...form.props}>{/* ... */}</form>
}
- The
resolve(schema).parse
API on both schema resolver is now replaced by parse with manual validation.
// Before
import { resolve } from '@conform-to/zod';
import { z } from 'zod';
const schema = resolve(
z.object({
// ...
}),
);
export let action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const submission = schema.parse(formData);
if (submission.state !== 'accepted') {
return submission.form;
}
return await process(submission.data);
};
// Now
import { formatError } from '@conform-to/zod';
import { parse } from '@conform-to/react';
import { z } from 'zod';
// Raw zod schema
const schema = z.object({
// ...
});
export let action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
/**
* The `submission` object is slightly different
* in the new version, with additional information
* like `submission.type` and `submission.intent`
*
* Learn more about it here: https://conform.guide/submission
*/
const submission = parse(formData);
try {
switch (submission.type) {
case 'valdiate':
case 'submit': {
// schema.parse() is a Zod API
const data = schema.parse(submissio.value);
// Skip if the submission is meant for validation only
if (submission.type === 'submit') {
return await process(data);
}
break;
}
}
} catch (error) {
// formatError is a new API provided by the schema resolver that
// transform the zod error to the conform error structure
submission.error.push(...formatError(error));
}
// Always returns the submission state until the submission is `done`
return submission;
};
- The
resolve(schema).validate
API is also replaced byvalidate()
:
// Before
import { resolve } from '@conform-to/zod';
import { z } from 'zod';
const schema = resolve(
z.object({
// ...
}),
);
export default function ExampleForm() {
const form = useForm({
validate: schema.validate,
});
// ...
}
// Now
import { validate } from '@conform-to/zod';
import { z } from 'zod';
// Raw zod schema
const schema = z.object({
// ...
});
export default function ExampleForm() {
const form = useForm({
// The `validate` config is renamed to `onValidate`
onValidate({ formData }) {
return validate(formData, schema);
},
});
// ...
}
/**
* The new `valdiate` API is just a wrapper on top of
* `parse` and `formatError`, so you can also do this:
*/
export default function ExampleForm() {
const form = useForm({
onValidate({ formData }) {
const submission = parse(formData);
try {
schema.parse(submission.value);
} catch (error) {
submission.error.push(...formatError(error));
}
return submission;
},
});
// ...
}
- The parsed value (i.e.
submission.value
) no longer removes empty string, which will affect how zod handlesrequired
error
/**
* Before v0.4, empty field value are removed from the form data before passing to the schema
* This allows empty string being treated as `undefiend` by zod to utilise `required_error`
* e.g. `z.string({ required_error: 'Required' })`
*
* However, this introduced an unexpected behaviour which stop the schema from running
* `.refine()` calls until all the defined fields are filled with at least 1 characters
*
* In short, please use `z.string().min(1, 'Required')` instead of `z.string({ required_error: 'Required' })` now
*/
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().min(1, 'Email is required').email('Email is invalid'),
title: z.string().min(1, 'Title is required').max(20, 'Title is too long'),
});
Improvements
- Conform is now able to autofocus first error field for both client validation and server validation
Docs
- Add comment about button values by @brandonpittman in #36
Special thanks to @brandonpittman for the kind words and support!
Full Changelog: v0.3.1...v0.4.0
v0.4.0-pre.3
What's Changed
- docs: documentation for v0.4 by @edmundhung in #45
Full Changelog: v0.4.0-pre.2...v0.4.0-pre.3
v0.4.0-pre.2
What's Changed
- feat!: simplify validation api by @edmundhung in #43
Full Changelog: v0.4.0-pre.1...v0.4.0-pre.2