Skip to content

Commit

Permalink
Initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
AlemTuzlak committed Apr 10, 2023
1 parent d0b9e0f commit bf75c5e
Show file tree
Hide file tree
Showing 8 changed files with 626 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ dist

# TernJS port file
.tern-port

/build
134 changes: 133 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,134 @@
# remix-hook-form
Open source wrapper for react-hook-form aimed at Remix.run

remix-hook-form is a lightweight wrapper around [remix-hook-form](https://react-hook-form.com/) that makes it easier to use in your [Remix](https://remix.run) applications. It provides a set of hooks and utilities that simplify the process of working with forms and form data, while leveraging the power and flexibility of remix-hook-form.

## Installation

You can install the latest version of remix-hook-form using [npm](https://www.npmjs.com/):

`npm install remix-hook-form`

Or, if you prefer [yarn](https://yarnpkg.com/):

`yarn add remix-hook-form`

## Usage

Here is an example usage of remix-hook-form:

```jsx
import { useRemixForm, getValidatedFormData } from "remix-hook-form";
import { Form } from "@remix-run/react";
import { zodResolver } from "@hookform/resolvers/zod";
import * as zod from "zod";
import { ActionArgs, json } from "@remix-run/server-runtime";

const schema = zod.object({
name: zod.string().nonempty(),
email: zod.string().email().nonempty(),
});

type FormData = zod.infer<typeof schema>;

const resolver = zodResolver(schema);

export const action = ({ request }: ActionArgs) => {
const { errors, data } =
getValidatedFormData < FormData > (request, resolver);
if (errors) {
return json(errors);
}
// Do something with the data
return json(data);
};

export default function MyForm() {
const {
handleSubmit,
formState: { errors },
register,
} = useRemixForm({
mode: "onSubmit",
defaultValues: {
name: "",
email: "",
},
resolver,
});

return (
<Form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</label>
<label>
Email:
<input type="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</label>
<button type="submit">Submit</button>
</Form>
);
}
```

## Utilities

### getValidatedFormData

getValidatedFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the request object and the resolver function. It returns an object with two properties: `errors` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`.

### validateFormData

validateFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the request object and the resolver function. It returns an object with two properties: `errors` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`.

The difference between `validateFormData` and `getValidatedFormData` is that `validateFormData` only validates the data while the `getValidatedFormData` function also extracts the data automatically from the request object assuming you were using the default setup

### createFormData

createFormData is a utility function that can be used to create a FormData object from the data returned by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `data` from the `handleSubmit` function and the second one is the key that the data will be stored in the FormData object. (default is `formData`). It returns a FormData object.

### parseFormData

parseFormData is a utility function that can be used to parse the data submitted to the action by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `request` submitted from the frontend and the second one is the key that the data will be stored in the FormData object. (default is `formData`). It returns an object that contains unvalidated `data` submitted from the frontend.

## Hooks

### useRemixForm

useRemixForm is a hook that can be used to create a form in your Remix application. It takes all the same properties as `react-hook-form`'s `useForm` hook, with the addition of 3 properties:
`submitHandlers` - an object containing two properties, `onValid` which can be passed into the function to override the default behavior of the handleSubmit success case provided by the hook, and `onInvalid` which can be passed into the function to override the default behavior of the handleSubmit error case provided by the hook.
`submitConfig` - allows you to pass additional configuration to the `useSubmit` function from remix such as `{ replace: true }` to replace the current history entry instead of pushing a new one.,
`submitData` - allows you to pass additional data to the backend when the form is submitted

The hook acts almost identically to the `react-hook-form` hook, with the exception of the `handleSubmit` function, and the `formState.errors`.

The `handleSubmit` function uses two thing under the hood to allow you easier usage in Remix, those two things are:

- The success case is provided by default where when the form is validated by the provided resolver, and it has no errors, it will automatically submit the form to the current route using a POST request. The data will be sent as `formData` to the action function.
- The data that is sent is automatically wrapped into a formData object and passed to the server ready to be used. Easiest way to consume it is by using the `parseFormData` or `getValidatedFormData` function from the `remix-hook-form` package.

The `formState.errors` object is automatically populated with the errors returned by the server. If the server returns an object with the same shape as the `errors` object returned by `useRemixForm`, it will automatically populate the `formState.errors` object with the errors returned by the server.
This is achieved by using `useActionData` from `@remix-run/react` to get the data returned by the action function. If the data returned by the action function is an object with the same shape as the `errors` object returned by `useRemixForm`, it will automatically populate the `formState.errors` object with the errors returned by the server. To assure this is done properly it is recommended that you use `getValidatedFormData` and then return the errors object from the action function as `json(errors)`.

### useRemixFormContext

Exactly the same as `useFormContext` from `react-hook-form` but it also returns the changed `formState.errors` and `handleSubmit` object.

## RemixFormProvider

Identical to the `FormProvider` from `react-hook-form`, but it also returns the changed `formState.errors` and `handleSubmit` object.

## License

MIT

## Bugs

If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/Code-Forge-Net/remix-hook-form/issues)

## Contributing

We welcome contributions from the community!
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "remix-hook-form",
"version": "1.0.0",
"description": "Utility wrapper around react-hook-form",
"main": "build/index.js",
"scripts": {
"prepare": "tsc",
"prepublish": "npm run prepare",
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Code-Forge-Net/remix-hook-form.git"
},
"keywords": [
"React",
"Remix",
"Remix.run",
"react-hook-form",
"hooks",
"forms"
],
"author": "Alem Tuzlak",
"license": "MIT",
"bugs": {
"url": "https://github.com/Code-Forge-Net/remix-hook-form/issues"
},
"homepage": "https://github.com/Code-Forge-Net/remix-hook-form#readme",
"peerDependencies": {
"@remix-run/react": "^1.15.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9"
},
"devDependencies": {
"@hookform/resolvers": "^3.0.1",
"@types/react": "^18.0.34",
"typescript": "^5.0.4"
}
}
106 changes: 106 additions & 0 deletions src/hook/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from "react";
import { SubmitFunction, useActionData, useSubmit } from "@remix-run/react";
import {
SubmitErrorHandler,
SubmitHandler,
useFormContext,
} from "react-hook-form";
import { useForm, FormProvider } from "react-hook-form";
import type {
FieldValues,
UseFormHandleSubmit,
UseFormProps,
UseFormReturn,
} from "react-hook-form/dist/types";
import { createFormData, mergeErrors } from "../utilities";

export type SubmitFunctionOptions = Parameters<SubmitFunction>[1];
interface UseRemixFormOptions<T extends FieldValues> extends UseFormProps<T> {
submitHandlers?: {
onValid?: SubmitHandler<T>;
onInvalid?: SubmitErrorHandler<T>;
};
submitConfig?: SubmitFunctionOptions;
submitData?: FieldValues;
}

export const useRemixForm = <T extends FieldValues>({
submitHandlers,
submitConfig,
submitData,
...formProps
}: UseRemixFormOptions<T>) => {
const submit = useSubmit();
const data = useActionData();
const methods = useForm<T>(formProps);

// Submits the data to the server when form is valid
const onSubmit = (data: T) => {
submit(createFormData({ ...data, ...submitData }), {
method: "post",
...submitConfig,
});
};

const onInvalid = () => {};

const formState = methods.formState;

const {
dirtyFields,
isDirty,
isSubmitSuccessful,
isSubmitted,
isSubmitting,
isValid,
isValidating,
touchedFields,
submitCount,
errors,
isLoading,
} = formState;

const formErrors = mergeErrors<T>(errors, data);

return {
...methods,
handleSubmit: methods.handleSubmit(
submitHandlers?.onValid ?? onSubmit,
submitHandlers?.onInvalid ?? onInvalid
),
formState: {
dirtyFields,
isDirty,
isSubmitSuccessful,
isSubmitted,
isSubmitting,
isValid,
isValidating,
touchedFields,
submitCount,
isLoading,
errors: formErrors,
},
};
};
interface RemixFormProviderProps<T extends FieldValues>
extends Omit<UseFormReturn<T>, "handleSubmit"> {
children: React.ReactNode;
handleSubmit: any;
}
export const RemixFormProvider = <T extends FieldValues>({
children,
...props
}: RemixFormProviderProps<T>) => {
return <FormProvider {...props}>{children}</FormProvider>;
};

export const useRemixFormContext = <T extends FieldValues>() => {
const methods = useFormContext<T>();
return {
...methods,
handleSubmit: methods.handleSubmit as any as ReturnType<
UseFormHandleSubmit<T>
>,
};
};
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./utilities";
export * from "./hook";
export * from "./utilities";
Loading

0 comments on commit bf75c5e

Please sign in to comment.