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 Password</label>
<input
name="confirmPassword"
type="password"
required
data-constraint-match="password"
/>
{confirmPassword.error === 'required' ? (
<div>Confirm Password is required</div>
) : confirmPassword.error === 'match' ? (
<div>Password does not match</div>
) : null}
</div>
<button>Signup</button>
</Form>
);
}
- Added support of aria-attributes on
form.props
(#114) - Fixed an issue with error not being caught on list validation, e.g. min / max (#75)
- Conform will now focus on first invalid non button fields even when pressing enter (#109)
Docs
- Fixed header spelling by @brandonpittman in #94
Full Changelog: v0.5.1...v0.6.0