Skip to content

Releases: edmundhung/conform

v0.7.4

17 Jul 19:53
Compare
Choose a tag to compare

Improvements

  • Added a new helper for fieldset element (#221)
import { conform } from '@conform-to/react';

<fieldset {...conform.fieldset(field, { ariaAttributes: true })>

// This is equivalent to:
<fieldset id={field.id} name={field.name} form={fieldset.form} aria-invalid={...} aria-describedby={...}> 
  • [Experimetnal] Introduced an option to strip empty value on parse (#224)
// Instead of checking if the value is an empty string:
const schema = z.object({
  text: z.string().min(1, 'Email is required');
});

const submission = parse(formData, { schema });

// To supports native zod required check directly:
const schema = z.object({
  email: z.string({ required_error: 'Email is required'),his 
});

const submission = parse(formData, { schema, stripEmptyValue: true });

New Contributors

Full Changelog: v0.7.3...v0.7.4

v0.7.3

11 Jul 21:32
Compare
Choose a tag to compare

Improvements

  • Fixed an issue with list default value restored after deletion (#209)
  • Ensure the zod refine helper is running by default (#208)
  • Added an example with useFetcher (#203)

New Contributors

Full Changelog: v0.7.2...v0.7.3

v0.7.2

26 Jun 21:51
Compare
Choose a tag to compare

Improvement

  • Fixed an issue when trying to reset the form after submission with a new approach (#194)
export let action = async ({ request }: ActionArgs) => {
    const formData = await request.formData();
    const submission = parse(formData, { schema });

    if (submission.intent !== 'submit' || !submission.value) {
        return json(submission);
    }

    return json({
        ...submission,
        // Notify the client to reset the form using `null`
        payload: null,
    });
};

export default function Component() {
    const lastSubmission = useActionData<typeof action>();
    const [form, { message }] = useForm({
        // The last submission should be updated regardless the submission is successful or not
        // If the submission payload is empty:
        // 1. the form will be reset automatically
        // 2. the default value of the form will also be reset if the document is reloaded (e.g. nojs)
        lastSubmission,
    })

    // ...
}

New Contributors

Full Changelog: v0.7.1...v0.7.2

v0.7.1

20 Jun 12:42
Compare
Choose a tag to compare

Improvements

  • Fixed a bug with invalid hidden style property (#189)
  • Ensure no unintentional console log printed when using useInputEvent() (#190)

Full Changelog: v0.7.0...v0.7.1

v0.7.0

19 Jun 21:12
Compare
Choose a tag to compare

Breaking Changes

  • Improved ESM compatibility with Node and Typescript. If you were running on ESM with hydration issue like this report, please upgrade. (#159, #160)
  • The initialReport config is now removed. Please use the shouldValidate and shouldRevalidate config instead (#176)
  • The shouldRevalidate config will now default to the shouldValidate config instead of onInput (#184)
  • The useInputEvent hook requires a ref object now (#173)
// Before - always returns a tuple with both ref and control object
const [ref, control] = useInputEvent();

// After - You need to provide a ref object now
const ref = useRef<HTMLInputElement>(null);
const control = useInputEvent({
  ref,
});

// Or you can provide a function as ref 
const control = useInputEvent({
  ref: () => document.getElementById('do whatever you want'),
});
  • The conform helpers no longer derive aria attributes by default. You can enable it with the ariaAttributes option (#183)
// Before
function Example() {
    const [form, { message }] = useForm();

    return (
        <form>
            <input {...conform.input(message, { type: 'text' })} />
        </form>
    )
}

// After
function Example() {
    const [form, { message }] = useForm();

    return (
        <form>
            <input
                {...conform.input(message, {
                    type: 'text',
                    ariaAttributes: true, // default to `false`
                })}
            />
        </form>
    )
}

Improvements

  • Conform will now track when the lastSubmission is cleared and triggered a form reset automatically. (Note: The example below is updated with the new approach introduced on v0.7.2 instead)
export let action = async ({ request }: ActionArgs) => {
    const formData = await request.formData();
    const submission = parse(formData, { schema });

    if (submission.intent !== 'submit' || !submission.value) {
        return json(submission);
    }

    return json({
        ...submission,
        // Notify the client to reset the form using `null`
        payload: null,
    });
};

export default function Component() {
    const lastSubmission = useActionData<typeof action>();
    const [form, { message }] = useForm({
        // The last submission should be updated regardless the submission is successful or not
        // If the submission payload is empty:
        // 1. the form will be reset automatically
        // 2. the default value of the form will also be reset if the document is reloaded (e.g. nojs)
        lastSubmission,
    })

    // ...
}
Original approach on v0.7.0
const actionData = useActionData();
const [form, fields] = useForm({
  // Pass the submission only if the action was failed
  // Or, skip sending the submission back on success
  lastSubmission: !actionData?.success ? actionData?.submission : null,
});
  • New refine helper to reduce the boilerplate when setting up async validation with zod (#167)
// Before
function createSchema(
  intent: string,
  constraints: {
    isEmailUnique?: (email) => Promise<boolean>;
  } = {},
) {
  return z.object({
    email: z
      .string()
      .min(1, 'Email is required')
      .email('Email is invalid')
      .superRefine((email, ctx) => {
        if (intent !== 'submit' && intent !== 'validate/email') {
          // 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',
            });
          });
        }
      }),
    // ...
  });
}

// After
import { refine } from '@conform-to/zod';

function createSchema(
  intent: string,
  constraints: {
    isEmailUnique?: (email) => Promise<boolean>;
  } = {},
) {
  return z.object({
    email: z
      .string()
      .min(1, 'Email is required')
      .email('Email is invalid')
      .superRefine((email, ctx) =>
        refine(ctx, {
          validate: () => constraints.isEmailUnique?.(email),
          when: intent === 'submit' || intent === 'validate/email',
          message: 'Email is already used',
        }),
      ),
    // ...
  });
}
  • Added basic zod union / discriminatedUnion support when inferring constraint (#165)
const schema = z
    .discriminatedUnion('type', [
        z.object({ type: z.literal('a'), foo: z.string(), baz: z.string() }),
        z.object({ type: z.literal('b'), bar: z.string(), baz: z.string() }),
    ])
    .and(
        z.object({
            qux: z.string(),
        }),
    ),

// Both `foo` and `bar` is considered optional now
// But `baz` and `qux` remains required  
expect(getFieldsetConstraint(schema)).toEqual({
    type: { required: true },
    foo: { required: false },
    bar: { required: false },
    baz: { required: true },
    quz: { required: true },
});

New Contributors

Full Changelog: v0.6.3...v0.7.0

v0.6.3

24 May 21:40
Compare
Choose a tag to compare

Improvements

  • Fixed a bug which considered form validate intent as a submit intent and triggered server validation (#152)
  • Improved type inference setup on both useFieldset() and useFieldList() when dealing with undefined or unknown type (#153)

Full Changelog: v0.6.2...v0.6.3

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