Skip to content

v0.6.0

Compare
Choose a tag to compare
@edmundhung edmundhung released this 11 Mar 22:06
· 372 commits to main since this release

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 Password</label>
                <input
                    name="confirmPassword"
                    type="password"
                    required
                    data-constraint-match="password"
                />
                {confirmPassword.error === 'required' ? (
                    <div>Confirm Password is required</div>
                ) : confirmPassword.error === 'match' ? (
                    <div>Password does not match</div>
                ) : null}
            </div>
            <button>Signup</button>
        </Form>
    );
}
  • Added support of aria-attributes on form.props (#114)
  • Fixed an issue with error not being caught on list validation, e.g. min / max (#75)
  • Conform will now focus on first invalid non button fields even when pressing enter (#109)

Docs

Full Changelog: v0.5.1...v0.6.0