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 @@ +

+ Solid Primitives I18n +

+ +# @solid-primitives/form + +[![turborepo](https://img.shields.io/badge/built%20with-turborepo-cc00ff.svg?style=for-the-badge&logo=turborepo)](https://turborepo.org/) +[![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/i18n?style=for-the-badge)](https://bundlephobia.com/package/@solid-primitives/i18n) +[![size](https://img.shields.io/npm/v/@solid-primitives/i18n?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/i18n) +[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](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 @@ + + + + + + + Solid App + + + + + +
+ + + + diff --git a/packages/form/dev/index.tsx b/packages/form/dev/index.tsx new file mode 100644 index 000000000..573800613 --- /dev/null +++ b/packages/form/dev/index.tsx @@ -0,0 +1,59 @@ +import { Component } from "solid-js"; +import { render } from "solid-js/web"; +import { z } from "zod"; +import { createForm } from "../src"; + +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 ( +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ ); +}; + +render(() => , 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..2c4dd9259 --- /dev/null +++ b/packages/form/package.json @@ -0,0 +1,57 @@ +{ + "name": "@solid-primitives/form", + "version": "1.1.4", + "description": "Primitive to create and compose forms", + "author": "Tanner Scadden ", + "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" + ], + "devDependencies": { + "zod": "3.20.6" + }, + "peerDependencies": { + "solid-js": "^1.6.0" + }, + "typesVersions": {} +} 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 = { + _input: TInput; + _output: TParsedInput; +}; + +export type ParserMyZodEsque = { + parse: (input: any) => TInput; +}; + +export type ParserSuperstructEsque = { + create: (input: unknown) => TInput; +}; + +export type ParserCustomValidatorEsque = (input: unknown) => TInput | Promise; + +export type ParserYupEsque = { + validateSync: (input: unknown) => TInput; +}; +export type ParserWithoutInput = + | ParserYupEsque + | ParserSuperstructEsque + | ParserCustomValidatorEsque + | ParserMyZodEsque; + +export type ParserWithInputOutput = ParserZodEsque; + +export type Parser = ParserWithoutInput | ParserWithInputOutput; + +export type inferParser = TParser extends ParserWithInputOutput< + infer $TIn, + infer $TOut +> + ? { + in: $TIn; + out: $TOut; + } + : TParser extends ParserWithoutInput + ? { + in: $InOut; + out: $InOut; + } + : never; + +export type ParseFn = (value: unknown) => TType | Promise; + +export function getParseFn(procedureParser: Parser): ParseFn { + 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( + procedureParser: Parser | undefined +): ParseFn { + if (!procedureParser) { + return v => v as TType; + } + return getParseFn(procedureParser); +} diff --git a/packages/form/src/index.ts b/packages/form/src/index.ts new file mode 100644 index 000000000..f7c31da70 --- /dev/null +++ b/packages/form/src/index.ts @@ -0,0 +1,60 @@ +import { getParseFn, Parser } from "./getParseFn"; + +export type FormError = { + data: T; + error: unknown; +}; + +export type CreateFormOptions = { + schema?: Parser; + onSubmit: (data: T) => Promise | void; + onError: (errors: FormError) => void | Promise; + castNumbers?: boolean; +}; + +type FormEvent = Event & { + submitter: HTMLElement; +} & { + currentTarget: HTMLFormElement; + target: Element; +}; + +// Taken from https://youmightnotneed.com/lodash#set +const set = (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 = (options: CreateFormOptions) => { + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + let data: Partial = {}; + + for (const [name, value] of formData.entries()) { + const v = !options.castNumbers || isNaN(value as any) ? value.toString() : Number(value); + set(data, name, v); + } + + try { + 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 }); + } + }; + + return { + handleSubmit + }; +}; 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..260212598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,15 @@ importers: node-fetch: 3.3.0 solid-js: 1.6.9 + packages/form: + specifiers: + solid-js: ^1.6.0 + zod: 3.20.6 + dependencies: + solid-js: 1.6.9 + devDependencies: + zod: 3.20.6 + packages/fullscreen: specifiers: solid-js: ^1.6.0 @@ -8410,3 +8419,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zod/3.20.6: + resolution: {integrity: sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==} + dev: true