A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
MIT License
Bot releases are hidden (Show)
Published by edmundhung about 1 year ago
submission.payload
to Record<string, unknown>
instead of Record<string, any>
to unblock usages on Remix v2-pre by @kentcdodds in https://github.com/edmundhung/conform/pull/272
Full Changelog: https://github.com/edmundhung/conform/compare/v0.8.2...v0.9.0
Published by edmundhung about 1 year ago
insert
intent triggering an error on the client when default value is set by @albert-schilling (#286)Full Changelog: https://github.com/edmundhung/conform/compare/v0.8.1...v0.8.2
Published by edmundhung about 1 year ago
params
option on the refine
helper for better error map support (#264) by @jansedlonrefine(ctx, {
when: intent === 'submit' || intent === 'validate/field',
validate: () => validate(...),
params: {
i18n: "key"
}
})
insert
list intent for inserting a new row to a list at a given index. If no index is given, then the element will be appended at the end of the list. Both append
and prepend
list intent are deprecated and will be removed on v1. (#270) by @albert-schilling<button {...list.insert('name', { index, defaultValue })}>Insert</button>
preprocess
bug upstream. (#283)parse
helper handling on environment with no File
constructor defined. (#281)Full Changelog: https://github.com/edmundhung/conform/compare/v0.8.0...v0.8.1
Published by edmundhung about 1 year ago
import { z } from 'zod';
// Before: Use .min(1) to mark a field as required
const required = z.string().min(1, 'Field is required');
// Now: It's required by default and use .optional() to mark it as optional
const required = z.string({ required_error: 'Field is required' });
const optional = z.string().optional();
// Before: Manualy transform the value to the desired type
const numberInput = z.string().transform((val) => Number(value));
// or use preprocess
const checkbox = z.preprocess(value => value === 'on', z.boolean());;
// Now: Conform does type coercion for you
const numberInput = z.number();
// Including checkbox. (Note: boolean coercion only works with default value 'on')
const checkbox = z.boolean().optional();
// You can continue transform / preprocess yourself
// e.g. checkbox with custom value "yes"
const checkbox = z.string().transform(value => value === 'yes');
const submission = parse(formData, {
// This option can now be removed
acceptMultipleErrors() {
// ...
}
});
export async function action({ request }) {
const formData = await request.formData();
const submission = parse(formData, {
// ...
});
if (submission.intent === 'submit' || !submission.value) {
return json(submission);
}
if (/* some additional checks */) {
return json({
...submission,
error: {
// Pass an array instead of a string
'': ['Something went wrong']
}
})
}
}
useFieldList
is now an empty list instead of an array with one item. (#245)function Example() {
const [form, fields] = useForm({
// You can set the default value to an array with one item if you want
defaultValue: {
tasks: [''],
},
})
const tasks = useFieldList(fields.tasks);
return (
<form>
<div>
{tasks.map((task, index) => (
<div key={task.key}>
{/* ... */}
</div>
))}
</div>
</form>
);
}
collection
helper. Check the new guide for details. (#201)import { conform, useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { z } from 'zod';
const schema = z.object({
answer: z
.string()
.array()
.nonEmpty('At least one answer is required'),
});
function Example() {
const [form, { answer }] = useForm({
onValidate({ formData }) {
return parse(formData, { schema });
},
});
return (
<form {...form.props}>
<fieldset>
<lengend>Please select the correct answers</legend>
{conform
.collection(answer, {
type: 'checkbox',
options: ['a', 'b', 'c', 'd'],
})
.map((props, index) => (
<div key={index}>
<label>{props.value}</label>
<input {...props} />
</div>
)))}
<div>{answer.error}</div>
</legend>
<button>Submit</button>
</form>
);
}
Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.4...v0.8.0
Published by edmundhung over 1 year ago
import { conform } from '@conform-to/react';
<fieldset {...conform.fieldset(field, { ariaAttributes: true })>
// This is equivalent to:
<fieldset id={field.id} name={field.name} form={fieldset.form} aria-invalid={...} aria-describedby={...}>
parse
(#224)// Instead of checking if the value is an empty string:
const schema = z.object({
text: z.string().min(1, 'Email is required');
});
const submission = parse(formData, { schema });
// To supports native zod required check directly:
const schema = z.object({
email: z.string({ required_error: 'Email is required'),his
});
const submission = parse(formData, { schema, stripEmptyValue: true });
Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.3...v0.7.4
Published by edmundhung over 1 year ago
Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.2...v0.7.3
Published by edmundhung over 1 year ago
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,
})
// ...
}
Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.1...v0.7.2
Published by edmundhung over 1 year ago
useInputEvent()
(#190)Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.0...v0.7.1
Published by edmundhung over 1 year ago
initialReport
config is now removed. Please use the shouldValidate
and shouldRevalidate
config instead (#176)shouldRevalidate
config will now default to the shouldValidate
config instead of onInput
(#184)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'),
});
conform
helpers no longer derive aria attributes by default. You can enable it with the ariaAttributes
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>
)
}
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,
})
// ...
}
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,
});
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',
}),
),
// ...
});
}
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 },
});
Full Changelog: https://github.com/edmundhung/conform/compare/v0.6.3...v0.7.0
Published by edmundhung over 1 year ago
useFieldset()
and useFieldList()
when dealing with undefined or unknown type (#153)Full Changelog: https://github.com/edmundhung/conform/compare/v0.6.2...v0.6.3
Published by edmundhung over 1 year ago
useForm
hook can now infer the shape directly based on the return type of the onValidate
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>
}
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 } }
Submission
type is slightly adjusted for better accessibility (#149)Full Changelog: https://github.com/edmundhung/conform/compare/v0.6.1...v0.6.2
Published by edmundhung over 1 year ago
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',
});
}
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>
);
}
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>
)
}
Full Changelog: https://github.com/edmundhung/conform/compare/v0.6.0...v0.6.1
Published by edmundhung over 1 year ago
field.config
is now merged with the field
itself (#113)function Example() {
const [form, { message }] = useForm();
return (
<form>
- <input {...conform.input(message.config)} />
+ <input {...conform.input(message)} />
{message.error}
</form>
);
submission.intent
is now merged with submission.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);
}
// ...
}
validate
and formatError
helpers are replaced by a new parse
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 });
},
// ...
});
// ...
}
VALIDATION_SKIPPED
and VALIDATION_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>
);
}
mode
option is removed. Conform will now decide the validation mode based on whether onValidate
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 });
// },
});
// ...
}
state
option is now called lastSubmission
on the useForm hook (#115)useControlledInput
hook is removed, please use useInputEvent (#97)getFormElements
and requestSubmit
API are also removed (#110)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>
)
}
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 });
},
});
// ...
}
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>
);
}
form.props
(#114)Full Changelog: https://github.com/edmundhung/conform/compare/v0.5.1...v0.6.0
Published by edmundhung over 1 year ago
useControlledInput
API is now deprecated and replaced with the new useInputEvent
hook. (https://github.com/edmundhung/conform/pull/90)Please check the new integration guide for details.
Full Changelog: https://github.com/edmundhung/conform/compare/v0.5.0...v0.5.1
Published by edmundhung almost 2 years ago
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.
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>
);
}
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>
);
}
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>
)
}
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>
);
}
Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.1...v0.5.0
Published by edmundhung almost 2 years ago
Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.1...v0.5.0-pre.0
Published by edmundhung almost 2 years ago
Merry Christmas 🎄
useControlledInput
should now be hidden from the accessibility API. (#60)FieldConfig
type is now re-exported from @conform-to/react
to simplify integration with custom inputs. (#57)Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.0...v0.4.1
Published by edmundhung almost 2 years ago
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:
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>
}
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;
};
resolve(schema).validate
API is also replaced by validate()
:// 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;
},
});
// ...
}
submission.value
) no longer removes empty string, which will affect how zod handles required
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'),
});
Special thanks to @brandonpittman for the kind words and support!
Full Changelog: https://github.com/edmundhung/conform/compare/v0.3.1...v0.4.0
Published by edmundhung almost 2 years ago
Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.0-pre.2...v0.4.0-pre.3
Published by edmundhung almost 2 years ago
Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.0-pre.1...v0.4.0-pre.2