Skip to content

Releases: edmundhung/conform

v0.6.2

21 May 22:48
Compare
Choose a tag to compare

Improvements

  • The useForm hook can now infer the shape directly based on the return type of the onValidate hook (#149)
// e.g. using zod
const schema = z.object({
    title: z.string().min(1, 'Title is required'),
    // ...
})

// Before (Manual typing is needed)
function Example() {
    const [form, fieldset] = useForm<z.infer<typeof schema>>({
        onValidate({ formData }) {
            return parse(formData, { schema });
        }
    })

    console.log(fieldset.title);
    // ^ FieldConfig<string>
}

// After (Directly inferred from the schema)
function Example() {
    const [form, fieldset] = useForm({
        onValidate({ formData }) {
            return parse(formData, { schema });
        }
    })

    console.log(fieldset.title);
    // ^ FieldConfig<string>
}
  • Added support to zod schema intersection when deriving validation attributes with getFieldsetConstraints (#148)
const constraint = getFieldsetConstraint(
    z
    .object({ a: z.string() })
    .and(z.object({ a: z.string().optional(), b: z.string() }))
);
// This generates { a: { required: false }, b: { required: true } }
  • Added union support on both schema and constraint type (#149)
  • The Submission type is slightly adjusted for better accessibility (#149)
  • Fixed a bug which triggers validation when user unfocused a button

New Contributors

Full Changelog: v0.6.1...v0.6.2

v0.6.1

29 Mar 19:00
Compare
Choose a tag to compare

Improvements

  • You can now customize when Conform should validate and revalidate (#127)
function Example() {
  const [form, { email, password }] = useForm({
    // This is now deprecated in favor of the new configs
    initialReport: 'onBlur',

    // Define when Conform should start validation. Default `onSubmit`.
    shouldValidate: 'onBlur',

    // Define when Conform should revalidate again. Default `onInput`.
    shouldRevalidate: 'onBlur',
  });
}
  • The useForm hook now accepts an optional ref object. If it is not provided, conform will fallback to its own ref object instead. (#122)
function Example() {
  const ref = useRef<HTMLFormElement>(null);
  const [form, { email, password }] = useForm({
    ref,
  });

  // `form.ref / form.props.ref` will now be the same as `ref`

  return (
    <form {...form.props}>
      {/* ... */}
    </form>
  );
}
  • The field config now generates an additional descriptionId to support accessible input hint. (#126)
function Example() {
    const [form, { email }] = useForm();

    return (
        <form {...form.props}>
            <label htmlFor={email.id}>Email</label>
            <input {...conform.input(email, { type: "email", description: true })} />
            {/* If invalid, it will set the aria-describedby to "${errorId} ${descriptionId}" */}
            <div id={email.descriptionId}>
                Email hint
            </div>
            <div id={email.errorId}>
                {email.error}
            </div>
        </form>
    )
}
  • The defaultValue and initialError is no longer cached to simplify form reset. (#130)

Full Changelog: v0.6.0...v0.6.1

v0.6.0

11 Mar 22:06
Compare
Choose a tag to compare

Breaking Changes

  • All properties on field.config is now merged with the field itself (#113)
function Example() {
  const [form, { message }] = useForm();

  return (
    <form>
-      <input {...conform.input(message.config)} />
+      <input {...conform.input(message)} />
      {message.error}
    </form>
  );
  • The submission.intent is now merged with submission.type to align with the intent button approach that are common in Remix. (#91)
export async function action({ request }: ActionArgs) {
    const formData = await request.formData();
    const submission = parse(formData);

    // The `submission.intent` is `submit` if the user click on the submit button with no specific intent (default)
    if (!submission.value || submission.intent !== 'submit') {
        return json(submission);
    }

    // ...
}
  • The validate and formatError helpers are replaced by a new parse helper, which can be used on both client and server side: (#92)
// The new `parse` helper can be treat as a replacement of the parse helper from `@conform-to/react`
import { parse } from '@conform-to/zod'; // or `@conform-to/yup`

const schema = z.object({ /* ... */ });

export async function action({ request }: ActionArgs) {
    const formData = await request.formData();
    const submission = parse(formData, {
        schema,
        
        // If you need to run async validation on the server
        async: true,
    });

    // `submission.value` is defined only if no error
    if (!submission.value || submission.intent !== 'submit') {
        return json(submission);
    }

    // ...
}

export default function Example() {
    const [form, fieldset] = useForm({
        onValidate({ formData }) {
            return parse(formData, { schema });
        },
        // ...
    });

    // ...
}
  • Redesigned the async validation setup with the VALIDATION_SKIPPED and VALIDATION_UNDEFINED message (#100)
import { conform, useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { z } from 'zod';

// Instead of sharing a schema, we prepare a schema creator
function createSchema(
    intent: string,
    // Note: the constraints parameter is optional
    constraints: {
        isEmailUnique?: (email: string) => Promise<boolean>;
    } = {},
) {
    return z.object({
        name: z
            .string()
            .min(1, 'Name is required'),
        email: z
            .string()
            .min(1, 'Email is required')
            .email('Email is invalid')
            // We use `.superRefine` instead of `.refine` for better control 
            .superRefine((value, ctx) => {
                if (intent !== 'validate/email' && intent !== 'submit') {
                    // Validate only when necessary
                    ctx.addIssue({
                        code: z.ZodIssueCode.custom,
                        message: conform.VALIDATION_SKIPPED,
                    });
                } else if (typeof constraints.isEmailUnique === 'undefined') {
                    // Validate only if the constraint is defined
                    ctx.addIssue({
                        code: z.ZodIssueCode.custom,
                        message: conform.VALIDATION_UNDEFINED,
                    });
                } else {
                    // Tell zod this is an async validation by returning the promise
                    return constraints.isEmailUnique(value).then((isUnique) => {
                        if (isUnique) {
                            return;
                        }

                        ctx.addIssue({
                            code: z.ZodIssueCode.custom,
                            message: 'Email is already used',
                        });
                    });
                }
            }),
        title: z
            .string().min(1, 'Title is required')
            .max(20, 'Title is too long'),
    });
}

export let action = async ({ request }: ActionArgs) => {
    const formData = await request.formData();
    const submission = await parse(formData, {
        schema: (intent) =>
            // create the zod schema with the intent and constraint
            createSchema(intent, {
                async isEmailUnique(email) {
                    // ...
                },
            }),
        async: true,
    });

    return json(submission);
};

export default function EmployeeForm() {
    const lastSubmission = useActionData();
    const [form, { name, email, title }] = useForm({
        lastSubmission,
        onValidate({ formData }) {
            return parse(formData, {
                // Create the schema without any constraint defined
                schema: (intent) => createSchema(intent),
            });
        },
    });

    return (
        <Form method="post" {...form.props}>
            {/* ... */}
        </Form>
    );
}
  • The validation mode option is removed. Conform will now decide the validation mode based on whether onValidate is defined or not. (#95)
export default function Example() {
    const [form, fieldset] = useForm({
        // Server validation will be enabled unless the next 3 lines are uncommented
        // onValidate({ formData }) {
        //    return parse(formData, { schema });
        // },
    });

    // ...
}
  • The state option is now called lastSubmission on the useForm hook (#115)
  • The useControlledInput hook is removed, please use useInputEvent (#97)
  • The getFormElements and requestSubmit API are also removed (#110)

Improvements

  • Added multiple errors support (#96)
import { parse } from '@conform-to/zod';
import { z } from 'zod';

export async function action({ request }: ActionArgs) {
    const formData = await request.formData();
    const submission = parse(formData, {
        schema: z.object({
            // ...
            password: z
                .string()
                .min(10, 'The password should have minimum 10 characters')
                .refine(password => password.toLowerCase() === password, 'The password should have at least 1 uppercase character')
                .refine(password => password.toUpperCase() === password, 'The password should have at least 1 lowercase character')
        }),

        // Default to false if not specified
        acceptMultipleErrors({ name }) {
            return name === 'password';
        }
    });

    // ...
}

export default function Example() {
    const lastSubmission = useActionData();
    const [form, { password }] = useForm({
        lastSubmission,
    });

    return (
        <Form {...form.props}>
            { /* ... */ }
            <div>
                <label>Password</label>
                <input {...conform.input(password, { type: 'password' })} />
                <ul>
                    {password.errors?.map((error, i) => (
                        <li key={i}>{error}</li>
                    ))}
                </ul>
            </div>
            { /* ... */ }
        </Form>

    )
}
  • Simplified access to form attributes within the onSubmit handler (#99)
export default function Login() {
  const submit = useSubmit();
  const [form] = useForm({
    async onSubmit(event, { formData, method, action, encType }) {
      event.preventDefault();

      formData.set("captcha", await captcha());

      // Both method, action, encType are properly typed
      // to fullfill the types required by the submit function
      // with awareness on submitter attributes
      submit(formData, { method, action, encType });
    },
  });

  // ...
}
  • Introduced a new validateConstraint helper to fully utilize browser validation. Best suited for application built with react-router. (#89)
import { useForm, validateConstraint } from '@conform-to/react';
import { Form } from 'react-router-dom';

export default function SignupForm() {
    const [form, { email, password, confirmPassword }] = useForm({
        onValidate(context) {
            // This enables validating each field based on the validity state and custom cosntraint if defined
            return validateConstraint(
              ...context,
              constraint: {
                // Define custom constraint
                match(value, { formData, attributeValue }) {
                    // Check if the value of the field match the value of another field
                    return value === formData.get(attributeValue);
                },
            });
        }
    });

    return (
        <Form method="post" {...form.props}>
            <div>
                <label>Email</label>
                <input
                    name="email"
                    type="email"
                    required
                    pattern="[^@]+@[^@]+\\.[^@]+"
                />
                {email.error === 'required' ? (
                    <div>Email is required</div>
                ) : email.error === 'type' ? (
                    <div>Email is invalid</div>
                ) : null}
            </div>
            <div>
                <label>Password</label>
                <input
                    name="password"
                    type="password"
                    required
                />
                {password.error === 'required' ? (
                    <div>Password is required</div>
                ) : null}
            </div>
            <div>
                <label>Confirm Pas...
Read more

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