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 month ago
fix: revert auto field value update (#778)
Revert https://github.com/edmundhung/conform/pull/729 and https://github.com/edmundhung/conform/pull/766
The auto field value update feature introduced in v1.2.0 has caused several critical issues with significant user impact. While I appreciate what they accomplished, I’ve realized the current solution isn't robust enough to handle all potential use cases. To minimize the impact on everyone, I believe it's best to revert these changes for now.
Full Changelog: https://github.com/edmundhung/conform/compare/v1.2.1...v1.2.2
Published by edmundhung about 1 month ago
Full Changelog: https://github.com/edmundhung/conform/compare/v1.2.0...v1.2.1
Published by edmundhung about 1 month ago
One of the most critical changes in this release is the auto field value update implemented in #729. Conform now updates input value using the DOM api instead of relying on the key
to re-mount the inputs with the new defaultValue. It means:
key
(e.g. fields.title.key
) to the input elements unless you are rendering a list.// Before: The minimum setup
<input key={field.title.key} name={fields.title.name} defaultValue={fields.title.defaultValue} />
// Now: the key props is no longer required
<input name={fields.title.name} defaultValue={fields.title.defaultValue} />
// Bonus: if the form is not rendered server side, or you don't mind the user experience before JS is loaded...
<input name={fields.title.name} />
key
to the input element. If you were seeing the message Warning: A props object containing a "key" prop is being spread into JSX
, it should be resolved now.Thanks to pkg.pr.new, we are now releasing a preview version on every pull request (#742)
You will find a comment on the PR like this one from pkg.pr.new with each package name listed. If you expand the item, you will see a command to install the pre-release version. If you are not using pnpm, you can swap it with npm install, or just copy the package URL and replace the version in the package.json with it.
We are also shipping a pre-release version on every commit merged to main
in the format https://pkg.pr.new/@conform-to/package@commit
. For example, if you would like to install the pre-release version of @conform-to/dom
and @conform-to/zod
up to db63782, you can run:
npm install https://pkg.pr.new/@conform-to/dom@db63782
npm install https://pkg.pr.new/@conform-to/zod@db63782
submission.payload
in #706. If you were using Remix with single fetch, the action results should no longer be in type never
. Thanks @timvandam!.default()
to set a fallback value. However, .default()
does not work as expected with z.string().default('')
. This issue has now been resolved, but keep in mind that the default value is still subject to validation errors. For more predictable results, we recommend using .transform(value => value ?? defaultValue)
instead.FormValue
should never be null
. Thanks @aaronadamsCA!control.value
in #721. Thanks @reborn2135!parseWithYup.md
and FormProvider.md
in #708, #751. Thanks @uttk, @felixyeboah!Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.5...v1.2.0
Published by edmundhung 4 months ago
unstable_useControl
not resetting the value of the registered input on form reset (#674)Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.4...v1.1.5
Published by edmundhung 5 months ago
Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.3...v1.1.4
Published by edmundhung 6 months ago
type
prop returned from the getInputProps
helper by @AMEH64 in https://github.com/edmundhung/conform/pull/579
instanceof
for Zod schemas by @colinhacks in https://github.com/edmundhung/conform/pull/601
Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.2...v1.1.3
Published by edmundhung 6 months ago
Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.1...v1.1.2
Published by edmundhung 6 months ago
Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.0...v1.1.1
Published by edmundhung 6 months ago
update
and reset
intents now accept an optional index similar to the insert
intent (#555)useFormMetadata
hook now accept no formId
(#560)type
prop returned from the getCollectionProps
helper is narrowed down to the specific type by @AMEH64 (#562)New japanese docs are now available on ja.conform.guide! Huge thanks to @coji for the translations. (#558)
Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.6...v1.1.0
Published by edmundhung 7 months ago
update
and reset
intents now accept an optional index similar to the insert
intent (#555)useFormMetadata
hook now accept no formId
(#560)Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.6...v1.1.0-pre.0
Published by edmundhung 7 months ago
Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.5...v1.0.6
Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.4...v1.0.5
Published by edmundhung 7 months ago
Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.3...v1.0.4
Published by edmundhung 8 months ago
Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.2...v1.0.3
Published by edmundhung 8 months ago
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.
useForm
hook.Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.1...v1.0.2
Published by edmundhung 9 months ago
useInputControl
hook should now populate initial value correctly with multiple select support.conformZodMessage.VALIDATION_SKIPPED
not working as expected.form.update()
will now serialize the value and populated on the input properlyuseForm
hook should now infer the bigint type correctly by @lifeiscontentFull Changelog: https://github.com/edmundhung/conform/compare/v1.0.0...v1.0.1
Published by edmundhung 9 months ago
This release candidate lands a few improvements from v1.0.0-rc.0
event.preventDefault()
within the onSubmit handler should now prevent form submission properlyundefined
as an accepted value for lastResult
, for improved compatibility with TypeScript's exactOptionalPropertyTypes
by @aaronadamsCA.requestSubmit
API if it is availableFull Changelog: https://github.com/edmundhung/conform/compare/v1.0.0-rc.0...v1.0.0-rc.1
Published by edmundhung 9 months ago
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
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 on getInputProps
is now required.
function Example() {
return <input {...getInputProps(fields.title, { type: 'text' })} />;
}
form.props
is removed. You can use the helper getFormProps()
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
and conform.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 called parseWithZod
with getFieldsetConstraint
renamed to getZodConstraint
The parse
helper on @conform-to/yup
is now called parseWithYup
with getFieldsetConstraint
renamed to getYupConstraint
Both useFieldset
and useFieldList
hooks are removed. You can now use meta.getFieldset()
or meta.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>
);
}
useInputControl
hookThe 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 through control.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}
/>
);
}
validate
and list
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>
);
}
form.reset
or update any field value with form.update
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>
);
}
Published by edmundhung 11 months ago
lastsubmission
option now accepts null
value by @zifahm in https://github.com/edmundhung/conform/pull/292
Full Changelog: https://github.com/edmundhung/conform/compare/v0.9.0...v0.9.1