Skip to content

Releases: edmundhung/conform

v0.9.0

10 Sep 21:18
Compare
Choose a tag to compare

Breaking Change

  • Restricted the type of submission.payload to Record<string, unknown> instead of Record<string, any> to unblock usages on Remix v2-pre by @kentcdodds in #272

Full Changelog: v0.8.2...v0.9.0

v0.8.2

07 Sep 18:25
Compare
Choose a tag to compare

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: v0.8.1...v0.8.2

v0.8.1

03 Sep 13:59
Compare
Choose a tag to compare

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: v0.8.0...v0.8.1

v0.8.0

10 Aug 19:18
Compare
Choose a tag to compare

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: v0.7.4...v0.8.0

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