Releases: edmundhung/conform
Releases · edmundhung/conform
v0.9.0
Breaking Change
- Restricted the type of
submission.payload
toRecord<string, unknown>
instead ofRecord<string, any>
to unblock usages on Remix v2-pre by @kentcdodds in #272
Full Changelog: v0.8.2...v0.9.0
v0.8.2
Improvements
- Fixed an issue with the new list
insert
intent triggering an error on the client when default value is set by @albert-schilling (#286)
Full Changelog: v0.8.1...v0.8.2
v0.8.1
Improvements
- Introduced a
params
option on therefine
helper for better error map support (#264) by @jansedlon
refine(ctx, {
when: intent === 'submit' || intent === 'validate/field',
validate: () => validate(...),
params: {
i18n: "key"
}
})
- Added a
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. Bothappend
andprepend
list intent are deprecated and will be removed on v1. (#270) by @albert-schilling
<button {...list.insert('name', { index, defaultValue })}>Insert</button>
- Fixed an issue with zod returning only the first error on zod v3.22 caused by a
preprocess
bug upstream. (#283) - Fixed a bug with zod default not working properly due to empty value not stripped. (#282)
- Improved zod
parse
helper handling on environment with noFile
constructor defined. (#281)
New Contributors
- @justintoman made their first contribution in #275
- @jansedlon made their first contribution in #264
- @albert-schilling made their first contribution in #270
Full Changelog: v0.8.0...v0.8.1
v0.8.0
Breaking Changes
- Conform does automatic type coercion with Zod now. The stripEmptyValue option is removed as empty values are always stripped. (#227, #230, #236, #244)
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');
- The acceptMultipleErrors option is removed since Conform handles multiple errors by default. There is no impact on the validation behaviour. If you were using this option, you can safely remove it. (#228)
const submission = parse(formData, {
// This option can now be removed
acceptMultipleErrors() {
// ...
}
});
- Conform now enforce an array for all errors. If you were setting errors manually, like parsing form data with custom resolver or setting additional errors to the submission object, you need to wrap it in an array. (#228)
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']
}
})
}
}
- The default value of
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>
);
}
Improvements
- You can now setup a checkbox group using the conform
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>
);
}
New Contributors
Full Changelog: v0.7.4...v0.8.0
v0.7.4
Improvements
- Added a new helper for fieldset element (#221)
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={...}>
- [Experimetnal] Introduced an option to strip empty value on
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 });
New Contributors
- @kentcdodds made their first contribution in #220
Full Changelog: v0.7.3...v0.7.4
v0.7.3
v0.7.2
Improvement
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,
})
// ...
}
New Contributors
- @lifeiscontent made their first contribution in #193
Full Changelog: v0.7.1...v0.7.2
v0.7.1
Improvements
- Fixed a bug with invalid hidden style property (#189)
- Ensure no unintentional console log printed when using
useInputEvent()
(#190)
Full Changelog: v0.7.0...v0.7.1
v0.7.0
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
v0.6.3
Improvements
- Fixed a bug which considered form validate intent as a submit intent and triggered server validation (#152)
- Improved type inference setup on both
useFieldset()
anduseFieldList()
when dealing with undefined or unknown type (#153)
Full Changelog: v0.6.2...v0.6.3