Coder Social home page Coder Social logo

code-forge-net / remix-hook-form Goto Github PK

View Code? Open in Web Editor NEW
271.0 271.0 19.0 1.02 MB

Open source wrapper for react-hook-form aimed at Remix.run

License: MIT License

TypeScript 99.02% JavaScript 0.98%
forms hooks javascript react react-hooks remix remix-run

remix-hook-form's People

Contributors

adirishi avatar alemtuzlak avatar devstojko avatar ekerik220 avatar geeron avatar jonbretman avatar mintchkin avatar monsterdeveloper avatar theyoxy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

remix-hook-form's Issues

Type mismatch

Screenshot 2023-10-27 at 1 01 33 PM

I am not sure what's the problem here, but I am following the README.md.

import { z } from 'zod';

const FormDataZodSchema = z.object({
  action: z.string(),
  outline: z.string().min(2, {
    message: 'Outline must be at least 2 characters.',
  }),
});

type FormData = z.infer<typeof FormDataZodSchema>;

const resolver = zodResolver(FormDataZodSchema);

// ...

const form = useRemixForm<FormData>({
    defaultValues: {
      outline: articleProject.articleOutline ?? '',
    },
    mode: 'onSubmit',
    resolver,
  });

  useEffect(() => {
    if (generateOutlineFetcher.data?.outlineMarkdown) {
      form.setValue('outline', generateOutlineFetcher.data.outlineMarkdown);
    }
  }, [form, generateOutlineFetcher.data?.outlineMarkdown]);

  const actionIsPending = isGenerating;

  return (
    <RemixFormProvider {...form}>
      <Form
        className={css({
          display: 'flex',
          flexDir: 'column',
          gap: '8',
        })}
        onSubmit={form.handleSubmit}
      >

`useFetcher()` support

Hey, I'm trying to use remix-hook-form with a <fetcher.Form> returned from useFetcher() hook. I supplied onSubmit props of the <fetcher.Form> with handleSubmit() function returned from useRemixForm(). However, it seems that the fetcher.state does not change when I submit the form. And useRemixForm() does not seem to have any related options. Do you have plans on supporting it?

Multiple buttons submit

Hi there,
I made a form with a submit button which his handled with the default configurations.
Now, I want to add a delete button that will simply call a delete function on the server but I can't manage to do that.
Any ideas?

[suggestion] Implicit Inference of Form Schema in `getValidatedFormData` and `validateFormData`

Hello!

I am currently exploring the remix-hook-form library and observed that when utilizing getValidatedFormData and validateFormData, the form schema needs to be explicitly included in the generic.

Considering that both functions require a resolver, it seems feasible to allow automatic inference of the T type from the resolver.

I could open a PR to introduce this enhancement. But before that, I would appreciate some clarification if there is a specific reason for not implementing this feature that I might be unaware of.

remix-hook-form not compatible with 7.51.0 of react-hook-form

As of 7.51.0 react-hook-form has a property called validatingFields in the formState object.
Since remix-hook-form does not export this, it leads to type errors when upgrading react-hook-form's latest version

error TS2322: Type '{ children: Element; handleSubmit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>; reset: (values?: { message: string; name: string; email: string; phoneNumber: string; } | { ...; } | undefined, options?: Partial<...> | undefined) => void; ... 12 more ...; setFocus: UseFormSetFocus<...>; }' is not assignable to type 'RemixFormProviderProps<{ message: string; name: string; email: string; phoneNumber: string; }>'.
  Types of property 'formState' are incompatible.
    Property 'validatingFields' is missing in type '{ disabled: boolean; dirtyFields: Partial<Readonly<{ message?: boolean | undefined; name?: boolean | undefined; email?: boolean | undefined; phoneNumber?: boolean | undefined; }>>; ... 9 more ...; errors: FieldErrors<...>; }' but required in type 'FormState<{ message: string; name: string; email: string; phoneNumber: string; }>'.

82           <FormProvider {...form}>
              ~~~~~~~~~~~~

  ../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-hook-form/dist/types/form.d.ts:103:5
    103     validatingFields: Partial<Readonly<FieldNamesMarkedBoolean<TFieldValues>>>;
            ~~~~~~~~~~~~~~~~
    'validatingFields' is declared here.

Number validation fails

Hi there, seems like there's an issue with numbers validation on the backend side, coz if I have z.number() in the schema it will correctly pass validation on the frontend but the action will return an error:

{
  message:  "Expected number, received string"
  type: "invalid_type"
}

I believe that's a bug as in readme you saying it should be parsed as a number
image

But based on what I see in the tryParseJSON it will never parse it as a number, also in tests you expecting strings ๐Ÿคทโ€โ™‚๏ธ

How to handle submit success?

  const {
    data,
    errors,
    receivedValues: defaultValues,
  } = await getValidatedFormData<FormData>(request, resolver);

  if (errors) {
    return json({ defaultValues, errors });
  }

  // router.push(`/article/${data.id}/outline`);

  return json(data);
};

export default () => {
  const form = useRemixForm<FormData>({
    mode: 'onSubmit',
    resolver,
  });

  // toast({
  //   title: 'Article Project Created',
  // });

My use case is that I want to trigger toast after successful action.

Property 'root' does not exist on type

Hello! I believe the library may be using the wrong type for the errors property of formState. Perhaps it should use FieldErrors rather than FieldErrorsImpl.

Also, the "root" error doesn't populate back in the formState if passed from the actions.

image image image image

"root" is supported by react-hook-form

image

File Upload With Other Form Data - Question

Hey I love this library! It works super well. I do, however have one conundrum I've been struggling with for a while. I have a form that has a file upload input as well as a few other text inputs, because I want to submit all of this data to the server at once. I can't seem to figure out a way to do it. Any advice? I've tried everything from sending the file as submit data in useRemixForm to messing with stringifyAllValues, and many other things. No luck.

I'm using react dropzone for my file upload component. I have gotten the file upload to work without the use of remix-hook-form, but I really want to be able to validate my other form elements.

here's what my form looks like currently, though I've played with a million variations:

<Form method="post" onSubmit={handleSubmit} encType="multipart/form-data">

                       <Input
                           placeholder="Event Title"
                           errorMsg={errors.title?.message}
                           {...register("title")}
                       />

                       <Textarea placeholder="Event Description"
                                 errorMsg={errors.description?.message}
                                 {...register("description")}
                       />

                       <ImageUpload previewUrl={imagePreview}
                                    minImageWidth={800}
                                    minImageHeight={500}
                                    onDrop={file => {
                                        setValue('upload', file)
                                        setImageFile(file)
                                        setImagePreview(URL.createObjectURL(file))
                                    }} onError={err => console.log(err)} onRemove={() => {
                           if (imagePreview != null) {
                               URL.revokeObjectURL(imagePreview);
                               setImageFile(null);
                               setImagePreview(null)
                           }
                       }
                       }/>

                       {errors.upload?.message && (<p className="text-red-500">{errors.upload?.message}</p>)}

                       <AddressSearchInput onAddressSelection={(item) => {
                           const loc = item.selectedItem
                           if (loc != null) {
                               setValue('location', {
                                   ...loc,
                                   state: loc.stateCode,
                                   lat: loc.latitude,
                                   long: loc.latitude,
                                   zip: loc.postalCode,
                               })
                           }
                           setLocation(item.selectedItem)
                       }} errorMsg={errors.location?.message}
                                           addressData={addressData}
                       />

                       <DateTimePicker
                           value={dateTime}
                           onChange={function (value: { date: Date; hasTime: boolean; }): void {
                               setValue('eventDateTime', value.date)
                               setDateTime(value)
                           }}
                           className="mt-3 bg-pingray-100 font-light focus:ring-2 focus:outline-none focus:ring-bg-pinblue-500"
                       />

                       {errors.eventDateTime?.message != null && (
                           <p className="text-red-500">{errors.eventDateTime?.message}</p>
                       )}

                       <button
                           type="submit"
                           disabled={isSubmitting}
                           className="bg-pinblue-500 shadow hover:shadow-md w-full text-gray-100 p-2 rounded mt-8 font-['bree'] focus:ring-2 focus:outline-none focus:ring-bg-pinblue-500"
                       >
                           {isSubmitting ? 'Submitting...' : 'Create Event'}
                       </button>
</Form>

note I'm registering anything using setValue in a useEffect.

on the action side I've attempted everything from removing my upload value from zod all together to using remix's Multipart form parser and many other variations.

Should I maybe use multiple actions, and somehow split up my file upload from the other form data processing? And if so... how can I do that in the same component?

Missing Blob type check to file upload V3

formData can be of type string or blob. The check here only checks for Files so you don't allow for all Blob types. This results in Blobs being validated as objects and going through the Object path, which gets JSON.stringify() and renders them as empty objects on the server.

} else if (value instanceof File) {
      formData.append(key, value);
}

Code Here: https://github.com/Code-Forge-Net/remix-hook-form/pull/41/files#diff-4f4a4192cd629d907783a6baf48c530f39d6a43181dd3515cc46231868e0ed08R155-R156

add these checks to fix: value instanceof Blob || toString.call(value) === "[object Blob]"; instead of value instanceof File the proceeding change will check for Files & Blobs.

Remix Hook Form File Upload

I'm testing Remix hook form for file upload, but I am having an issue where data is undefined after call validateFormData.

here is my action

export const action = async ({ request, params }: ActionFunctionArgs) => {

  const uploadHandler = unstable_composeUploadHandlers(
    unstable_createFileUploadHandler({
      maxPartSize: 5 * 1024 * 1024,
      file: ({ filename }) => filename,
      directory: "./public/uploads",
    }),
    unstable_createMemoryUploadHandler()
  );
  const formData = await unstable_parseMultipartFormData(
    request,
    uploadHandler
  );

  let { errors, data } = await validateFormData<FormData>(formData, resolver);

  console.log("data", data) --> data is undefined here
}

here is my render function

export default function () {
  const {
    ...
  } = useRemixForm({
    mode: "all",
    resolver,
    defaultValues: {
      title: "",
      content: "",
      image: undefined,
    },
    submitConfig: {
      encType: "multipart/form-data",
      method: "POST",
    },
  });

  return (
    <Form onSubmit={handleSubmit} method="POST">
      <div>
       
          <Input type="text" {...register("title")} />
          
      </div>
      <div>
       
          <Input type="text" {...register("content")} />
         
        </Label>
      </div>
      <div>
          <Input
            type="file"
            id="file-upload"
            accept="image/*"
            {...register("image")}
          />      
      </div>

    </Form>
  );
}

Can't post files / upload files in remix-hook-form

In the below code, you are stringifying the whole form and Files can not be stringified they do not get sent to the server.

export const createFormData = <T extends FieldValues>( data: T, key = "formData" ): FormData => { const formData = new FormData(); const finalData = JSON.stringify(data); formData.append(key, finalData); return formData; };

I'm working on the PR to fix this, and will submit it once I'm finished. I just wanted to document the issue first.

Radio inputs have both `value` and `defaultValue` props defined

When moving from base react-hook-form to remix-hook-form the browser console (Firefox) started showing this error:

Warning: DetailsForm contains an input of type radio with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components

This is happening with radio inputs defined with the register function from userRemixForm:

<label className="space-x-1">
  <input
    type="radio"
    {...register('accountType')}
    value="personal"
  />
  <span>Personal</span>
</label>
<label className="space-x-1">
  <input
    type="radio"
    {...register('accountType')}
    value="corporate"
  />
  <span>Corporate</span>
</label>

I solved this by restructuring register to remove the defaultValue key, but that's not particularly pretty.

Thanks for the great library!

getValidatedFormData auto parse JSON cause error if value is JSON string

Problem

Given this schema

export const FooSchema = z.object({
  title: z.string()
  number: z.string(),
  jsonArray: z.string()
})

which will pass validation on the client side

<input name="title" value="title" />
<input name="number" value="0" />
<input name="jsonArray" value="[]" />

If submit by form.submit() event or submit from Remix.useSubmit() which maybe equal with submit without JS ?
It will fails when using getValidatedFormData because the types of number and jsonArray are not strings. This happens when the following line is executed:

getValidatedFormData(request, FooSchema)

This issue arises because the parseFormData function auto-parses all values due to the use of preserveStringified = false.

const data = isGet(request) ? getFormDataFromSearchParams(request) : await parseFormData(request);

var parseFormData = async (request, preserveStringified = false) => {
  const formData = request instanceof Request ? await request.formData() : request;
  return generateFormData(formData, preserveStringified);
};

Expected behavior

It should pass. Perhaps we can pass preserveStringified as the third parameter of getValidatedFormData?
For now, I must use dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })) as a work around

Type error with RemixFormProvider props

I'm trying to use RemixFormProvider to wrap my form, and am receiving a type error when attempting to pass the form methods to the provider. Here's a minimal reproduction, which is almost identical to the guidance in the docs:

import { zodResolver } from '@hookform/resolvers/zod'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'

const formSchema = z.object({
  username: z.string().min(2).max(50),
})

const resolver = zodResolver(formSchema)

export const TestForm = () => {
  const methods = useRemixForm({
    resolver,
    defaultValues: {
      username: '',
    },
  })

  return (
    <RemixFormProvider {...methods}>
      <form onSubmit={methods.handleSubmit}>
        <input {...methods.register('username')} />
      </form>
    </RemixFormProvider>
  )
}

And here's the type error I'm getting on the Provider:

Type '{ children: Element; handleSubmit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>; reset: (values?: { username: string; } | { ...; } | undefined) => void; ... 12 more ...; setFocus: UseFormSetFocus<...>; }' is not assignable to type 'RemixFormProviderProps<{ username: string; }>'.
  Types of property 'formState' are incompatible.
    Property 'disabled' is missing in type '{ dirtyFields: Partial<Readonly<{ username?: boolean | undefined; }>>; isDirty: boolean; isSubmitSuccessful: boolean; isSubmitted: boolean; isSubmitting: boolean; ... 5 more ...; errors: Partial<...>; }' but required in type 'FormState<{ username: string; }>'.ts(2322)
form.d.ts(89, 5): 'disabled' is declared here.

This seems similar to this recent issue (#42 ), but it's a slightly different error. Any help would be appreciated. Thank you!

[BUG]: `npm i remix-hook-form` fails in Remix V2

Steps to reproduce

  1. Install Remix V2
npx create-remix@latest remix
  1. Attempt to install remix-hook-form
cd remix
npm i remix-hook-form

Expected behavior

remix-hook-form installs correctly

Actual behavior

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: remix@undefined
npm ERR! Found: @remix-run/[email protected]
npm ERR! node_modules/@remix-run/react
npm ERR!   @remix-run/react@"^2.0.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer @remix-run/react@"^1.15.0" from [email protected]
npm ERR! node_modules/remix-hook-form
npm ERR!   remix-hook-form@"*" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

Submit error if used with formData() method

Hello,
I have a simple input that is not part of the validation form
<input type="hidden" name="redirectTo" value={redirectTo} />

I want to get this input value in the action function along with getValidatedFormData method, but using await request.formData() together with await getValidatedFormData<FormData>(request, resolver) throw backend error

TypeError: body used already for: http://localhost:3000/login

You can replicate this issue by adding a simple input field into the form and try to get its value on submit the form in the action method using formData method.

Type error

I just stated using the lib, following this example but im having type errors.

The complete error:

Type '{ children: Element[]; handleSubmit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>; register: (name: "name" | "email" | "password", options?: RegisterOptions<...> | undefined) => { ...; }; ... 12 more ...; setFocus: UseFormSetFocus<...>; }' is not assignable to type 'RemixFormProviderProps<{ name: string; email: string; password: string; }>'.
  Types of property 'register' are incompatible.
    Type '(name: "name" | "email" | "password", options?: RegisterOptions<{ name: string; email: string; password: string; }, "name" | "email" | "password"> | undefined) => { ...; }' is not assignable to type 'UseFormRegister<{ name: string; email: string; password: string; }>'.
      Types of parameters 'options' and 'options' are incompatible.
        Type 'RegisterOptions<{ name: string; email: string; password: string; }, TFieldName> | undefined' is not assignable to type 'RegisterOptions<{ name: string; email: string; password: string; }, "name" | "email" | "password"> | undefined'.
          Type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'RegisterOptions<{ name: string; email: string; password: string; }, "name" | "email" | "password"> | undefined'.
            Type 'Partial<{ required: string | import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<boolean>; min: import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<string | number>; ... 10 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'Partial<{ required: string | import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<boolean>; min: import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<string | number>; ... 10 more ...; deps: string | string[]; }> & { ...; }'. Two different types with this name exist, but they are unrelated.
              Type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }>'.
                Types of property 'validate' are incompatible.
                  Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFi...' is not assignable to type 'Validate<string, { name: string; email: string; password: string; }> | Record<string, Validate<string, { name: string; email: string; password: string; }>> | undefined'.
                    Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFi...' is not assignable to type 'Validate<string, { name: string; email: string; password: string; }> | Record<string, Validate<string, { name: string; email: string; password: string; }>> | undefined'.
                      Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFi...' is not assignable to type 'Validate<string, { name: string; email: string; password: string; }>'.
                        Type 'string' is not assignable to type 'TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFieldName e...'

Here is a codesandbox with the issue

reset method does not accept options

React Hook Form's reset() method (returned by useForm()) allows options to be submitted to a second argument. This library omits that argument.

I came across this due to another issue: After updating values in a form and submitting them, the form does not use the new values when calculating isDirty. I found that reset() is the documented method for React Hook Form. I saw that Remix Hook Form performs a similar update (if the action returns {defaultValues: {}}, but only in the case of an error return. Perhaps Remix Hook Form could perform the reset or update the values automatically on success? Or maybe I've missed something about this library?

I plan to write a PR for the change to reset() since it's blocking my current work and seems trivial. Please let me know if this is something you would accept. Any input on the larger issue is appreciated as well.

Thanks for your work on this repo!

Type error when passing in fetcher

I'm getting a type error when attempting to pass in a fetcher to my form configuration.

Here's a minimal reproduction:

import { zodResolver } from '@hookform/resolvers/zod'
import { useFetcher } from '@remix-run/react'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'

const formSchema = z.object({
  username: z.string().min(3).max(20),
  password: z.string().min(8).max(100),
})

const resolver = zodResolver(formSchema)

export const ExampleForm = () => {
  const fetcher = useFetcher()
  const form = useRemixForm({
    resolver,
    defaultValues: {
      username: '',
      password: '',
    },
    fetcher,
  })

  return (
    <RemixFormProvider {...form}>
        ...
    </RemixFormProvider>
  )
}

And the type error I'm getting with the fetcher field in the hook is:

Type 'FetcherWithComponents<unknown>' is not assignable to type 'FetcherWithComponents<FieldValues> | undefined'.
  Type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: unknown; } & { Form: ForwardRefExoticComponent<FetcherFormProps & RefAttributes<...>>; submit: FetcherSubmitFunction; load: (href: string) => void; }' is not assignable to type 'FetcherWithComponents<FieldValues> | undefined'.
    Type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: unknown; } & { Form: ForwardRefExoticComponent<FetcherFormProps & RefAttributes<...>>; submit: FetcherSubmitFunction; load: (href: string) => void; }' is not assignable to type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: FieldValues | undefined; } & { ...; }'.
      Type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: unknown; } & { Form: ForwardRefExoticComponent<FetcherFormProps & RefAttributes<...>>; submit: FetcherSubmitFunction; load: (href: string) => void; }' is not assignable to type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: FieldValues | undefined; }'.
        Types of property 'data' are incompatible.
          Type 'unknown' is not assignable to type 'FieldValues | undefined'.ts(2322)
index.d.ts(64, 5): The expected type comes from property 'fetcher' which is declared here on type 'UseRemixFormOptions<FieldValues>'

I've tried typing my fetcher more explicitly (via useFetcher<typeof action>()) with no luck. Any ideas or suggestions would be very much appreciated!

Allow `encType: "application/json"` from `submitConfig`

Hi! ๐Ÿ‘‹

Firstly, thanks for your work on this project! ๐Ÿ™‚

Today I used patch-package to patch [email protected] for the project I'm working on.

Here is the diff that solved my problem:

diff --git a/node_modules/remix-hook-form/dist/index.js b/node_modules/remix-hook-form/dist/index.js
index 6fd94e4..cd0637a 100644
--- a/node_modules/remix-hook-form/dist/index.js
+++ b/node_modules/remix-hook-form/dist/index.js
@@ -130,8 +130,9 @@ var useRemixForm = ({
   const onSubmit = useMemo(
     () => (data2) => {
       setIsSubmittedSuccessfully(true);
-      const formData = createFormData(
-        { ...data2, ...submitData },
+      const submitPayload = { ...data2, ...submitData };
+      const formData = submitConfig?.encType === 'application/json' ? submitPayload : createFormData(
+        submitPayload,
         stringifyAllValues
       );
       submit(formData, {

I noticed that when I set the encType value to application/json in the submitConfig, the data being sent still remained as FormData.

This issue body was partially generated by patch-package.

Form State isDirty not resetting after submit

Hi,

I have a single route that is a simple instance update form. The form submits to itself and the action function then saves the changes to an API. It then returns the latest copy of the instance.

I'm using instance.updated from the API as a key into the form as this changes every time the instance is updated in the backend.

What is the best way to tell remix form that is should reload the default values so that the isDirty field goes back to false and hence the buttons go back to being disabled?

export default function InstanceView() {
    let instance: any = useLoaderData();
    
    const {
        handleSubmit,
        formState: { errors },
        register,
        control,
        reset,
    } = useRemixForm({
        mode: "onSubmit",
        resolver,
        defaultValues: {
            ...instance,
        },
        
    });

    const { isDirty} = useFormState({control});
    
    console.log({updated: instance.updated, isDirty});

    return (
        <div className="mx-4 my-4">
            <Form onSubmit={handleSubmit} key={instance.updated}>
                <CommonFormField
                    name={"name"}
                    label={"Name"}
                    description={"Name for the entry"}
                    register={register}
                    errors={errors}
                />
                <CommonFormField
                    name={"description"}
                    label={"Description"}
                    description={"User-defined descriptive text"}
                    inputType={CommonFormInputType.Textarea}
                    register={register}
                    errors={errors}
                />
                <div className={"flex"}>
                    <Button className="bg-red-500 w-[150px]" disabled={!isDirty} onClick={() => reset()}>
                        Cancel
                    </Button>
                    <Button className="ml-8 bg-green-500 w-[150px]" disabled={!isDirty}>
                        Save
                    </Button>
                </div>
            </Form>
        </div>
    )
}

Console output:

index.tsx:133 {updated: '2023-12-09T06:49:35.997Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:49:35.997Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:49:35.997Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:53:31.449Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:53:31.449Z', isDirty: true}

request: examples of file input usage

I saw that files are now supported in v3, but each time I try to send a file in my form it just comes through as the string representing the file name. Could we get a proper example added to the docs for this maybe?

Originally posted by @jrnxf in #45

Types getting inferred incorrectly with "moduleResolution: Bundler"

Hey folks. ๐Ÿ‘‹

I'm trying to setup a new Remix app (v2 already) and it seems the new tsconfig.json is conflicting with how remix-hook-form is shipping types.

Now, Remix'stsconfig.json define something like this:

{
  // ... rest
  "compilerOptions": {
    // ... rest
    // NEW REMIX SETUP
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "target": "ES2022"
  }
}

And because of the module resolution got changed, I started getting 2 errors and I think they are related.

Here's what happens when I run pnpm tsc:

$ pnpm typecheck

> remix-hook-form-error@ typecheck /Users/raulmelo/development/personal/error-repos/remix-hook-form-error
> tsc

app/routes/_index.tsx:37:5 - error TS2345: Argument of type '{ mode: string; resolver: <TFieldValues extends FieldValues, TContext>(values: TFieldValues, context: TContext | undefined, options: ResolverOptions<TFieldValues>) => Promise<...>; }' is not assignable to parameter of type 'UseRemixFormOptions<FieldValues$1>'.
  Object literal may only specify known properties, and 'mode' does not exist in type 'UseRemixFormOptions<FieldValues$1>'.

37     mode: "onSubmit",
       ~~~~

node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/remix-hook-form/dist/index.d.ts:5:98 - error TS2307: Cannot find module 'react-hook-form/dist/types' or its corresponding type declarations.

5 import { FieldValues as FieldValues$1, UseFormProps, Path, RegisterOptions, UseFormReturn } from 'react-hook-form/dist/types';
                                                                                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Found 2 errors in 2 files.

Errors  Files
     1  app/routes/_index.tsx:37
     1  node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/remix-hook-form/dist/index.d.ts:5

After some debuging, I noticed that's because remix-hook-form is importing the types from a path that "does not exists in the export" (which seems to be relevant when we have "bundler" mode set):

image

Fix I change it by removing the submodule:

-import { FieldValues as FieldValues$1, UseFormProps, Path, RegisterOptions, UseFormReturn } from 'react-hook-form/dist/types';
+import { FieldValues as FieldValues$1, UseFormProps, Path, RegisterOptions, UseFormReturn } from 'react-hook-form';

Everything seems to be working again:

CleanShot 2023-09-26 at 13 23 12

Not sure about the backwards compatibility of it though. I think some tests would be necessary.

Reproduction

I've created a repository to help you to debug this problem: https://github.com/devraul/remix-v2-remix-hook-form-type-error

Or, you can simply:

  1. create a brand-new remix project;
  2. install zod, remix-hook-form, etc.;
  3. setup the initial example you have in the README;
  4. running dev and build, everything will just work;
  5. run pnpm typecheck to see the error

Tips

Also, there's 2 friendly suggestions regarding the type def:

  1. For consistency, might be a good idea to define the types field in the export:
    "exports": {
        ".": {
          "import": "./dist/index.js",
          "require": "./dist/index.cjs",
    +     "types": "./dist/index.d.ts"
        }
      },
  2. despite of that "typings" is "types" field equivalent, might be good to stick the one TS suggests at the docs snippet (types), just for consistency: https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#including-declarations-in-your-npm-package

Let me know if you need something else.

Cheers!

TypeScript: Return type of useRemixForm hook is incorrect.

Hi,
Thanks for building this package!
I'm trying to migrate my app from remix-validated-form to this one but I've run into some issue regarding TS types.

Loos like useRemixForm is incorrectly returning form types.

Here's the type returned from react-hook-form:
CleanShot 2024-02-07 at 15 05 53

...and here's remix-hook-form:
CleanShot 2024-02-07 at 15 05 44

You can see the values are incorrectly typed as undefined. According to the zod schema, those fields should never be undefined just like in the first example.

const schema = z.object({
  name: z.string().trim().min(3).max(32),
  slug: z.string().trim().min(3).max(32),
})

Have to set stringifyAllValues: false else all strings are double quoted

Because of this line in createFormData(data: T, stringifyAll = true) in ./utilities/index.ts

 if (stringifyAll) {
        formData.append(key, JSON.stringify(value)); // <---- This double quotes everything
      } else {
        formData.append(
          key,
          typeof value === "string" ? value : JSON.stringify(value),
        );
      }

So when accessing FormData in backend with formData.get('email') the result is "/"[email protected]/"" with double quoted string. What can be the workaround for this?

Currently I am setting stringifyAllValues: false and coercing all to strings in my zod schema.

Cannot clear custom errors when returned by the server

Hello, thanks for the library!
Got a small issue

When i'm returning a custom error from the action as following:

export async function action({request, context, params}: ActionArgs) {

    const {
        data,
        errors,
        receivedValues ,
      } = await getValidatedFormData(request, zodResolver(createSpaceInput))

      try {
        const result = context.api.onboarding.setUpSpace.mutate({
            name: data.name
          })
      
        return json({ space: result })
      } catch(e) {
        return json({ 
            errors: {
                root: {
                    message: e.message
                }
            } 
        })
      }
  }

The error can be displayed just fine.
However, when i try to clear the error for example onChange it simply doesn't clear.

 const form = useRemixForm({
    mode: "onSubmit",
    defaultValues: {
      name: "",
    },
    resolver: zodResolver(formSchema),
  });

<RemixForm onSubmit={form.handleSubmit} method={"POST"} onChange={() => {
              console.log('form on change', form.formState.errors)
              form.clearErrors('root')

            }}>

I have also noticed that when I return an error which has the same name as one of the fields:

export async function action({request, context, params}: ActionArgs) {
     ... // rest of the function

        return json({ 
            errors: {
                name: {
                    message: 'some backend error coming from external api'
                }
            } 
        })
  }

It behaves the same way, as in the message is shown just fine, but it can't be cleared up

Any tips?

`formState.isSubmitSuccessful` does not behave like react-hook-form

Hey

I noticed that formState.isSubmitSuccessful always remains false

Likely #22 but very different..

Actual behaviour

isSubmitSuccessful is always false even after a successful submission

Expected behaviour

Upon submitting the form and it completing successfully isSubmitSuccessful remains true until a subsequent reset is called (like react-hook-form) (https://www.react-hook-form.com/api/useformstate/ and https://www.react-hook-form.com/api/useform/reset/)

Possible solution

I'm unsure how this would work given the action response is handled by the developer rather than this library. But thought i'd post the issue and see if you had any ideas!

Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode'.

Here is another, again just copied from the README sample in a TypeScript file.

{errors.name && <p>{errors.name.message}</p>} gives two errors:

Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode'.   Type 'FieldError' is not assignable to type 'ReactNode'.

ะขype 'FieldError' is missing the following properties from type 'ReactPortal': key, children, props

{errors.email && <p>{errors.email.message}</p>} gives error:

Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode'.

submitData string issue

Since 3.2.0 all of my forms that has submitData passes strings with quotes.
Example:

  const {
    handleSubmit,
    formState: { errors },
    register,
    setValue,
  } = useRemixForm({
    mode: "onSubmit",
    defaultValues: {
      title: currentCourse.title ?? "",
      desc: currentCourse.desc ?? "",
    },
    submitData: {
      submitAction: "SAVE",
    },
    resolver,
  });
image

Downgrading to version 3.1.0 make it work as it should:
image

Form Reset On Success

Once the submission is successful, I have a form that stays on the same page once the action is complete (account page in settings).

I need the form to reset (i.e. isDirty) to go back to false.

Is there a way to do this without using a useEffect? Totally happy to contribute to the docs

`formState.isSubmitting` does not behave as we could expect

Hi,

I noticed that formState.isSubmitting is not working as expected. I tried to setup this library with Remix, implementing some slow action.

Actual behaviour

isSubmitting is set to false as soon as the POST network call is fired, before it returns.

Expected behaviour

isSubmitting should stay true until action POST terminates.

Reasons

In react-hook-form, formState.isSubmitting is true as long as onSubmit promise is not either rejected or resolved, onSubmit being the argument of handleSubmit.

In remix-hook-form, users don't provide any onSubmit function as it's using the native implementation, triggering a Remix action.

In this repo, you're using Remix's const submit = useSubmit() hook to submit the form. But it does not seem to me that there is any way to listen to termination of that submit event (e.g. wait for the end of the POST request).

Possible solution?

Actually, there is a debate here about returning a promise for useSubmit() hook in react-router. They also give a workaround that could work for us : to use useActionData() hook to know when the remix action is complete.

I guess we could do that to properly set formState.isSubmitting, submitted, ... etc.

let actionData = useActionData();
useEffect(() => {
    if (actionData.ok) {
       complete(actionData)
    }
}, [actionData]);

Could not parse content as FormData

i'm trying to make a form with file upload but i can't make it work using remix hook form, i am getting an error "Could not parse content as FormData".

<Form onSubmit={handleSubmit} method="POST" encType="multipart/form-data">
      <div>
        <Label htmlFor="title">
          Title:
          <Input type="text" {...register("title")} />
          {errors.title && errors.title.message && (
            <ErrorMessage message={errors.title.message} />
          )}
        </Label>
      </div>
      <div>
        <Label htmlFor="content">
          Content:
          <Input type="text" {...register("content")} />
          {errors.content && errors.content.message && (
            <ErrorMessage message={errors.content.message} />
          )}
        </Label>
      </div>
      <div>
        <Label htmlFor="file-upload">
          Choose file:
          <Input
            type="file"
            id="file-upload"
            accept="image/*"
            {...register("image")}
          />
          {errors.image && errors.image.message && (
            <ErrorMessage message={errors.image.message} />
          )}
        </Label>
      </div>

      <Button variant={"default"} className="mt-4 w-full">
        Add
      </Button>
    </Form>
export const action = async ({ request, params }: ActionFunctionArgs) => {
  const imageFile = await unstable_parseMultipartFormData(
    request,
    fileUploadHandler()
  );

  console.log("imageFile", imageFile);

  let { data, errors, receivedValues } = await getValidatedFormData<FormData>(
    request,
    resolver
  );

  console.log("receivedValues", receivedValues);
  console.log("data", data);
const schema = z.object({
  title: z.string().min(2).max(300),
  content: z.string().min(1).max(200),
  image: z.any().refine((files) => files?.length == 1, "Image is required."),
});

Missing Controller, type ControllerProps, type FieldPath, type FieldValues,

I am trying to migrate from react-hook-form as it was used with https://shadow-panda.dev/, and I am missing a few elements to piece it together. This is what I have so far.

import { Label } from '@/components/ui/label';
import { css, cx } from '@/styled-system/css';
import { styled } from '@/styled-system/jsx';
import {
  formControl,
  formDescription,
  formItem,
  formLabel,
  formMessage,
} from '@/styled-system/recipes';
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import { createContext, forwardRef, useContext, useId, useMemo } from 'react';
import {
  Controller,
  type ControllerProps,
  type FieldPath,
  type FieldValues,
  useRemixFormContext,
} from 'remix-hook-form';

type FormFieldContextValue<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName;
};

type FormItemContextValue = {
  id: string;
};

const FormFieldContext = createContext<FormFieldContextValue>(
  {} as FormFieldContextValue,
);
const FormItemContext = createContext<FormItemContextValue>(
  {} as FormItemContextValue,
);

export const useFormField = () => {
  const fieldContext = useContext(FormFieldContext);
  const itemContext = useContext(FormItemContext);
  const { formState, getFieldState } = useRemixFormContext();

  const fieldState = getFieldState(fieldContext.name, formState);

  if (!fieldContext) {
    throw new Error('useFormField should be used within <FormField>');
  }

  const { id } = itemContext;

  return {
    formDescriptionId: `${id}-form-item-description`,
    formItemId: `${id}-form-item`,
    formMessageId: `${id}-form-item-message`,
    id,
    name: fieldContext.name,
    ...fieldState,
  };
};

const BaseFormField = <
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  ...props
}: ControllerProps<TFieldValues, TName>) => {
  const context = useMemo(() => {
    return {
      name: props.name,
    };
  }, [props.name]);

  return (
    <FormFieldContext.Provider value={context}>
      <Controller {...props} />
    </FormFieldContext.Provider>
  );
};

const BaseFormItem = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>((props, ref) => {
  const id = useId();

  const context = useMemo(() => {
    return {
      id,
    };
  }, [id]);

  return (
    <FormItemContext.Provider value={context}>
      <div
        ref={ref}
        {...props}
      />
    </FormItemContext.Provider>
  );
});
BaseFormItem.displayName = 'FormItem';

const BaseFormLabel = forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof Label>
>(({ className, ...props }, ref) => {
  const { error, formItemId } = useFormField();

  return (
    <Label
      className={cx(error && css({ color: 'destructive' }), className)}
      htmlFor={formItemId}
      ref={ref}
      {...props}
    />
  );
});
BaseFormLabel.displayName = 'FormLabel';

const BaseFormControl = forwardRef<
  React.ElementRef<typeof Slot>,
  React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
  const { error, formDescriptionId, formItemId, formMessageId } =
    useFormField();

  return (
    <Slot
      aria-describedby={
        error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`
      }
      aria-invalid={Boolean(error)}
      id={formItemId}
      ref={ref}
      {...props}
    />
  );
});
BaseFormControl.displayName = 'FormControl';

const BaseFormDescription = forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>((props, ref) => {
  const { formDescriptionId } = useFormField();

  return (
    <p
      id={formDescriptionId}
      ref={ref}
      {...props}
    />
  );
});
BaseFormDescription.displayName = 'FormDescription';

const BaseFormMessage = forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ children, ...props }, ref) => {
  const { error, formMessageId } = useFormField();
  const body = error ? String(error?.message) : children;

  if (!body) {
    return null;
  }

  return (
    <p
      id={formMessageId}
      ref={ref}
      {...props}
    >
      {body}
    </p>
  );
});
BaseFormMessage.displayName = 'FormMessage';

export const FormField = BaseFormField;
export const FormLabel = styled(BaseFormLabel, formLabel);
export const FormItem = styled(BaseFormItem, formItem);
export const FormControl = styled(BaseFormControl, formControl);
export const FormDescription = styled(BaseFormDescription, formDescription);
export const FormMessage = styled(BaseFormMessage, formMessage);

export { Form } from '@remix-run/react';

Form always assumes the response from the action is an error.

Hi, we're using this in a fairly simple way, I think. Our action looks something like this:

export async function DoThingAction({ request }: ActionArgs) {
  const { errors, data } = await getValidatedFormData<DoThingForm>(
    request,
    zodResolver(DoThingForm)
  );
  if (errors) {
    return badRequest(
        result: 'error',
        error: errors.toString(),
      });
  }

  return json(await doThing(data));
}

The return type of doThing looks something like this:

export type doThingResponse = { result: 'success', someValue: string } | { result: 'error', error: string };

I don't think we're doing anything particularly unusual here but it seems that the errors property of formState in the component is always being populated with the returned value of the action, so it always looks like there are errors.

Is there something unusual about how we've set up our action? (We don't want to have a separate success state component because it's feasible that folks will want to submit this form multiple times with more than one set of values)

Can we use this library with react-router as well?

Sorry to open this as an issue, wanted to ask this in a discussion but couldn't find the tab.
I'm trying to use react-router and react-hook-form together and I was looking for how to do it and I ended up here.

Since Remix is built on react-router is it possible to use it there as well?

(bug): Remix hook form is not behaving as expected or documented

I've encountered a problem with remix-hook-form where form submissions are not behaving as expected or as I believe is documented.
My apologies in advance if this issue stems from a misunderstanding on my part. However, after several attempts and reviews of my code, I'm inclined to believe there might be an underlying issue with how remix-hook-form handles form submissions, specifically when trying to use the method="post" and action attributes without explicitly using a useFetcher.

Overview

  • Expected Behavior: When setting up a form using remix-hook-form with method="post" and an action pointing to a route, I expected the form submission to be directed to the specified route without causing a full page reload.
  • Actual Behavior: Instead of submitting to the specified route as expected, the form submission results in a full page reload along with it completely ignoring the url location given in the action prop. This behavior suggests that the form props are not being read and submission is not using the underlying fetcher as the docs state.
  • Workaround: The only way I've managed to get the form to submit to the correct route without causing a page reload is by explicitly using useFetcher and specifying a submitConfig in the form setup. This approach seems counterintuitive if the library is supposed to handle submissions more seamlessly by default.

Issue reproduction

I've made a repository that demonstrates the problem. You can find it here - https://stackblitz.com/edit/remix-run-remix-xr6vlj?file=app%2Froutes%2F_index.tsx

Key Points:

  • Two Forms: The demo includes two forms. One (SubscribeFormThatDoesNotWork) doesn't use useFetcher and fails to submit properly, causing a page reload. The other (SubscribeFormThatDoesWork) uses useFetcher and works fine, submitting without reloading the page.
  • Submission State: Another thing I noticed is that the form.formState.isSubmitSuccessful value that comes back from remix-hook-form is completely wrong. I've setup the API endpoint to respond with a 400, and yet it doesn't reflect that at all. Also the value is set as soon as the request initiates, it doesn't wait for success

Conclusion

Again, I'm very sorry if I'm being dumb and missing something obvious, but I can't for the life of me figure out what is going wrong.
Looking forward to finding out more ๐Ÿ™

`formState.isSubmitting` is `true` when the loaders are refetching

According to this line of the hook, the isSubmitting state is true when the navigation state (or a fetcher) is not "idle".

This includes a moment after the response from action is received but the loaders are revalidating.

I believe the check should rather be === "submitting", as per Remix docs:

submitting - A route action is being called due to a form submission using POST, PUT, PATCH, or DELETE

This would make sure that formState.isSubmitting is actually reflecting the submitting state, and not loading.

What do you think?

Using react-hook-form Controller with file type input

Great job with this awesome library! Unfortunately, I haven't been able to get it to work with a FileInput. I'm using Controller from react-hook-form and useRemixForm to handle the rest. The problem I'm facing is that whenever I submit the form, the value of the "avatar" field is always an empty object. Interestingly, on the client side, I've set up a watcher for the "avatar" field, and it is not empty. Do you have any ideas on what might be causing this issue?

Relevant code:

<Controller
    control={control} 
    name="avatar"
    render={({ field: { onChange } }) => (
      <FileInput
        w="100%"
        size="md"
        icon={<IconPhotoUp />}
        label="Your Photo"
        placeholder="Upload your profile photo"
        onChange={onChange}
        accept="image/png,image/jpeg,image/jpg,image/webp"
      />
    )}
  />

This is my squema

const schema = zod.object({
  avatar: zod.any() as zod.ZodType<File>,
  firstName: zod.string().optional(),
  lastName: zod.string().optional(),
  email: zod.string().email({ message: "Invalid email address" }),
  role: zod.string().optional(),
  country: zod.string().optional(),
  timezone: zod.string().optional(),
  username: zod.string().optional()
});
type FormData = zod.infer<typeof schema>;
const resolver = zodResolver(schema);


//on action 

const { errors: formErrors, data: formData, receivedValues } = await getValidatedFormData<FormData>(
    request,
    resolver
);

Form states

Hi there,
A bit new to this library and react-hook-form.
How can I use the form states to show a loading and also a message when form is submitted successfully?
I saw all of formState available properties, but didn't manage to put it to work.
Thanks!!

validateFormData broke

const FormDataZodSchema = z.object({
  action: z.string(),
  outline: z.string().min(2, {
    message: 'Outline must be at least 2 characters.',
  }),
});

type FormData = z.infer<typeof FormDataZodSchema>;

const resolver = zodResolver(FormDataZodSchema);
export const action: ActionFunction = async ({ params, request }) => {
  const articleId = Number(params.id);

  if (!articleId) {
    throw new Error('Article ID is required.');
  }

  const formData = await request.formData();

  if (formData.get('action') === 'generate') {
    return json(await replaceOutline(articleId));
  }

  if (formData.get('action') === 'accept') {
    console.log('formData', Object.fromEntries(formData));

    const { data, errors } = await validateFormData<FormData>(
      formData,
      resolver,
    );

    console.log('errors', errors);

    if (errors) {
      return json(errors);
    }

    return await acceptOutline(articleId, data.outline);
  }

  throw new Error('Invalid action.');
};
formData {
  action: 'accept',
  outline: '* Introduction\n' +
    '  * Overview of Threats Affecting Polar Bears\n' +
    '  * Significance of the Circumpolar Action Plan\n' +
    '  * The Compound Effects of these Threats\n' +
    '* Climate Change As A Major Threat\n' +
    "  * Effects of Climate Change on Polar Bear's Habitat\n" +
    '  * Relation of Climate Change with Other Threats\n' +
    '* Human-Caused Mortality\n' +
    '  * Causes of Human-Induced Polar Bear Deaths\n' +
    '  * Connection with Other Threats and Climate Change\n' +
    '* Industrial Threats: Mineral and Energy Resource Exploration and Development\n' +
    "  * Impacts of Industrial Activities On Polar Bears' Habitats\n" +
    '  * Combination with Climate Change and Other Threats\n' +
    '* Contaminants and Pollution\n' +
    '  * effect of Pollution on Polar Bears\n' +
    '  * Connection with Climate Change and Other Threats\n' +
    '* Shipping and Tourism\n' +
    '  * Impacts of Shipping and Tourism on Polar Bear Habitat\n' +
    '  * Connection with Climate Change and Other Threats\n' +
    '* Diseases and Parasites\n' +
    '  * Effects of Diseases and Parasites on Polar Bears\n' +
    '  * connection with Climate Change and Other Threats\n' +
    '* Conclusion\n' +
    '  * Summary of Challenges and their Impacts\n' +
    '  * Importance of Protecting Polar Bears for Biodiversity\n'
}
errors {
  action: { message: 'Required', type: 'invalid_type', ref: undefined },
  outline: { message: 'Required', type: 'invalid_type', ref: undefined }
}

I believe I am providing the expected inputs, but validator is still producing an error.

'mode' does not exist in type 'UseRemixFormOptions'

Tried implementing the first example in the readme, but there is a type error:

Screenshot 2024-01-15 at 20 07 58

Object literal may only specify known properties, and 'mode' does not exist in type 'UseRemixFormOptions<{ name: string; email: string; }>'.ts(2353)

    "remix-hook-form": "^4.0.0",
    "zod": "^3.22.4"
    "@hookform/resolvers": "^3.3.4",

All deps:

"dependencies": {
    "@hookform/resolvers": "^3.3.4",
    "@remix-run/css-bundle": "^2.5.0",
    "@remix-run/node": "^2.5.0",
    "@remix-run/react": "^2.5.0",
    "@remix-run/serve": "^2.5.0",
    "drizzle-orm": "^0.29.3",
    "isbot": "^4.1.0",
    "pg": "^8.11.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "remix-hook-form": "^4.0.0",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@playwright/test": "^1.40.1",
    "@remix-run/dev": "^2.5.0",
    "@types/node": "^20.11.0",
    "@types/pg": "^8.10.9",
    "@types/react": "^18.2.20",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.7.4",
    "daisyui": "^4.6.0",
    "drizzle-kit": "^0.20.12",
    "eslint": "^8.38.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.28.1",
    "eslint-plugin-jsx-a11y": "^6.7.1",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "tailwindcss": "^3.4.1",
    "typescript": "^5.1.6"
  },

Files/Blobs are not getting returned in Action

getValidatedFormData is not returning the file data.

{
  firstName: 'Jack',
  lastName: 'Sparrow',
  screenName: 'sparrow1',
  profileImage: { '0': {} } // No blob/file data
}

Issue is here: https://github.com/Centerworx/remix-hook-form/blob/d082e1d0e7929451505db9ac0519952860d56bc4/src/utilities/index.ts#L49

FormData Blob/FIles are not handled. JSON cannot hold a file.

formData.set(pathString, blob) or  formData.set(pathString, blob, filenameString)

you can use react-hook-form set function to do this.

import { set } from "react-hook-form";

set(
  object: FieldValues, // in your case currentObject
  path: string, // formData key
  value?: unknown, // formData blob 
)

With this set function you can eliminate all the string parsing and conversion you are doing in "generateFormData" it would just become the below:

/**
 * Generates an output object from the given form data, where the keys in the output object retain
 * the structure of the keys in the form data. Keys containing integer indexes are treated as arrays.
 *
 * @param {FormData} formData - The form data to generate an output object from.
 * @param {boolean} [preserveStringified=false] - Whether to preserve stringified values or try to convert them
 * @returns {Object} The output object generated from the form data.
 */
export const generateFormData = (
  formData: FormData,
  preserveStringified = false,
) => {
  // Initialize an empty output object.
  const outputObject: Record<any, any> = {};

  // Iterate through each key-value pair in the form data.
  for (const [key, value] of formData.entries()) {
    if (value instanceof Blob || toString.call(value) === "[object Blob]") {
        set(currentObject, key, value)
        continue;
    }

    // Try to convert data to the original type, otherwise return the original value
    const data = preserveStringified ? value : tryParseJSON(value.toString());

    set(currentObject, key, data)
  }

  // Return the output object.
  return outputObject;
};

The only issue with using react-hook-form set function is it will not use empty [] in keys urlSearchParams or dotKey syntax. You can use my "cleanArrayStringKeys" function to allow for empty [] or remove those test to conform to react-hook-form standards, you could even throw an error if there is an empty [].

/**
 * cleanArrayStringUrl - This is a Fix for react-hook-form url empty [] brackets set conversion,
 * react-hook-form will not load array values that use the empty [] keys.
 *
 * This utility will add number keys to empty [].
 *
 * Note: empty url arrays and no js forms don't seam to be very popular either.
 *
 * @param {string} urlString
 * @returns {string}
 */
export const cleanArrayStringKeys = (urlString: URLSearchParams) => {
  const cleanedFormData = new URLSearchParams();
  const counter = {} as Record<string, number>;

  for (const [path, value] of urlString.entries()) {
    const keys = path.split(/([\D]$\[\])/g);
    // early return if single key
    if (keys.length === 1 && !/\[\]/g.test(keys[0])) {
      cleanedFormData.set(keys.join(""), value);
      continue;
    }

    const cleanedKey = [] as string[];

    // adds indexes to empty arrays to match react-hook-form
    keys.forEach((key) => {
      key = key.replace(/\.\[/g, "[");
      counter[key] = (counter[key] === undefined ? -1 : counter[key]) + 1;

      cleanedKey.push(key.replace(/\[\]/g, () => `[${counter[key]}]`));

      // merge array keys back into path
      const cleanedPath = cleanedKey.join("");

      cleanedFormData.set(cleanedPath, value);
    });
  }

If you don't want to simplify your code, you can just add this to "tryParseJSON", this should do the trick, but should be tested:

 tryParseJSON= (jsonString) => {
  if (jsonString === "null") {
    return null;
  }
  if (jsonString === "undefined") {
    return void 0;
  }
  if (jsonString instanceof File || jsonString instanceof Blob) { // new check before JSON.parse 
    return jsonString;
  }

on a side note: value instanceof Blob -> Some node servers (<16, It can be imported in 16+) may not have access to Blob and so it could break some projects. See ref: https://stackoverflow.com/questions/14653349/node-js-cant-create-blobs#:~:text=41-,Since%20Node.js%2016,-%2C%20Blob%20can

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.