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/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 @@
+
+
+
+
+
+
+ Solid App
+
+
+
+
+ You need to enable JavaScript to run this 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