Skip to content

Commit

Permalink
Added full no-js support
Browse files Browse the repository at this point in the history
  • Loading branch information
AlemTuzlak committed May 13, 2023
1 parent b754895 commit 126668e
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 1,424 deletions.
87 changes: 84 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Remix-hook-form is a powerful and lightweight wrapper around [react-hook-form](h

And the best part? Remix-hook-form has zero dependencies, making it easy to integrate into your existing projects and workflows. Say goodbye to bloated dependencies and hello to a cleaner, more efficient development process with Remix-hook-form.

Oh, and did we mention that this is fully Progressively enhanced? That's right, you can use this with or without javascript!

## Installation

Expand All @@ -24,7 +25,7 @@ Or, if you prefer [yarn](https://yarnpkg.com/):

`yarn add remix-hook-form`

## Usage
## Basic usage

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

Expand Down Expand Up @@ -86,6 +87,68 @@ export default function MyForm() {
}
```

## Usage with NO js

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 = async ({ request }: ActionArgs) => {
const { errors, data, receivedValues: defaultValues } =
await getValidatedFormData<FormData>(request, resolver);
if (errors) {
return json({ errors, defaultValues });
}
// 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
Expand All @@ -96,8 +159,11 @@ If you made a GET request instead of a POST request and you are using this insid

If the form is submitted without js it will try to parse the formData object and covert it to the same format as the data object returned by `useRemixForm`. If the form is submitted with js it will automatically extract the data from the request object and validate it.

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`.
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 three properties: `errors`, `receivedValues` 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 `receivedValues` property allows you to set the default values of your form to the values that were received from the request object. This is useful if you want to display the form again with the values that were submitted by the user when there is no JS present

### Example with errors only
```jsx
/** all the same code from above */

Expand All @@ -110,6 +176,21 @@ export const action = async ({ request }: ActionArgs) => {
}
// Do something with the data
};
```

### Example with errors and receivedValues
```jsx
/** all the same code from above */

export const action = async ({ request }: ActionArgs) => {
// Takes the request from the frontend, parses and validates it and returns the data
const { errors, data, receivedValues } =
await getValidatedFormData<FormData>(request, resolver);
if (errors) {
return json({ errors, receivedValues });
}
// Do something with the data
};

```

Expand Down Expand Up @@ -205,13 +286,13 @@ The hook acts almost identically to the `react-hook-form` hook, with the excepti

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.

The `register` function returned also has super powers that allows it to set the default value of the input returned from 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 ensure this is done properly, it is recommended that you use `getValidatedFormData` and then return the errors object from the action function as `json(errors)`.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "remix-hook-form",
"version": "1.0.8",
"version": "1.1.0",
"description": "Utility wrapper around react-hook-form for use with Remix.run",
"type": "module",
"main": "./dist/index.umd.cjs",
Expand Down
8 changes: 7 additions & 1 deletion src/hook/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
import { useForm, FormProvider } from "react-hook-form";
import type {
FieldValues,
Path,
RegisterOptions,
UseFormHandleSubmit,
UseFormProps,
UseFormReturn,
Expand Down Expand Up @@ -60,14 +62,18 @@ export const useRemixForm = <T extends FieldValues>({
isLoading,
} = formState;

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

return {
...methods,
handleSubmit: methods.handleSubmit(
submitHandlers?.onValid ?? onSubmit,
submitHandlers?.onInvalid ?? onInvalid
),
register: (name: Path<T>, options: RegisterOptions<T>) => ({
...methods.register(name, options),
defaultValue: data?.defaultValues?.[name] ?? "",
}),
formState: {
dirtyFields,
isDirty,
Expand Down
17 changes: 17 additions & 0 deletions src/utilities/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,13 @@ describe("getValidatedFormData", () => {
colors: ["red", "green", "blue"],
numbers: ["1", "2", "3"],
},
receivedValues: {
user: {
name: "john",
},
colors: ["red", "green", "blue"],
numbers: ["1", "2", "3"],
},
errors: undefined,
});
});
Expand Down Expand Up @@ -383,6 +390,7 @@ describe("getValidatedFormData", () => {
);
expect(validatedFormData).toStrictEqual({
data: formData,
receivedValues: formData,
errors: undefined,
});
});
Expand Down Expand Up @@ -424,6 +432,15 @@ describe("getValidatedFormData", () => {
something: "else",
},
},
receivedValues: {
name: "John Doe",
age: "30",
hobbies: ["Reading", "Writing", "Coding"],
other: {
skills: ["testing", "testing"],
something: "else",
},
},
errors: undefined,
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const getValidatedFormData = async <T extends FieldValues>(
? getFormDataFromSearchParams(request)
: await parseFormData<T>(request);
const validatedOutput = await validateFormData<T>(data, resolver);
return validatedOutput;
return { ...validatedOutput, receivedValues: data };
};

/**
Expand Down
Loading

0 comments on commit 126668e

Please sign in to comment.