conform

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

Downloads
1M
Stars
1.9K
Committers
25

Bot releases are visible (Hide)

conform - v1.2.2

Published by edmundhung about 1 month ago

What's Changed

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

conform - v1.2.1

Published by edmundhung about 1 month ago

What's Changed

  • Fixed an issue with Conform overwriting the value of input buttons in #766. In v1.2.0, if you have any input buttons rendered, their value will be likely rewritten to empty string as Conform treats it as normal inputs and tries to update its value based on the default value.

Full Changelog: https://github.com/edmundhung/conform/compare/v1.2.0...v1.2.1

conform - v1.2.0

Published by edmundhung about 1 month ago

Embrace the platform

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:

  • It is no longer required to pass the 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} />
  • Helpers like getInputProps, getSelectProps or getTextareaProps are no longer spreading a 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.
  • Outstanding issues caused by inputs being unmounted (e.g. #701 and #730) are now fixed

Pre-release

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

Other Improvements

  • Improved the types of submission.payload in #706. If you were using Remix with single fetch, the action results should no longer be in type never. Thanks @timvandam!
  • Fixed empty string default value support in #741. Previously, we suggested using .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.
  • Implement zod object coercion in #733. Conform should support nested fields with only checkboxes now.
  • Added bigint coercion support with zod in #726. Thanks @lifeiscontent!
  • Improved the types of the default value in #719. As FormValue should never be null. Thanks @aaronadamsCA!
  • Added a multiple select example with shadcn-ui in #753. Thanks @pwli0755!
  • Improved the shadcn-ui Switch Component example to use control.value in #721. Thanks @reborn2135!
  • Fixed typo in parseWithYup.md and FormProvider.md in #708, #751. Thanks @uttk, @felixyeboah!
  • Improved the ja docs messages in #709, #710, #711, #712. Thanks @k70suK3-k06a7ash1!
  • Explained the usage of allErrors with checkbox group in #735.

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.5...v1.2.0

conform - v1.1.5

Published by edmundhung 4 months ago

Improvements

  • Fixed an issue with 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

conform - v1.1.4

Published by edmundhung 5 months ago

Improvements

  • The default value are now serialized properly when inserting an item to a list (#648)
  • Fixed Bigint support on default value (#613, #619, #636)
  • Addressed a few typos and code snippet issues on the docs (#621, #622, #646)
  • Improved build and test setup thanks to @lifeiscontent 🙌🏼 (#616, #615)

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.3...v1.1.4

conform - v1.1.3

Published by edmundhung 6 months ago

What's Changed

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.2...v1.1.3

conform - v1.1.2

Published by edmundhung 6 months ago

What's Changed

Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.1...v1.1.2

conform - v1.1.1

Published by edmundhung 6 months ago

What's Changed

Full Changelog: https://github.com/edmundhung/conform/compare/v1.1.0...v1.1.1

conform - v1.1.0

Published by edmundhung 6 months ago

Improvements

  • The form value should now keep in synced on DOM updates (e.g. when you render an addition input) (#491)
  • You can now access the latest form or field metadata in the callback without the need to subscribe it during render (#467)
  • Form errors will be cleared immediately on form submit now instead of waiting until the server result is back (#553)
  • Both the update and reset intents now accept an optional index similar to the insert intent (#555)
  • Conform will revalidate on blur only if there was any changes made before (i.e. an input event was triggered) to minimize the chance server error get cleared simply because of moving focus out of the inputs. (#559)
  • The useFormMetadata hook now accept no formId (#560)
  • Fixed an issue with form reset failed if the form element is unmounted and form id getting out of sync (#571)
  • The type prop returned from the getCollectionProps helper is narrowed down to the specific type by @AMEH64 (#562)
  • Added object and array support to getYupConstraint by @gglee89 (#465)

Docs

New japanese docs are now available on ja.conform.guide! Huge thanks to @coji for the translations. (#558)

  • Added a language switcher by @coji (#544)
  • Fixed wrong import example by @hpiaia (#574)
  • Added one-time-code input to shadcn-ui examples by @lewisblackburn (#530)

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.6...v1.1.0

conform - v1.1.0-pre.0

Published by edmundhung 7 months ago

Improvements

  • The form value should now keep in synced on DOM updates (e.g. when you render an addition input) (#491)
  • You can now access the latest form or field metadata in the callback without the need to subscribe it during render (#467)
  • Form errors will be cleared immediately on form submit now instead of waiting until the server result is back (#553)
  • Both the update and reset intents now accept an optional index similar to the insert intent (#555)
  • Conform will revalidate on blur only if there was any changes made before (i.e. an input event was triggered) to minimize the chance server error get cleared simply because of moving focus out of the inputs. (#559)
  • The useFormMetadata hook now accept no formId (#560)

Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.6...v1.1.0-pre.0

conform - v1.0.6

Published by edmundhung 7 months ago

What's Changed

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.5...v1.0.6

conform - v1.0.5 Latest Release

Published by edmundhung 7 months ago

What's Changed

Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.4...v1.0.5

conform - v1.0.4

Published by edmundhung 7 months ago

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: https://github.com/edmundhung/conform/compare/v1.0.3...v1.0.4

conform - v1.0.3

Published by edmundhung 8 months ago

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

Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.2...v1.0.3

conform - v1.0.2

Published by edmundhung 8 months ago

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

Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.1...v1.0.2

conform - v1.0.1

Published by edmundhung 9 months ago

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

Full Changelog: https://github.com/edmundhung/conform/compare/v1.0.0...v1.0.1

conform - v1.0.0

Published by edmundhung 9 months ago

If you are looking into upgrading to v1, checkout the migration guide here.

Thank you everyone for your support on this project ❤️

conform - v1.0.0-rc.1

Published by edmundhung 9 months ago

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 for lastResult, for improved compatibility with TypeScript's exactOptionalPropertyTypes 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: https://github.com/edmundhung/conform/compare/v1.0.0-rc.0...v1.0.0-rc.1

conform - v1.0.0-rc.0

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

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 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>
  );
}

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 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}
    />
  );
}

Refined intent button setup

  • Both 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>
  );
}
  • You can now reset a form with form.reset or update any field value with form.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>
  );
}
conform - v0.9.1

Published by edmundhung 11 months ago

Improvements

  • The lastsubmission option now accepts null value by @zifahm in https://github.com/edmundhung/conform/pull/292
  • Fixed several typos on the documentation by @fiws, @mattmazzola, @MrLeebo, @sylvainDNS, @Littletonconnor (#294, #296, #299, #304, #320)

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v0.9.0...v0.9.1