Releases: edmundhung/conform
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
v0.4.0-pre.1
What's Changed
- feat!: finialize server validation mode by @edmundhung in #42
Full Changelog: v0.4.0-pre.0...v0.4.0-pre.1
v0.4.0-pre.0
What's Changed
- Add comment about button values by @brandonpittman in #36
- fix(conform-react): remove unwanted state sync in useFieldList by @edmundhung in #38
- feat(conform-dom,conform-react,conform-yup,conform-zod)!: make internal state uncontrolled by @edmundhung in #39
- feat(conform-dom,conform-react,conform-zod,conform-yup)!: server validation by @edmundhung in #40
Full Changelog: v0.3.1...v0.4.0-pre.0
v0.3.1
What's Changed
Conform guide (https://conform.guide)
This includes updated examples and a new Get started guide covering the core ideas behind conform.
I hope you will find it useful 😅
Autofocus first error field
Conform now automatically focus on first error field whenever user tries to initiate a form submission.
This applies to all native input fields with no changes required.
However, if you are working with controlled inputs and you want to have the invalid fields focused as well, you will need to pass the ref object provided by useControlledInput()
to the input.
For example, you need to pass the ref object as inputRef
with material-ui
:
export default function ArticleForm() {
const formProps = useForm();
const { category } = useFieldset<Article>(formProps.ref);
const [categoryInput, control] = useControlledInput(category.config);
return (
<form {...formProps}>
<Stack spacing={3}>
<input {...categoryInput} required />
<TextField
label="Category"
inputRef={control.ref}
value={control.value}
onChange={control.onChange}
onBlur={control.onBlur}
error={Boolean(category.error)}
helperText={category.error}
inputProps={{
// To disable browser report caused by the required
// attribute set by mui input
onInvalid: control.onInvalid,
}}
select
required
>
<MenuItem value="">Please select</MenuItem>
<MenuItem value="a">Option A</MenuItem>
<MenuItem value="b">Option B</MenuItem>
<MenuItem value="c">Option C</MenuItem>
</TextField>
<Button type="submit" variant="contained">
Submit
</Button>
</Stack>
</form>
);
}
You can check out the full example here
New Contributors
- @abenhamdine made their first contribution in #26
- @brandonpittman made their first contribution in #28
Thank you!
Full Changelog: v0.3.0...v0.3.1