From 866ef5a2b30229f669fbd9541dd1e5142cee0da1 Mon Sep 17 00:00:00 2001 From: TannerScadden <tanner@scaddenfamily.com> Date: Mon, 13 Feb 2023 21:27:56 -0500 Subject: [PATCH 1/4] Saving a rough draft of a createForm --- packages/form/CHANGELOG.md | 5 +++ packages/form/LICENSE | 21 ++++++++++++ packages/form/README.md | 24 ++++++++++++++ packages/form/dev/index.html | 35 ++++++++++++++++++++ packages/form/dev/index.tsx | 51 +++++++++++++++++++++++++++++ packages/form/dev/vite.config.ts | 2 ++ packages/form/package.json | 54 +++++++++++++++++++++++++++++++ packages/form/src/index.ts | 46 ++++++++++++++++++++++++++ packages/form/test/index.test.ts | 7 ++++ packages/form/test/server.test.ts | 7 ++++ packages/form/tsconfig.json | 5 +++ pnpm-lock.yaml | 6 ++++ 12 files changed, 263 insertions(+) create mode 100644 packages/form/CHANGELOG.md create mode 100644 packages/form/LICENSE create mode 100644 packages/form/README.md create mode 100644 packages/form/dev/index.html create mode 100644 packages/form/dev/index.tsx create mode 100644 packages/form/dev/vite.config.ts create mode 100644 packages/form/package.json create mode 100644 packages/form/src/index.ts create mode 100644 packages/form/test/index.test.ts create mode 100644 packages/form/test/server.test.ts create mode 100644 packages/form/tsconfig.json diff --git a/packages/form/CHANGELOG.md b/packages/form/CHANGELOG.md new file mode 100644 index 000000000..9feeb39d8 --- /dev/null +++ b/packages/form/CHANGELOG.md @@ -0,0 +1,5 @@ +# @solid-primitives/form + +0.0.100 + +First commit of the form primitive. diff --git a/packages/form/LICENSE b/packages/form/LICENSE new file mode 100644 index 000000000..38b41d975 --- /dev/null +++ b/packages/form/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Solid Primitives Working Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/form/README.md b/packages/form/README.md new file mode 100644 index 000000000..77703c32e --- /dev/null +++ b/packages/form/README.md @@ -0,0 +1,24 @@ +<p> + <img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=I18n" alt="Solid Primitives I18n"> +</p> + +# @solid-primitives/form + +[](https://turborepo.org/) +[](https://bundlephobia.com/package/@solid-primitives/i18n) +[](https://www.npmjs.com/package/@solid-primitives/i18n) +[](https://github.com/solidjs-community/solid-primitives#contribution-process) + +Creates state and helpers for managing forms. + +## How to use it + +Install it: + +```bash +yarn add @solid-primitives/form +``` + +## Changelog + +See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/form/dev/index.html b/packages/form/dev/index.html new file mode 100644 index 000000000..65f53c4b6 --- /dev/null +++ b/packages/form/dev/index.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="theme-color" content="#000000" /> + <title>Solid App</title> + <style> + html { + font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif; + } + + body { + padding: 0; + margin: 0; + } + + a, + button { + cursor: pointer; + } + + * { + margin: 0; + } + </style> + </head> + + <body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root"></div> + + <script src="/index.tsx" type="module"></script> + </body> +</html> diff --git a/packages/form/dev/index.tsx b/packages/form/dev/index.tsx new file mode 100644 index 000000000..3a6732d5c --- /dev/null +++ b/packages/form/dev/index.tsx @@ -0,0 +1,51 @@ +import { Component } from "solid-js"; +import { render } from "solid-js/web"; +import "uno.css"; +import { createForm } from "../src"; + +const App: Component = () => { + const { updateForm, formData } = createForm({ name: "", food: "", email: "" }); + + const onSubmit = (e: any) => { + e.preventDefault(); + console.log(formData); + }; + + return ( + <form + class="p-24 box-border w-full min-h-screen flex flex-col justify-center items-center space-y-4 bg-gray-800 text-white" + onSubmit={onSubmit} + > + <label>Name</label> + <input + name="name" + required + value={formData.name} + onInput={e => updateForm("name", e.currentTarget.value)} + /> + <label>Favorite Food</label> + <select + required + name="food" + value={formData.food} + onInput={e => updateForm("food", e.currentTarget.value)} + > + <option value="" /> + <option value="apple">Apple</option> + <option value="pear">Pear</option> + <option value="banana">banana</option> + </select> + <label>Email</label> + <input + name="email" + type="email" + required + value={formData.email} + onInput={e => updateForm("email", e.currentTarget.value)} + /> + <button type="submit">Submit</button> + </form> + ); +}; + +render(() => <App />, document.getElementById("root")!); diff --git a/packages/form/dev/vite.config.ts b/packages/form/dev/vite.config.ts new file mode 100644 index 000000000..7ca66a520 --- /dev/null +++ b/packages/form/dev/vite.config.ts @@ -0,0 +1,2 @@ +import { viteConfig } from "../../../configs/vite.config"; +export default viteConfig; diff --git a/packages/form/package.json b/packages/form/package.json new file mode 100644 index 000000000..a2b264b83 --- /dev/null +++ b/packages/form/package.json @@ -0,0 +1,54 @@ +{ + "name": "@solid-primitives/form", + "version": "1.1.4", + "description": "Primitive to create and compose forms", + "author": "Tanner Scadden <tanner@scaddenfamily.com>", + "license": "MIT", + "homepage": "https://github.com/solidjs-community/solid-primitives/tree/main/packages/form", + "repository": { + "type": "git", + "url": "git+https://github.com/solidjs-community/solid-primitives.git" + }, + "primitive": { + "name": "form", + "stage": 3, + "list": [ + "createForm" + ], + "category": "Utilities" + }, + "files": [ + "dist" + ], + "private": false, + "sideEffects": false, + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "browser": {}, + "types": "./dist/index.d.ts", + "exports": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": "./dist/index.cjs" + }, + "scripts": { + "dev": "vite serve dev", + "page": "vite build dev", + "start": "vite -r ./dev/ -c ./dev/vite.config.ts", + "build": "jiti ../../scripts/build.ts", + "test": "vitest -c ../../configs/vitest.config.ts", + "test:ssr": "pnpm run test --mode ssr" + }, + "keywords": [ + "form", + "solid", + "primitives" + ], + "peerDependencies": { + "solid-js": "^1.6.0" + }, + "typesVersions": {} +} diff --git a/packages/form/src/index.ts b/packages/form/src/index.ts new file mode 100644 index 000000000..df601f07f --- /dev/null +++ b/packages/form/src/index.ts @@ -0,0 +1,46 @@ +import { createStore } from "solid-js/store"; + +// type FormSubmitEvent = Event & { +// submitter: HTMLElement; +// } & { +// currentTarget: HTMLFormElement; +// target: Element; +// }; + +// export type FormErrors<F extends object> = { [K in keyof F]: boolean }; +// function createErrors<F extends object>(data: F): FormErrors<F> { +// const values = Object.keys(data) as Array<keyof F>; + +// const errors = values.reduce((acc, val) => { +// acc[val] = false; +// return acc; +// }, {} as Partial<FormErrors<F>>); + +// return errors as FormErrors<F>; +// } + +export const createForm = <F extends object>(initialFormData: F) => { + const [formData, setFormData] = createStore<F>(initialFormData); + + const updateForm = <K extends keyof F>(key: K, value: F[K]): void => { + setFormData(key as any, value); + }; + + const bulkUpdateForm = (bulkUpdateData: Partial<F>): void => { + setFormData(val => ({ + ...val, + ...bulkUpdateData + })); + }; + + const overrideForm = (overrideData: F): void => { + setFormData(() => overrideData); + }; + + return { + formData, + updateForm, + bulkUpdateForm, + overrideForm + }; +}; diff --git a/packages/form/test/index.test.ts b/packages/form/test/index.test.ts new file mode 100644 index 000000000..29e7016e7 --- /dev/null +++ b/packages/form/test/index.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from "vitest"; + +describe("createForm", () => { + it("Should create a form", () => { + expect(false).toBe(true); + }); +}); diff --git a/packages/form/test/server.test.ts b/packages/form/test/server.test.ts new file mode 100644 index 000000000..29e7016e7 --- /dev/null +++ b/packages/form/test/server.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from "vitest"; + +describe("createForm", () => { + it("Should create a form", () => { + expect(false).toBe(true); + }); +}); diff --git a/packages/form/tsconfig.json b/packages/form/tsconfig.json new file mode 100644 index 000000000..0f52db9f5 --- /dev/null +++ b/packages/form/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src", "./test", "./dev", "./demo"], + "exclude": ["node_modules", "./dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c17d51acc..ff4dd4e67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,12 @@ importers: node-fetch: 3.3.0 solid-js: 1.6.9 + packages/form: + specifiers: + solid-js: ^1.6.0 + dependencies: + solid-js: 1.6.9 + packages/fullscreen: specifiers: solid-js: ^1.6.0 From 7fde7e911b7463bc61506b6b61f0d453cdf817c0 Mon Sep 17 00:00:00 2001 From: TannerScadden <tanner@scaddenfamily.com> Date: Wed, 15 Feb 2023 12:30:15 -0500 Subject: [PATCH 2/4] Initial form proposal. Handles the form submit event, gathers the data, and uses zod for validation --- packages/form/dev/index.tsx | 88 ++++++++++++++++++++----------------- packages/form/package.json | 7 +++ packages/form/src/index.ts | 67 ++++++++++++++-------------- pnpm-lock.yaml | 25 +++++++++++ 4 files changed, 113 insertions(+), 74 deletions(-) diff --git a/packages/form/dev/index.tsx b/packages/form/dev/index.tsx index 3a6732d5c..573800613 100644 --- a/packages/form/dev/index.tsx +++ b/packages/form/dev/index.tsx @@ -1,49 +1,57 @@ import { Component } from "solid-js"; import { render } from "solid-js/web"; -import "uno.css"; +import { z } from "zod"; import { createForm } from "../src"; -const App: Component = () => { - const { updateForm, formData } = createForm({ name: "", food: "", email: "" }); - - const onSubmit = (e: any) => { - e.preventDefault(); - console.log(formData); - }; +const registerSchema = z.object({ + password: z.string().min(8), + emergencyContact: z.array(z.object({ firstName: z.string() })).min(2), + favoriteNumber: z.number(), + user: z.object({ + lastName: z.string(), + email: z.object({ + random: z.array( + z.object({ + keys: z.object({ + email: z.string().email() + }) + }) + ) + }) + }) +}); +const App: Component = () => { + const { handleSubmit } = createForm({ + schema: registerSchema, + onError(errors) { + console.log(errors); + }, + onSubmit(data) { + console.log(data); + } + }); return ( - <form - class="p-24 box-border w-full min-h-screen flex flex-col justify-center items-center space-y-4 bg-gray-800 text-white" - onSubmit={onSubmit} - > - <label>Name</label> - <input - name="name" - required - value={formData.name} - onInput={e => updateForm("name", e.currentTarget.value)} - /> - <label>Favorite Food</label> - <select - required - name="food" - value={formData.food} - onInput={e => updateForm("food", e.currentTarget.value)} - > - <option value="" /> - <option value="apple">Apple</option> - <option value="pear">Pear</option> - <option value="banana">banana</option> - </select> - <label>Email</label> - <input - name="email" - type="email" - required - value={formData.email} - onInput={e => updateForm("email", e.currentTarget.value)} - /> - <button type="submit">Submit</button> + <form onSubmit={handleSubmit}> + <label>1st Emergency Contact First Name</label> + <input name="emergencyContact[0].firstName" required type="text" /> + <br /> + <label>2nd Emergency Contact First Name</label> + <input name="emergencyContact[1].firstName" required type="text" /> + <br /> + <label>Your Favorite Number</label> + <input name="favoriteNumber" required type="number" /> + <br /> + <label>Your Last Name</label> + <input name="user.lastName" required type="text" /> + <br /> + <label>Crazy Set Path Email</label> + <input name="user.email.random[0].keys.email" required type="email" /> + <br /> + <label>Password</label> + <input name="password" required type="password" /> + <br /> + <button type="submit">Test</button> </form> ); }; diff --git a/packages/form/package.json b/packages/form/package.json index a2b264b83..e87064a2e 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -47,6 +47,13 @@ "solid", "primitives" ], + "devDependencies": { + "@types/lodash.set": "4.3.7" + }, + "dependencies": { + "zod": "3.20.6", + "lodash.set": "4.3.2" + }, "peerDependencies": { "solid-js": "^1.6.0" }, diff --git a/packages/form/src/index.ts b/packages/form/src/index.ts index df601f07f..5de979077 100644 --- a/packages/form/src/index.ts +++ b/packages/form/src/index.ts @@ -1,46 +1,45 @@ -import { createStore } from "solid-js/store"; +import set from "lodash.set"; +import { z, ZodError } from "zod"; -// type FormSubmitEvent = Event & { -// submitter: HTMLElement; -// } & { -// currentTarget: HTMLFormElement; -// target: Element; -// }; - -// export type FormErrors<F extends object> = { [K in keyof F]: boolean }; -// function createErrors<F extends object>(data: F): FormErrors<F> { -// const values = Object.keys(data) as Array<keyof F>; +export type FormError<T> = { + data: T; + error: ZodError; +}; -// const errors = values.reduce((acc, val) => { -// acc[val] = false; -// return acc; -// }, {} as Partial<FormErrors<F>>); +export type CreateFormOptions<T> = { + schema: z.AnyZodObject; + onSubmit: (data: T) => Promise<void> | void; + onError: (errors: FormError<T>) => void | Promise<void>; +}; -// return errors as FormErrors<F>; -// } +type FormEvent = Event & { + submitter: HTMLElement; +} & { + currentTarget: HTMLFormElement; + target: Element; +}; -export const createForm = <F extends object>(initialFormData: F) => { - const [formData, setFormData] = createStore<F>(initialFormData); +export const createForm = <T>(options: CreateFormOptions<T>) => { + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); - const updateForm = <K extends keyof F>(key: K, value: F[K]): void => { - setFormData(key as any, value); - }; + let data: Partial<T> = {}; - const bulkUpdateForm = (bulkUpdateData: Partial<F>): void => { - setFormData(val => ({ - ...val, - ...bulkUpdateData - })); - }; + for (const [name, value] of formData.entries()) { + const v = isNaN(value as any) ? value : Number(value); + set(data, name, v); + } - const overrideForm = (overrideData: F): void => { - setFormData(() => overrideData); + try { + options.schema.parse(data); + options.onSubmit(data as T); + } catch (e) { + options.onError({ data: data as T, error: e as ZodError }); + } }; return { - formData, - updateForm, - bulkUpdateForm, - overrideForm + handleSubmit }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff4dd4e67..c6a178a35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,9 +227,16 @@ importers: packages/form: specifiers: + '@types/lodash.set': 4.3.7 + lodash.set: 4.3.2 solid-js: ^1.6.0 + zod: 3.20.6 dependencies: + lodash.set: 4.3.2 solid-js: 1.6.9 + zod: 3.20.6 + devDependencies: + '@types/lodash.set': 4.3.7 packages/fullscreen: specifiers: @@ -2950,6 +2957,16 @@ packages: '@types/geojson': 7946.0.10 dev: true + /@types/lodash.set/4.3.7: + resolution: {integrity: sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==} + dependencies: + '@types/lodash': 4.14.191 + dev: true + + /@types/lodash/4.14.191: + resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} + dev: true + /@types/minimist/1.2.2: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true @@ -6045,6 +6062,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.set/4.3.2: + resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} + dev: false + /lodash.sortby/4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true @@ -8416,3 +8437,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zod/3.20.6: + resolution: {integrity: sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==} + dev: false From 30fe9ccaeff63531c108e2d7f1cd38423fe5a60d Mon Sep 17 00:00:00 2001 From: TannerScadden <tanner@scaddenfamily.com> Date: Wed, 15 Feb 2023 17:26:55 -0500 Subject: [PATCH 3/4] Removed lodash and zod as dependencies --- packages/form/package.json | 6 +-- packages/form/src/getParseFn.ts | 92 +++++++++++++++++++++++++++++++++ packages/form/src/index.ts | 28 +++++++--- pnpm-lock.yaml | 22 +------- 4 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 packages/form/src/getParseFn.ts diff --git a/packages/form/package.json b/packages/form/package.json index e87064a2e..2c4dd9259 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -48,11 +48,7 @@ "primitives" ], "devDependencies": { - "@types/lodash.set": "4.3.7" - }, - "dependencies": { - "zod": "3.20.6", - "lodash.set": "4.3.2" + "zod": "3.20.6" }, "peerDependencies": { "solid-js": "^1.6.0" diff --git a/packages/form/src/getParseFn.ts b/packages/form/src/getParseFn.ts new file mode 100644 index 000000000..6aa3c7150 --- /dev/null +++ b/packages/form/src/getParseFn.ts @@ -0,0 +1,92 @@ +// Credit to @trpc/server +// https://github.com/trpc/trpc/blob/main/packages/server/src/core/parser.ts +// https://github.com/trpc/trpc/blob/main/packages/server/src/core/internals/getParseFn.ts + +export type ParserZodEsque<TInput, TParsedInput> = { + _input: TInput; + _output: TParsedInput; +}; + +export type ParserMyZodEsque<TInput> = { + parse: (input: any) => TInput; +}; + +export type ParserSuperstructEsque<TInput> = { + create: (input: unknown) => TInput; +}; + +export type ParserCustomValidatorEsque<TInput> = (input: unknown) => TInput | Promise<TInput>; + +export type ParserYupEsque<TInput> = { + validateSync: (input: unknown) => TInput; +}; +export type ParserWithoutInput<TInput> = + | ParserYupEsque<TInput> + | ParserSuperstructEsque<TInput> + | ParserCustomValidatorEsque<TInput> + | ParserMyZodEsque<TInput>; + +export type ParserWithInputOutput<TInput, TParsedInput> = ParserZodEsque<TInput, TParsedInput>; + +export type Parser = ParserWithoutInput<any> | ParserWithInputOutput<any, any>; + +export type inferParser<TParser extends Parser> = TParser extends ParserWithInputOutput< + infer $TIn, + infer $TOut +> + ? { + in: $TIn; + out: $TOut; + } + : TParser extends ParserWithoutInput<infer $InOut> + ? { + in: $InOut; + out: $InOut; + } + : never; + +export type ParseFn<TType> = (value: unknown) => TType | Promise<TType>; + +export function getParseFn<TType>(procedureParser: Parser): ParseFn<TType> { + const parser = procedureParser as any; + + if (typeof parser === "function") { + // ProcedureParserCustomValidatorEsque + return parser; + } + + if (typeof parser.parseAsync === "function") { + // ProcedureParserZodEsque + return parser.parseAsync.bind(parser); + } + + if (typeof parser.parse === "function") { + // ProcedureParserZodEsque + return parser.parse.bind(parser); + } + + if (typeof parser.validateSync === "function") { + // ProcedureParserYupEsque + return parser.validateSync.bind(parser); + } + + if (typeof parser.create === "function") { + // ProcedureParserSuperstructEsque + return parser.create.bind(parser); + } + + throw new Error("Could not find a validator fn"); +} + +/** + * @deprecated only for backwards compat + * @internal + */ +export function getParseFnOrPassThrough<TType>( + procedureParser: Parser | undefined +): ParseFn<TType> { + if (!procedureParser) { + return v => v as TType; + } + return getParseFn(procedureParser); +} diff --git a/packages/form/src/index.ts b/packages/form/src/index.ts index 5de979077..59444dfc9 100644 --- a/packages/form/src/index.ts +++ b/packages/form/src/index.ts @@ -1,5 +1,5 @@ -import set from "lodash.set"; -import { z, ZodError } from "zod"; +import { ZodError } from "zod"; +import { getParseFn, Parser } from "./getParseFn"; export type FormError<T> = { data: T; @@ -7,9 +7,10 @@ export type FormError<T> = { }; export type CreateFormOptions<T> = { - schema: z.AnyZodObject; + schema?: Parser; onSubmit: (data: T) => Promise<void> | void; onError: (errors: FormError<T>) => void | Promise<void>; + castNumbers?: boolean; }; type FormEvent = Event & { @@ -19,20 +20,35 @@ type FormEvent = Event & { target: Element; }; +// Taken from https://youmightnotneed.com/lodash#set +const set = <T extends object>(obj: T, path: string | string[], value: string | number) => { + // Regex explained: https://regexr.com/58j0k + const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g)!; + + pathArray.reduce((acc, key, i) => { + if (acc[key] === undefined) acc[key] = {}; + if (i === pathArray.length - 1) acc[key] = value; + return acc[key]; + }, obj as any); +}; + export const createForm = <T>(options: CreateFormOptions<T>) => { - const handleSubmit = (e: FormEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); let data: Partial<T> = {}; for (const [name, value] of formData.entries()) { - const v = isNaN(value as any) ? value : Number(value); + const v = !options.castNumbers || isNaN(value as any) ? value.toString() : Number(value); set(data, name, v); } try { - options.schema.parse(data); + if (options.schema) { + const parser = getParseFn(options.schema); + await parser(data); + } options.onSubmit(data as T); } catch (e) { options.onError({ data: data as T, error: e as ZodError }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6a178a35..260212598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,16 +227,12 @@ importers: packages/form: specifiers: - '@types/lodash.set': 4.3.7 - lodash.set: 4.3.2 solid-js: ^1.6.0 zod: 3.20.6 dependencies: - lodash.set: 4.3.2 solid-js: 1.6.9 - zod: 3.20.6 devDependencies: - '@types/lodash.set': 4.3.7 + zod: 3.20.6 packages/fullscreen: specifiers: @@ -2957,16 +2953,6 @@ packages: '@types/geojson': 7946.0.10 dev: true - /@types/lodash.set/4.3.7: - resolution: {integrity: sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==} - dependencies: - '@types/lodash': 4.14.191 - dev: true - - /@types/lodash/4.14.191: - resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} - dev: true - /@types/minimist/1.2.2: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true @@ -6062,10 +6048,6 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true - /lodash.set/4.3.2: - resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} - dev: false - /lodash.sortby/4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true @@ -8440,4 +8422,4 @@ packages: /zod/3.20.6: resolution: {integrity: sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==} - dev: false + dev: true From b78a704a564fdcbb8b48f07632c0d2aebf7ba41c Mon Sep 17 00:00:00 2001 From: TannerScadden <tanner@scaddenfamily.com> Date: Wed, 15 Feb 2023 17:28:53 -0500 Subject: [PATCH 4/4] Changed error to be of type unknown --- packages/form/src/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/form/src/index.ts b/packages/form/src/index.ts index 59444dfc9..f7c31da70 100644 --- a/packages/form/src/index.ts +++ b/packages/form/src/index.ts @@ -1,9 +1,8 @@ -import { ZodError } from "zod"; import { getParseFn, Parser } from "./getParseFn"; export type FormError<T> = { data: T; - error: ZodError; + error: unknown; }; export type CreateFormOptions<T> = { @@ -51,7 +50,7 @@ export const createForm = <T>(options: CreateFormOptions<T>) => { } options.onSubmit(data as T); } catch (e) { - options.onError({ data: data as T, error: e as ZodError }); + options.onError({ data: data as T, error: e }); } };