Skip to content

Commit

Permalink
Support for NO JS forms
Browse files Browse the repository at this point in the history
  • Loading branch information
AlemTuzlak committed May 1, 2023
1 parent 1cb2a31 commit 616fad3
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 10 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,13 @@ export default function MyForm() {
```

## Utilities
<hr />

## getValidatedFormData

Now supports no-js form submissions!

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`.

```jsx
Expand Down
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.6",
"version": "1.0.7",
"description": "Utility wrapper around react-hook-form for use with Remix.run",
"type": "module",
"main": "./dist/index.umd.cjs",
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export * from "./utilities";
export {
parseFormData,
createFormData,
getValidatedFormData,
validateFormData,
} from "./utilities";
export * from "./hook";
88 changes: 83 additions & 5 deletions src/utilities/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { createFormData, mergeErrors, parseFormData } from "./index";
import {
createFormData,
generateFormData,
mergeErrors,
parseFormData,
} from "./index";

describe("createFormData", () => {
it("should create a FormData object with the provided data", () => {
Expand Down Expand Up @@ -55,13 +60,37 @@ describe("parseFormData", () => {
expect(parsedData).toEqual(data);
});

it("should throw an error if no data is found for the specified key", async () => {
it("should return an empty object if no formData exists", async () => {
const request = new Request("http://localhost:3000");
const requestFormDataSpy = vi.spyOn(request, "formData");
requestFormDataSpy.mockResolvedValueOnce(createFormData({}));
await expect(parseFormData(request, "randomKey")).rejects.toThrow(
"No data found"
);
const parsedData = await parseFormData(request);
expect(parsedData).toEqual({});
});

it("should return formData if NO js was used and formData was passed as is", async () => {
const formData = new FormData();
formData.append("name", "John Doe");
formData.append("age", "30");
formData.append("hobbies.0", "Reading");
formData.append("hobbies.1", "Writing");
formData.append("hobbies.2", "Coding");
formData.append("other.skills.0", "testing");
formData.append("other.skills.1", "testing");
formData.append("other.something", "else");
const request = new Request("http://localhost:3000");
const requestFormDataSpy = vi.spyOn(request, "formData");
requestFormDataSpy.mockResolvedValueOnce(formData);
const parsedData = await parseFormData(request);
expect(parsedData).toEqual({
name: "John Doe",
age: "30",
hobbies: ["Reading", "Writing", "Coding"],
other: {
skills: ["testing", "testing"],
something: "else",
},
});
});

it("should throw an error if the retrieved data is not a string (but a file instead)", async () => {
Expand Down Expand Up @@ -132,3 +161,52 @@ describe("mergeErrors", () => {
expect(mergedErrors).toEqual(expectedErrors);
});
});

describe("generateFormData", () => {
it("should generate an output object for flat form data", () => {
const formData = new FormData();
formData.append("name", "John Doe");
formData.append("email", "[email protected]");

const expectedOutput = {
name: "John Doe",
email: "[email protected]",
};

expect(generateFormData(formData)).toEqual(expectedOutput);
});

it("should generate an output object for nested form data", () => {
const formData = new FormData();
formData.append("user.name.first", "John");
formData.append("user.name.last", "Doe");
formData.append("user.email", "[email protected]");

const expectedOutput = {
user: {
name: {
first: "John",
last: "Doe",
},
email: "[email protected]",
},
};

expect(generateFormData(formData)).toEqual(expectedOutput);
});

it("should generate an output object with arrays for integer indexes", () => {
const formData = new FormData();
formData.append("user.roles.0", "admin");
formData.append("user.roles.1", "editor");
formData.append("user.roles.2", "contributor");

const expectedOutput = {
user: {
roles: ["admin", "editor", "contributor"],
},
};

expect(generateFormData(formData)).toEqual(expectedOutput);
});
});
52 changes: 50 additions & 2 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,55 @@ import {
FieldErrorsImpl,
DeepRequired,
} from "react-hook-form";

/**
* 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.
* @returns {Object} The output object generated from the form data.
*/
export const generateFormData = (formData: FormData) => {
// 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()) {
// Split the key into an array of parts.
const keyParts = key.split(".");
// Initialize a variable to point to the current object in the output object.
let currentObject = outputObject;

// Iterate through each key part except for the last one.
for (let i = 0; i < keyParts.length - 1; i++) {
// Get the current key part.
const keyPart = keyParts[i];
// If the current object doesn't have a property with the current key part,
// initialize it as an object or array depending on whether the next key part is a valid integer index or not.
if (!currentObject[keyPart]) {
currentObject[keyPart] = /^\d+$/.test(keyParts[i + 1]) ? [] : {};
}
// Move the current object pointer to the next level of the output object.
currentObject = currentObject[keyPart];
}

// Get the last key part.
const lastKeyPart = keyParts[keyParts.length - 1];

// If the last key part is a valid integer index, push the value to the current array.
if (/^\d+$/.test(lastKeyPart)) {
currentObject.push(value);
}
// Otherwise, set a property on the current object with the last key part and the corresponding value.
else {
currentObject[lastKeyPart] = value;
}
}

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

/**
* Parses the data from an HTTP request and validates it against a schema.
*
Expand Down Expand Up @@ -62,7 +111,6 @@ export const createFormData = <T extends FieldValues>(
return formData;
};
/**
Parses the specified Request object's FormData to retrieve the data associated with the specified key.
@template T - The type of the data to be returned.
@param {Request} request - The Request object whose FormData is to be parsed.
Expand All @@ -78,7 +126,7 @@ export const parseFormData = async <T extends any>(
const data = formData.get(key);

if (!data) {
throw new Error("No data found");
return generateFormData(formData);
}

if (!(typeof data === "string")) {
Expand Down

0 comments on commit 616fad3

Please sign in to comment.