remix-validated-form: [Bug]: `zfd.checkbox()` should require user to check by default

zfd.checkbox() should mean the field is required (e.g accept terms and conditions) to follow zod’s pattern

Because your Union transforms undefined into false, technically this is a valid value:

export const checkbox = ({ trueValue = "on" }: CheckboxOpts = {}) =>
  z.union([
    z.literal(trueValue).transform(() => true),
    z.literal(undefined).transform(() => false),
  ]);

I don’t believe the union is necesarry at all, as undefined is the same as false during checks.

If you skip that transform we can zfd.checkbox().optional()

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 34 (29 by maintainers)

Most upvoted comments

I don’t really agree with this. I want zfd.checkbox() to be analogous to z.boolean(). If the checkbox is checked it should be true, and if the checkbox is unchecked then it should be false. And like z.boolean(), we shouldn’t make either value fail the validation by default.

For better or worse, an unchecked checkbox is omitted from the FormData when the form is submitted. To me this means there’s no meaningful way to use optional since there’s no way for us to tell the difference between “unchecked” and “there is no checkbox”.

That said, you can enforce that a checkbox is checked with refine.

acceptsLicenseAgreement:
  zfd
    .checkbox()
    .refine(val => val, "Please accept the license agreement")

In the interest of not spiraling on this forever I think this is going to have to be my last comment on this issue. I’ve outlined in this comment what I see as the path forward for this.

if you don’t have a scenario where false is necessary, you’ve essentially solved for something that was never a problem, and created confusion at the same time.

I think this library should take form data and return properly typed and well modeled representation of the data in the form. From a data modeling perspective, boolean is a more accurate representation of a checkbox than true | undefined. true | undefined is also a “boolean”, it’s just not a boolean which makes it confusing.

If we don’t transform the value to a boolean, then why have a helper at all? It sounds like your ideal version of checkbox

  • Fails on undefined by default
  • Doesn’t transform to a boolean
  • Passes on any string (not just the specified one)

But that’s the exact behavior of z.string.

mustBeChecked: z.string(),
canBeUnchecked: z.string().optional(),
coerceToBoolean: z.string().optional().transform(val => val !== undefined)

The purpose of having a helper for checkboxes at all, for me, is to get that coercion to happen automatically.

I’m also not super concerned about optional not doing anything on checkbox. zod surfaces the optional method even on schemas where it doesn’t make sense to have it (e.g. it’s valid to do z.string().optional().optional() and the second optional does nothing).

The state of a checkbox is also a boolean (checked vs not checked), even if it’s not encoded as a boolean type. And it can be checked even if the user hasn’t interacted with it (can default to checked).

What I’m ultimately trying to get at with zfd.checkbox is “is the checkbox checked, true or false?”. It’s up to the developer to make the judgement on a per-form basis if the user is required to check that checkbox or not. For the vast majority of forms I’ve written, both checked an unchecked were totally valid. Every once in awhile you have a form with a checkbox like acceptsLicenseAgreement where unchecked is invalid in that particular case.