@opentf/web-form 0.1.0-alpha.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.
Files changed (2) hide show
  1. package/index.js +163 -0
  2. package/package.json +20 -0
package/index.js ADDED
@@ -0,0 +1,163 @@
1
+ import { signal, effect, computed, batch } from "@preact/signals-core";
2
+
3
+ /**
4
+ * Helper to set a nested property in an object.
5
+ */
6
+ const setPath = (obj, path, value) => {
7
+ const parts = path.split('.');
8
+ let current = obj;
9
+ for (let i = 0; i < parts.length - 1; i++) {
10
+ const part = parts[i];
11
+ const next = parts[i + 1];
12
+ if (!current[part]) {
13
+ current[part] = !isNaN(next) ? [] : {};
14
+ }
15
+ current = current[part];
16
+ }
17
+ current[parts[parts.length - 1]] = value;
18
+ };
19
+
20
+ /**
21
+ * Creates a reactive form state manager.
22
+ */
23
+ export function createForm({ initialValues = {}, validate, schema, validator } = {}) {
24
+ const fields = {}; // Stores field signals by path
25
+ const touched = {}; // Stores touched signals by path
26
+ const fieldPaths = signal([]); // Track list of paths for computed to depend on
27
+
28
+ function ensureField(path, initVal) {
29
+ if (!fields[path]) {
30
+ fields[path] = signal(initVal === undefined ? "" : initVal);
31
+ touched[path] = signal(false);
32
+ }
33
+ if (!fieldPaths.value.includes(path)) {
34
+ fieldPaths.value = [...fieldPaths.value, path];
35
+ }
36
+ if (initVal !== undefined) {
37
+ fields[path].value = initVal;
38
+ }
39
+ return fields[path];
40
+ }
41
+
42
+ const walk = (obj, path = "") => {
43
+ if (obj && typeof obj === "object" && obj !== null) {
44
+ Object.keys(obj).forEach(key => {
45
+ const fullPath = path ? `${path}.${key}` : key;
46
+ walk(obj[key], fullPath);
47
+ });
48
+ } else if (path) {
49
+ ensureField(path, obj);
50
+ }
51
+ };
52
+
53
+ // Initial population
54
+ batch(() => walk(initialValues));
55
+
56
+ // Computed state object
57
+ const values = computed(() => {
58
+ const res = {};
59
+ fieldPaths.value.forEach(path => {
60
+ setPath(res, path, fields[path].value);
61
+ });
62
+ return res;
63
+ });
64
+
65
+ const errors = signal({});
66
+
67
+ // Reactive validation
68
+ effect(() => {
69
+ const currentValues = values.value;
70
+ if (validator) {
71
+ const result = validator(currentValues, schema);
72
+ errors.value = result.errors || {};
73
+ } else if (validate) {
74
+ errors.value = validate(currentValues) || {};
75
+ }
76
+ });
77
+
78
+ function register(name) {
79
+ const s = ensureField(name);
80
+ return {
81
+ name,
82
+ value: s,
83
+ oninput: (e) => {
84
+ s.value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
85
+ },
86
+ onblur: () => {
87
+ if (touched[name]) touched[name].value = true;
88
+ }
89
+ };
90
+ }
91
+
92
+ function handleSubmit(onSubmit) {
93
+ return (e) => {
94
+ if (e && e.preventDefault) e.preventDefault();
95
+
96
+ // Mark all fields as touched on submit
97
+ batch(() => {
98
+ fieldPaths.value.forEach(path => {
99
+ if (touched[path]) touched[path].value = true;
100
+ });
101
+ });
102
+
103
+ const currentValues = values.value;
104
+ let currentErrors = {};
105
+ if (validator) {
106
+ currentErrors = validator(currentValues, schema).errors || {};
107
+ } else if (validate) {
108
+ currentErrors = validate(currentValues) || {};
109
+ }
110
+
111
+ errors.value = currentErrors;
112
+
113
+ if (Object.keys(currentErrors).length === 0) {
114
+ onSubmit(currentValues);
115
+ }
116
+ };
117
+ }
118
+
119
+ function setValues(newValues) {
120
+ batch(() => {
121
+ const keysToUpdate = Object.keys(newValues);
122
+ const remainingPaths = fieldPaths.value.filter(path => {
123
+ return !keysToUpdate.some(k => path === k || path.startsWith(k + "."));
124
+ });
125
+ fieldPaths.value = remainingPaths;
126
+ walk(newValues);
127
+ });
128
+ }
129
+
130
+ function array(path) {
131
+ return computed(() => values.value[path] || []);
132
+ }
133
+
134
+ return {
135
+ register,
136
+ handleSubmit,
137
+ setValues,
138
+ getValues: () => values.value,
139
+ array,
140
+ values: new Proxy({}, {
141
+ get: (_, key) => values.value[key],
142
+ set: (_, key, val) => {
143
+ setValues({ [key]: val });
144
+ return true;
145
+ },
146
+ ownKeys: (_) => Object.keys(values.value),
147
+ getOwnPropertyDescriptor: (_, key) => {
148
+ const val = values.value[key];
149
+ return val !== undefined ? {
150
+ enumerable: true,
151
+ configurable: true,
152
+ value: val,
153
+ writable: true
154
+ } : undefined;
155
+ }
156
+ }),
157
+ errors,
158
+ touched: new Proxy({}, {
159
+ get: (_, key) => touched[key]
160
+ }),
161
+ fields // Also expose the raw signals map for advanced usage
162
+ };
163
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@opentf/web-form",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "A high-performance, path-based reactive forms library for Web App Framework and beyond.",
5
+ "main": "index.js",
6
+ "module": "index.js",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./index.js"
10
+ },
11
+ "dependencies": {
12
+ "@preact/signals-core": "^1.14.1"
13
+ },
14
+ "peerDependencies": {
15
+ "zod": "^4.3.6"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ }
20
+ }