@michalzard/svelte-forms 1.0.0

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/index.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { writable, get, type Writable } from "svelte/store";
2
+ import {
3
+ InferOutput,
4
+ safeParse,
5
+ type BaseSchema,
6
+ type BaseIssue,
7
+ } from "valibot";
8
+ type SubmitFn<T> = (values: T) => void | Promise<void>;
9
+
10
+ type UseFormParams<S extends BaseSchema<unknown, unknown, BaseIssue<unknown>>> =
11
+ {
12
+ initialValues: InferOutput<S>;
13
+ schema: S;
14
+ onSubmit: SubmitFn<InferOutput<S>>;
15
+ debounceInterval?: number;
16
+ };
17
+
18
+ export function useForm<
19
+ S extends BaseSchema<unknown, Record<string, any>, BaseIssue<unknown>>,
20
+ >(params: UseFormParams<S>) {
21
+ type T = InferOutput<S>;
22
+ const { initialValues, schema, onSubmit, debounceInterval = 50 } = params;
23
+ const values: Writable<T> = writable({ ...initialValues });
24
+ // only contains fields that currenlty have errors
25
+ const errors: Writable<Partial<Record<keyof T, string>>> = writable({});
26
+ const touched: Writable<Record<keyof T, boolean>> = writable(
27
+ Object.keys(initialValues).reduce((acc, key) => {
28
+ acc[key as keyof T] = false;
29
+ return acc;
30
+ }, {} as Record<keyof T, boolean>),
31
+ );
32
+
33
+ function markTouched(field: keyof T) {
34
+ touched.update((t) => ({ ...t, [field]: true }));
35
+ }
36
+
37
+ function mapIssues(
38
+ issues: BaseIssue<any>[],
39
+ ): Partial<Record<keyof T, string>> {
40
+ const newErrors: Partial<Record<keyof T, string>> = {};
41
+ for (const issue of issues ?? []) {
42
+ const field = issue.path?.[0]?.key as keyof T;
43
+ if (field) newErrors[field] = issue.message;
44
+ }
45
+
46
+ return newErrors;
47
+ }
48
+ // Validation with debounce
49
+ let validationTimer: ReturnType<typeof setTimeout> | null = null;
50
+ values.subscribe(($values) => {
51
+ if (validationTimer) clearTimeout(validationTimer);
52
+
53
+ validationTimer = setTimeout(() => {
54
+ const result = safeParse(schema, $values);
55
+ errors.set(result.success ? {} : mapIssues(result.issues));
56
+ }, debounceInterval);
57
+ });
58
+
59
+ async function handleSubmit(event: SubmitEvent) {
60
+ event.preventDefault();
61
+ const currentValues = get(values);
62
+ errors.set({});
63
+ const result = safeParse(schema, currentValues);
64
+
65
+ if (!result.success) {
66
+ errors.set(mapIssues(result.issues));
67
+ // mark all invalid fields as touched
68
+ touched.update((t) => {
69
+ for (const issue of result.issues ?? []) {
70
+ const field = issue.path?.[0]?.key as keyof T;
71
+ if (field) t[field] = true;
72
+ }
73
+ return t;
74
+ });
75
+ return;
76
+ }
77
+
78
+ await onSubmit(currentValues);
79
+ }
80
+
81
+ function resetForm() {
82
+ values.set({ ...initialValues });
83
+ errors.set({});
84
+ touched.set(
85
+ Object.keys(initialValues).reduce((acc, key) => {
86
+ acc[key as keyof T] = false;
87
+ return acc;
88
+ }, {} as Record<keyof T, boolean>),
89
+ );
90
+ }
91
+
92
+ return { values, errors, touched, markTouched, handleSubmit, resetForm };
93
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@michalzard/svelte-forms",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.mjs",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "bun build index.ts --format cjs --outdir dist/cjs && bun build index.ts --format esm --outdir dist/esm && mv dist/cjs/index.js dist/index.js && mv dist/esm/index.js dist/index.mjs",
9
+ "release": "bun run build && changeset publish"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "devDependencies": {
15
+ "@changesets/cli": "^2.29.8",
16
+ "@types/bun": "latest"
17
+ },
18
+ "peerDependencies": {
19
+ "typescript": "^5"
20
+ },
21
+ "dependencies": {
22
+ "svelte": "^5.53.6",
23
+ "valibot": "^1.2.0"
24
+ }
25
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext"],
4
+ "target": "ESNext",
5
+ "module": "Preserve",
6
+ "moduleDetection": "force",
7
+ "jsx": "react-jsx",
8
+ "allowJs": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "noEmit": true,
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUncheckedIndexedAccess": true,
16
+ "noImplicitOverride": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noPropertyAccessFromIndexSignature": true
20
+ }
21
+ }