Skip to content

Releases: edmundhung/conform

v0.5.1

25 Jan 23:31
Compare
Choose a tag to compare

What's Changed

  • The useControlledInput API is now deprecated and replaced with the new useInputEvent hook. (#90)

Please check the new integration guide for details.

Full Changelog: v0.5.0...v0.5.1

v0.5.0

17 Jan 21:15
Compare
Choose a tag to compare

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

04 Jan 21:18
Compare
Choose a tag to compare
v0.5.0-pre.0 Pre-release
Pre-release

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

25 Dec 11:18
Compare
Choose a tag to compare

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

Full Changelog: v0.4.0...v0.4.1

v0.4.0

29 Oct 20:55
Compare
Choose a tag to compare

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 by validate():
// 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 handles required 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

Special thanks to @brandonpittman for the kind words and support!

Full Changelog: v0.3.1...v0.4.0

v0.4.0-pre.3

29 Oct 20:10
Compare
Choose a tag to compare
v0.4.0-pre.3 Pre-release
Pre-release

What's Changed

Full Changelog: v0.4.0-pre.2...v0.4.0-pre.3

v0.4.0-pre.2

26 Oct 23:29
Compare
Choose a tag to compare
v0.4.0-pre.2 Pre-release
Pre-release

What's Changed

Full Changelog: v0.4.0-pre.1...v0.4.0-pre.2

v0.4.0-pre.1

21 Oct 20:17
Compare
Choose a tag to compare
v0.4.0-pre.1 Pre-release
Pre-release

What's Changed

Full Changelog: v0.4.0-pre.0...v0.4.0-pre.1

v0.4.0-pre.0

16 Oct 22:33
Compare
Choose a tag to compare
v0.4.0-pre.0 Pre-release
Pre-release

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

20 Sep 20:03
Compare
Choose a tag to compare

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

Thank you!

Full Changelog: v0.3.0...v0.3.1