@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.
- package/index.js +163 -0
- 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
|
+
}
|