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 hidden (Show)

conform - v0.9.0

Published by edmundhung about 1 year ago

Breaking Change

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

conform - v0.8.2

Published by edmundhung about 1 year ago

Improvements

  • Fixed an issue with the new list insert intent triggering an error on the client when default value is set by @albert-schilling (#286)

Full Changelog: https://github.com/edmundhung/conform/compare/v0.8.1...v0.8.2

conform - v0.8.1

Published by edmundhung about 1 year ago

Improvements

  • Introduced a params option on the refine helper for better error map support (#264) by @jansedlon
refine(ctx, {
  when: intent === 'submit' || intent === 'validate/field',
  validate: () => validate(...),
  params: {
    i18n: "key"
  }
})
  • Added a insert list intent for inserting a new row to a list at a given index. If no index is given, then the element will be appended at the end of the list. Both append and prepend list intent are deprecated and will be removed on v1. (#270) by @albert-schilling
<button {...list.insert('name', { index, defaultValue })}>Insert</button>
  • Fixed an issue with zod returning only the first error on zod v3.22 caused by a preprocess bug upstream. (#283)
  • Fixed a bug with zod default not working properly due to empty value not stripped. (#282)
  • Improved zod parse helper handling on environment with no File constructor defined. (#281)

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v0.8.0...v0.8.1

conform - v0.8.0

Published by edmundhung about 1 year ago

Breaking Changes

  • Conform does automatic type coercion with Zod now. The stripEmptyValue option is removed as empty values are always stripped. (#227, #230, #236, #244)
import { z } from 'zod';

// Before: Use .min(1) to mark a field as required
const required = z.string().min(1, 'Field is required');

// Now: It's required by default and use .optional() to mark it as optional
const required = z.string({ required_error: 'Field is required' });
const optional = z.string().optional();

// Before: Manualy transform the value to the desired type
const numberInput = z.string().transform((val) => Number(value));
// or use preprocess
const checkbox = z.preprocess(value => value === 'on', z.boolean());;

// Now: Conform does type coercion for you
const numberInput = z.number();
// Including checkbox. (Note: boolean coercion only works with default value 'on')
const checkbox = z.boolean().optional();

// You can continue transform / preprocess yourself
// e.g. checkbox with custom value "yes"
const checkbox = z.string().transform(value => value === 'yes');
  • The acceptMultipleErrors option is removed since Conform handles multiple errors by default. There is no impact on the validation behaviour. If you were using this option, you can safely remove it. (#228)
const submission = parse(formData, {
    // This option can now be removed 
    acceptMultipleErrors() {
        // ...   
    }
});
  • Conform now enforce an array for all errors. If you were setting errors manually, like parsing form data with custom resolver or setting additional errors to the submission object, you need to wrap it in an array. (#228)
export async function action({ request }) {
    const formData = await request.formData();
    const submission = parse(formData, {
        // ...
    });


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

    if (/* some additional checks */) {
        return json({
            ...submission,
            error: {
                // Pass an array instead of a string 
                '': ['Something went wrong']
            }
        }) 
    }
}
  • The default value of useFieldList is now an empty list instead of an array with one item. (#245)
function Example() {
  const [form, fields] = useForm({
    // You can set the default value to an array with one item if you want
    defaultValue: {
      tasks: [''],
    },
  })
  const tasks = useFieldList(fields.tasks);

  return (
    <form>
      <div>
        {tasks.map((task, index) => (
          <div key={task.key}>
            {/* ... */}
          </div>
        ))} 
      </div>
    </form>
  );
}
  • The ariaAttributes option on the conform helpers is now enabled by default (#226)

Improvements

  • You can now setup a checkbox group using the conform collection helper. Check the new guide for details. (#201)
import { conform, useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { z } from 'zod';

const schema = z.object({
  answer: z
    .string()
    .array()
    .nonEmpty('At least one answer is required'),
});

function Example() {
  const [form, { answer }] = useForm({
    onValidate({ formData }) {
      return parse(formData, { schema });
    },
  });

  return (
    <form {...form.props}>
      <fieldset>
        <lengend>Please select the correct answers</legend>
        {conform
          .collection(answer, {
            type: 'checkbox',
            options: ['a', 'b', 'c', 'd'],
          })
          .map((props, index) => (
            <div key={index}>
              <label>{props.value}</label>
              <input {...props} />
            </div>
          )))}
        <div>{answer.error}</div>
      </legend>
      <button>Submit</button>
    </form>
  );
}

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.4...v0.8.0

conform - v0.7.4

Published by edmundhung over 1 year ago

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

  • @kentcdodds made their first contribution in #220

Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.3...v0.7.4

conform - v0.7.3

Published by edmundhung over 1 year ago

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: https://github.com/edmundhung/conform/compare/v0.7.2...v0.7.3

conform - v0.7.2

Published by edmundhung over 1 year ago

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: https://github.com/edmundhung/conform/compare/v0.7.1...v0.7.2

conform - v0.7.1

Published by edmundhung over 1 year ago

Improvements

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

Full Changelog: https://github.com/edmundhung/conform/compare/v0.7.0...v0.7.1

conform - v0.7.0

Published by edmundhung over 1 year ago

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

    // ...
}
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 },
});
  • Added contextual error map support with the zod parse helper (#177)
  • Conform will now ignore duplicated intent instead of throwing an error to get around the FormData issue on Safari 15. (#164)
  • Fixed an issue that blocks submission on a form with async validation setup even when all errors are resolved (#168)
  • Fixed an error when parsing list intent with slash in the payload (#185)

New Contributors

Full Changelog: https://github.com/edmundhung/conform/compare/v0.6.3...v0.7.0

conform - v0.6.3

Published by edmundhung over 1 year ago

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: https://github.com/edmundhung/conform/compare/v0.6.2...v0.6.3

conform - v0.6.2

Published by edmundhung over 1 year ago

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: https://github.com/edmundhung/conform/compare/v0.6.1...v0.6.2

conform - v0.6.1

Published by edmundhung over 1 year ago

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: https://github.com/edmundhung/conform/compare/v0.6.0...v0.6.1

conform - v0.6.0

Published by edmundhung over 1 year ago

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

  • Fixed header spelling by @brandonpittman in #94

Full Changelog: https://github.com/edmundhung/conform/compare/v0.5.1...v0.6.0

conform - v0.5.1

Published by edmundhung over 1 year ago

What's Changed

Please check the new integration guide for details.

Full Changelog: https://github.com/edmundhung/conform/compare/v0.5.0...v0.5.1

conform - v0.5.0

Published by edmundhung almost 2 years ago

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>
    )
}
  • Introduced a new API to execute command imperatively: requestCommand. (#70)
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: https://github.com/edmundhung/conform/compare/v0.4.1...v0.5.0

conform - v0.5.0-pre.0

Published by edmundhung almost 2 years ago

What's Changed

Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.1...v0.5.0-pre.0

conform - v0.4.1

Published by edmundhung almost 2 years ago

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: https://github.com/edmundhung/conform/compare/v0.4.0...v0.4.1

conform - v0.4.0

Published by edmundhung almost 2 years ago

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: https://github.com/edmundhung/conform/compare/v0.3.1...v0.4.0

conform - v0.4.0-pre.3

Published by edmundhung almost 2 years ago

What's Changed

Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.0-pre.2...v0.4.0-pre.3

conform - v0.4.0-pre.2

Published by edmundhung almost 2 years ago

What's Changed

Full Changelog: https://github.com/edmundhung/conform/compare/v0.4.0-pre.1...v0.4.0-pre.2