Breaking Changes
- Improved ESM compatibility with Node and Typescript. If you were running on ESM with hydration issue like this report, please upgrade. (#159, #160)
- The
initialReport
config is now removed. Please use theshouldValidate
andshouldRevalidate
config instead (#176) - The
shouldRevalidate
config will now default to theshouldValidate
config instead ofonInput
(#184) - The
useInputEvent
hook requires a ref object now (#173)
// Before - always returns a tuple with both ref and control object
const [ref, control] = useInputEvent();
// After - You need to provide a ref object now
const ref = useRef<HTMLInputElement>(null);
const control = useInputEvent({
ref,
});
// Or you can provide a function as ref
const control = useInputEvent({
ref: () => document.getElementById('do whatever you want'),
});
- The
conform
helpers no longer derive aria attributes by default. You can enable it with theariaAttributes
option (#183)
// Before
function Example() {
const [form, { message }] = useForm();
return (
<form>
<input {...conform.input(message, { type: 'text' })} />
</form>
)
}
// After
function Example() {
const [form, { message }] = useForm();
return (
<form>
<input
{...conform.input(message, {
type: 'text',
ariaAttributes: true, // default to `false`
})}
/>
</form>
)
}
Improvements
- Conform will now track when the
lastSubmission
is cleared and triggered a form reset automatically. (Note: The example below is updated with the new approach introduced on v0.7.2 instead)
export let action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const submission = parse(formData, { schema });
if (submission.intent !== 'submit' || !submission.value) {
return json(submission);
}
return json({
...submission,
// Notify the client to reset the form using `null`
payload: null,
});
};
export default function Component() {
const lastSubmission = useActionData<typeof action>();
const [form, { message }] = useForm({
// The last submission should be updated regardless the submission is successful or not
// If the submission payload is empty:
// 1. the form will be reset automatically
// 2. the default value of the form will also be reset if the document is reloaded (e.g. nojs)
lastSubmission,
})
// ...
}
Original approach on v0.7.0
const actionData = useActionData();
const [form, fields] = useForm({
// Pass the submission only if the action was failed
// Or, skip sending the submission back on success
lastSubmission: !actionData?.success ? actionData?.submission : null,
});
- New
refine
helper to reduce the boilerplate when setting up async validation with zod (#167)
// Before
function createSchema(
intent: string,
constraints: {
isEmailUnique?: (email) => Promise<boolean>;
} = {},
) {
return z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Email is invalid')
.superRefine((email, ctx) => {
if (intent !== 'submit' && intent !== 'validate/email') {
// 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',
});
});
}
}),
// ...
});
}
// After
import { refine } from '@conform-to/zod';
function createSchema(
intent: string,
constraints: {
isEmailUnique?: (email) => Promise<boolean>;
} = {},
) {
return z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Email is invalid')
.superRefine((email, ctx) =>
refine(ctx, {
validate: () => constraints.isEmailUnique?.(email),
when: intent === 'submit' || intent === 'validate/email',
message: 'Email is already used',
}),
),
// ...
});
}
- Added basic zod union / discriminatedUnion support when inferring constraint (#165)
const schema = z
.discriminatedUnion('type', [
z.object({ type: z.literal('a'), foo: z.string(), baz: z.string() }),
z.object({ type: z.literal('b'), bar: z.string(), baz: z.string() }),
])
.and(
z.object({
qux: z.string(),
}),
),
// Both `foo` and `bar` is considered optional now
// But `baz` and `qux` remains required
expect(getFieldsetConstraint(schema)).toEqual({
type: { required: true },
foo: { required: false },
bar: { required: false },
baz: { required: true },
quz: { required: true },
});
- Added contextual error map support with the zod parse helper (#177)
- Conform will now ignore duplicated intent instead of throwing an error to get around the FormData issue on Safari 15. (#164)
- Fixed an issue that blocks submission on a form with async validation setup even when all errors are resolved (#168)
- Fixed an error when parsing list intent with slash in the payload (#185)
New Contributors
- @miguelsndc made their first contribution in #158
- @jessethomson made their first contribution in #178
Full Changelog: v0.6.3...v0.7.0