Releases: edmundhung/conform
v1.0.6
What's Changed
- docs(tutorial.md): Fix some typos & grammar by @machour in #535
- fix: ensure nested list keys to be generated properly by @edmundhung in #537
- fix: validate only if the form element is mounted by @edmundhung in #538
New Contributors
Full Changelog: v1.0.5...v1.0.6
v1.0.5
What's Changed
- docs: Fix missing map index in intent-button.md by @hawkcookie in #517
- docs: using a variable-width font for docs by @aust1nz in #500
- fix: conform should derive keys of nested list properly by @edmundhung in #528
- fix: update form value at the form level by @edmundhung in #521
Full Changelog: v1.0.4...v1.0.5
v1.0.4
What's Changed
- Reverted the changes made in v1.0.3 to resolve an issue with form value out of sync if multiple intents are dispatched in a single callback (#513) which caused another issue when used in lifecycle method (#512)
Full Changelog: v1.0.3...v1.0.4
v1.0.3
Improvements
- GET form will no longer include the internal state in the URLSearchParams (#501)
- Resolved an issue with form value out of sync if multiple intents are dispatched in a single callback (#496)
- Fixed a regression introduced in v1.0.2 in which Nextjs complains about returning file to the client (#490)
- Refined type inference with interface and nullable schemas by @aaronadamsCA (#508)
- New Shadcn UI example by @marilari88 (#489)
- Fixed several typos on the docs. Thanks to @Forus-Spec (#471), @nphmuller (#477), @ozanbulut (#483), @notomo (#492), @hawkcookie (#498, #502)
New Contributors
- @nphmuller made their first contribution in #477
- @ozanbulut made their first contribution in #483
- @notomo made their first contribution in #492
- @hawkcookie made their first contribution in #498
Full Changelog: v1.0.2...v1.0.3
v1.0.2
New APIs: unstable_useControl
and <unstable_Control />
In v1, the useInputControl
hook is introduced with ability to insert an hidden input for you. Unfortunately, this is found to be problematic in situations where you need to dynamically render the input. After some discussions, we believe it would be better stop supporting this and have developers deciding how they wanna render the hidden input instead. To avoid breaking changes on the useInputControl
hook, a new useControl
hook is introduced which works similar to useInputControl()
except you are required to register the input element yourself:
Here is an example with Headless UI Listbox
function Select({
name,
options,
placeholder,
}: {
name: FieldName<string>;
placeholder: string;
options: string[];
}) {
const [field] = useField(name);
const control = useControl(field);
return (
<Listbox
value={control.value ?? ''}
onChange={value => control.change(value)}
>
{/* Render a select element manually and register with a callback ref */}
<select
className="sr-only"
aria-hidden
tabIndex={-1}
ref={control.register}
name={field.name}
defaultValue={field.initialValue}
>
<option value="" />
{options.map((option) => (
<option key={option} value={option} />
))}
</select>
<div className="relative mt-1">
<Listbox.Button className="...">
<span className="block truncate">
{control.value ?? placeholder}
</span>
<span className="...">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Listbox.Options className="...">
{options.map((option) => (
<Listbox.Option
key={option}
className="..."
value={option}
>
{({ selected, active }) => (
<>
<span className="...">
{option}
</span>
{option !== '' && selected ? (
<span className="...">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
);
}
You might also find the <Control />
component in the render-props style useful when working with checkbox group in which we can use it to register each individual checkbox element without creating an additional component. Here is an example based on Radix Checkbox:
function CheckboxGroup({
name,
options,
}: {
name: FieldName<string[]>;
options: string[];
}) {
const [field] = useField(name);
// The initialValue can be a string or string array depending on how it was submitted before.
// To make it easy working with the initial value, we make sure it is always an array
const initialValue =
typeof field.initialValue === 'string'
? [field.initialValue]
: field.initialValue ?? [];
return (
<div className="py-2 space-y-4">
{options.map((option) => (
<Control
key={option}
meta={{
key: field.key,
initialValue: initialValue.includes(option) ? option : '',
}}
render={(control) => (
<div
className="flex items-center"
ref={(element) => {
// Radix does not expose the inner input ref. That's why we query it from the container element
control.register(element?.querySelector('input'))
}}
>
<RadixCheckbox.Root
type="button"
className="flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-[4px] bg-white outline-none shadow-[0_0_0_2px_black]"
id={`${field.id}-${option}`}
name={field.name}
value={option}
checked={control.value === option}
onCheckedChange={(state) =>
control.change(state.valueOf() ? option : '')
}
onBlur={control.blur}
>
<RadixCheckbox.Indicator>
<CheckIcon className="w-4 h-4" />
</RadixCheckbox.Indicator>
</RadixCheckbox.Root>
<label
htmlFor={`${field.id}-${option}`}
className="pl-[15px] text-[15px] leading-none"
>
{option}
</label>
</div>
)}
/>
))}
</div>
);
}
Feel free to open a discussion if you have any issue using the new APIs. We will deprecate useInputControl
and remove the unstable
prefix once we are confident with the new APIs.
Improvements
- Improved type inference with nested discriminated union (#459)
- Fixed an issue with zod not coercing the value of a multi select correctly (#447)
- Fixed an issue with react devtool inspecting the state of the
useForm
hook. - Fixed an issue with file input never marked as dirty (#457)
- Fixed several typos on the docs. Thanks to @ngbrown (#449) and @Kota-Yamaguchi (#463)
New Contributors
- @ngbrown made their first contribution in #449
- @Kota-Yamaguchi made their first contribution in #463
Full Changelog: v1.0.1...v1.0.2
v1.0.1
Improvements
- The
useInputControl
hook should now populate initial value correctly with multiple select support. - Fixed an issue with the
conformZodMessage.VALIDATION_SKIPPED
not working as expected. - Conform no longer requires the form element to be available when doing any updates.
form.update()
will now serialize the value and populated on the input properly- The
useForm
hook should now infer the bigint type correctly by @lifeiscontent - New Radix UI example by @marilari88
- Fixed several issues on the documentation including typos and links by @diurivj, @marilari88, @narutosstudent, @jsparkdev, @Forus-Spec and @aust1nz
New Contributors
- @diurivj made their first contribution in #407
- @marilari88 made their first contribution in #402
- @narutosstudent made their first contribution in #415
- @jsparkdev made their first contribution in #421
- @Forus-Spec made their first contribution in #428
- @aust1nz made their first contribution in #431
Full Changelog: v1.0.0...v1.0.1
v1.0.0
v1.0.0-rc.1
This release candidate lands a few improvements from v1.0.0-rc.0
What's changed
- Fixed an issue with inputs on nested list being reset when the list is modified.
- Calling
event.preventDefault()
within the onSubmit handler should now prevent form submission properly - Added
undefined
as an accepted value forlastResult
, for improved compatibility with TypeScript'sexactOptionalPropertyTypes
by @aaronadamsCA. - Removed image as an accepted value for input type by @aaronadamsCA.
- All form controls are now triggered with the native
requestSubmit
API if it is available
Full Changelog: v1.0.0-rc.0...v1.0.0-rc.1
v1.0.0-rc.0
This release candidate is a complete rewrite of the library.
You can find the update remix example at https://stackblitz.com/github/edmundhung/conform/tree/v1.0.0-rc.0/examples/remix
or try it out locally using the following command:
npm install @conform-to/react@next @conform-to/zod@next
Breaking Changes
-
The minimum react version supported is now React 18
-
All conform helpers are renamed.
conform.input
->getInputProps
conform.select
->getSelectProps
conform.textarea
->getTextareaProps
conform.fieldset
->getFieldsetProps
conform.collection
->getCollectionProps
-
The
type
option ongetInputProps
is now required.
function Example() {
return <input {...getInputProps(fields.title, { type: 'text' })} />;
}
form.props
is removed. You can use the helpergetFormProps()
instead.
import { getFormProps } from '@conform-to/react';
function Example() {
const [form] = useForm();
return <form {...getFormProps(form)} />;
}
-
conform.INTENT
is removed. If you need to setup an intent button, please use the name "intent" or anything you preferred. -
You will find
conform.VALIDATION_UNDEFINED
andconform.VALIDATION_SKIPPED
on our zod integration (@conform-to/zod
) instead.conform.VALIDATION_UNDEFINED
->conformZodMessage.VALIDATION_UNDEFINED
conform.VALIDATION_SKIPPED
->conformZodMessage.VALIDATION_SKIPPED
.
-
The
parse
helper on@conform-to/zod
is now calledparseWithZod
withgetFieldsetConstraint
renamed togetZodConstraint
-
The
parse
helper on@conform-to/yup
is now calledparseWithYup
withgetFieldsetConstraint
renamed togetYupConstraint
-
Both
useFieldset
anduseFieldList
hooks are removed. You can now usemeta.getFieldset()
ormeta.getFieldList()
instead.
function Example() {
const [form, fields] = useForm();
// Instead of `useFieldset(form.ref, fields.address)`, it is now:
const address = fields.address.getFieldset();
// Instead of `useFieldList(form.ref, fields.tasks)`, it is now:
const tasks = fields.tasks.getFieldList();
return (
<form>
<ul>
{tasks.map((task) => {
// It is no longer necessary to define an addtional component
// As you can access the fieldset directly
const taskFields = task.getFieldset();
return <li key={task.key}>{/* ... */}</li>;
})}
</ul>
</form>
);
}
Improved submission handling
We have redesigned the submission object received after parsing the formdata to simplify the setup with a new reply
API for you to set additional errors or reset the form.
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, { schema });
/**
* The submission status could be either "success", "error" or undefined
* If the status is undefined, it means that the submission is not ready (i.e. `intent` is not `submit`)
*/
if (submission.status !== 'success') {
return json(submission.reply(), {
// You can also use the status to determine the HTTP status code
status: submission.status === 'error' ? 400 : 200,
});
}
const result = await save(submission.value);
if (!result.successful) {
return json(
submission.reply({
// You can also pass additional error to the `reply` method
formError: ["Submission failed"],
fieldError: {
address: ["Address is invalid"],
},
// or avoid sending the the field value back to client by specifying the field names
hideFields: ['password'],
})
);
}
// Reply the submission with `resetForm` option
return json(submission.reply({ resetForm: true }));
}
export default function Example() {
const lastResult = useActionData<typeof action>();
const [form, fields] = useForm({
// `lastSubmission` is renamed to `lastResult` to avoid confusion
lastResult,
});
// We can now find out the status of the submission from the form metadata as well
console.log(form.status); // "success", "error" or undefined
}
Simplified integration with the useInputControl
hook
The useInputEvent
hook is replaced by the useInputControl
hook with some new features.
-
There is no need to provide a ref of the inner input element anymore. It looks up the input element from the DOM and will insert one for you if it is not found.
-
You can now use
control.value
to integrate a custom input as a controlled input and update the value state throughcontrol.change(value)
. The value will also be reset when a form reset happens
import { useInputControl } from '@conform-to/react';
import { CustomSelect } from './some-ui-library';
function Example() {
const [form, fields] = useForm();
const control = useInputControl(fields.title);
return (
<CustomSelect
name={fields.title.name}
value={control.value}
onChange={(e) => control.change(e.target.value)}
onFocus={control.focus}
onBlur={control.blur}
/>
);
}
Refined intent button setup
- Both
validate
andlist
exports are removed in favor of setting up through the form metadata object.validate
->form.validate
list.insert
->form.insert
list.remove
->form.remove
list.reorder
->form.reorder
list.replace
->form.update
function Example() {
const [form, fields] = useForm();
const tasks = fields.tasks.getFieldList();
return (
<form>
<ul>
{tasks.map((task) => {
return <li key={task.key}>{/* ... */}</li>;
})}
</ul>
<button {...form.insert.getButtonProps({ name: fields.tasks.name })}>
Add (Declarative API)
</button>
<button onClick={() => form.insert({ name: fields.tasks.name })}>
Add (Imperative API)
</button>
</form>
);
}
- You can now reset a form with
form.reset
or update any field value withform.update
Form Context
By setting up a react context with the <FormProvider />
, we will now be able to subscribe to the form metadata using the useField()
hook. This not only avoids prop drilling but also prevent unneccessary re-renders by tracking the usage of indivudal metadata through a proxy and only rerender it if the relevant metadata is changed.
The <FormProvider />
can also be nesteded with different form context and Conform will look up the closest form context unless a formId
is provided.
import { type FieldName, FormProvider, useForm, useField } from '@conform-to/react';
function Example() {
const [form, fields] = useForm({ ... });
return (
<FormProvider context={form.context}>
<form>
<AddressFieldset name={fields.address.name} />
</form>
</FormProvider>
);
}
// The `FieldName<Schema>` type is basically a string with additional type information encoded
type AddressFieldsetProps = {
name: FieldName<Address>
}
export function AddressFieldset({ name }: AddressFieldsetProps) {
const [meta] = useField(name);
const address = meta.getFieldset();
// ...
}
If you want to create a custom input component, it is now possible too!
import { type FieldName, FormProvider, useForm, useField, getInputProps } from '@conform-to/react';
function Example() {
const [form, fields] = useForm({ ... });
return (
<FormProvider context={form.context}>
<form>
<CustomInput name={fields.title.name} />
</form>
</FormProvider>
);
}
type InputProps = {
name: FieldName<string>
}
// Make your own custom input component!
function CustomInput({ name }: InputProps) {
const [
meta,
form, // You can also access the form metadata directly
] = useField(name);
return (
<input {...getInputProps(meta)} />
);
}
Similarly, you can access the form metadata on any component using the useFormMetadata()
hook:
import { type FormId, FormProvider, useForm, getFormProps } from '@conform-to/react';
function Example() {
const [form, fields] = useForm({ ... });
return (
<FormProvider context={form.context}>
<CustomForm id={form.id}>
{/* ... */}
</CustomForm>
</FormProvider>
);
}
function CustomForm({ id, children }: { id: FormId; children: ReactNode }) {
const form = useFormMetadata(id);
return (
<form {...getFormProps(form)}>
{children}
</form>
);
}
v0.9.1
Improvements
- The
lastsubmission
option now acceptsnull
value by @zifahm in #292 - Fixed several typos on the documentation by @fiws, @mattmazzola, @MrLeebo, @sylvainDNS, @Littletonconnor (#294, #296, #299, #304, #320)
New Contributors
- @fiws made their first contribution in #294
- @mattmazzola made their first contribution in #296
- @MrLeebo made their first contribution in #299
- @sylvainDNS made their first contribution in #304
- @Littletonconnor made their first contribution in #320
- @zifahm made their first contribution in #292
Full Changelog: v0.9.0...v0.9.1