@opentf/web-form 0.1.0-alpha.1 → 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/CHANGELOG.md +8 -0
- package/README.md +141 -0
- package/bunfig.toml +6 -0
- package/index.js +343 -129
- package/package.json +12 -5
- package/tests/Basic.test.jsx +37 -0
- package/tests/BasicForm.jsx +29 -0
- package/tests/Component.test.jsx +79 -0
- package/tests/DynamicArrayForm.jsx +32 -0
- package/tests/DynamicArrays.test.jsx +33 -0
- package/tests/DynamicListForm.jsx +20 -0
- package/tests/Logic.test.js +153 -0
- package/tests/NestedFields.test.jsx +28 -0
- package/tests/NestedForm.jsx +32 -0
- package/tests/Reactivity.test.jsx +26 -0
- package/tests/RegisterMetadata.test.js +83 -0
- package/tests/State.test.js +93 -0
- package/tests/StateComponent.test.jsx +84 -0
- package/tests/ValidationModes.test.js +84 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# @opentf/web-form
|
|
2
|
+
|
|
3
|
+
A reactive form management library for **OTF Web**.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Reactive Binding**: Seamlessly bind form values to signals.
|
|
8
|
+
- 🌲 **Nested State**: Support for complex, nested form structures.
|
|
9
|
+
- 🛡️ **Validation**: Flexible validation system with Zod or custom logic.
|
|
10
|
+
- 🚀 **High Performance**: Direct DOM updates without VDOM overhead.
|
|
11
|
+
- 🛠️ **Simple API**: Featuring a `register` pattern for clean boilerplate-free code.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @opentf/web-form
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
### Using the `register` Pattern
|
|
22
|
+
|
|
23
|
+
The `register` function returns all necessary props (`name`, `value`, `oninput`, `onblur`) to bind a field to an input element.
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
import { createForm } from '@opentf/web-form';
|
|
27
|
+
|
|
28
|
+
const form = createForm({
|
|
29
|
+
initialValues: { username: '' }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// In your component
|
|
33
|
+
<input {...form.register('username')} />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Validation with Zod
|
|
37
|
+
|
|
38
|
+
You can easily integrate **Zod** for schema-based validation by providing a `validator` function.
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
import { createForm } from '@opentf/web-form';
|
|
42
|
+
import { z } from 'zod';
|
|
43
|
+
|
|
44
|
+
const schema = z.object({
|
|
45
|
+
username: z.string().min(3, 'Too short'),
|
|
46
|
+
email: z.string().email('Invalid email')
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// A simple Zod validator wrapper
|
|
50
|
+
const zodValidator = (values, schema) => {
|
|
51
|
+
const result = schema.safeParse(values);
|
|
52
|
+
if (result.success) return {};
|
|
53
|
+
const errors = {};
|
|
54
|
+
result.error.errors.forEach(e => {
|
|
55
|
+
errors[e.path.join('.')] = e.message;
|
|
56
|
+
});
|
|
57
|
+
return { errors };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const form = createForm({
|
|
61
|
+
initialValues: { username: '', email: '' },
|
|
62
|
+
schema,
|
|
63
|
+
validator: zodValidator // Can return { errors: ... } or direct errors object
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const onSubmit = (values) => {
|
|
67
|
+
console.log('Form Submitted:', values);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// In your component
|
|
71
|
+
<form onsubmit={form.handleSubmit(onSubmit)}>
|
|
72
|
+
<div>
|
|
73
|
+
<input {...form.register('username')} placeholder="Username" />
|
|
74
|
+
<span className="error">{form.errors.username}</span>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div>
|
|
78
|
+
<input {...form.register('email')} placeholder="Email" />
|
|
79
|
+
<span className="error">{form.errors.email}</span>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<button type="submit">Submit</button>
|
|
83
|
+
</form>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Form State Helpers
|
|
87
|
+
|
|
88
|
+
`createForm` returns several reactive signals and proxies to track the form lifecycle:
|
|
89
|
+
|
|
90
|
+
| Helper | Type | Description |
|
|
91
|
+
| :--- | :--- | :--- |
|
|
92
|
+
| `isValid` | `Signal<boolean>` | `true` if there are no validation errors. |
|
|
93
|
+
| `isChanged` | `Signal<boolean>` | `true` if the form values differ from `initialValues`. |
|
|
94
|
+
| `isTouched` | `Signal<boolean>` | `true` if any field has been blurred. |
|
|
95
|
+
| `isSubmitting` | `Signal<boolean>` | `true` while the `onSubmit` handler is running. |
|
|
96
|
+
| `isSubmitted` | `Signal<boolean>` | `true` if the form has been successfully submitted. |
|
|
97
|
+
| `touched` | `Proxy<boolean>` | Access per-field touched state: `form.touched.email`. |
|
|
98
|
+
| `changed` | `Proxy<boolean>` | Access per-field changed state: `form.changed.email`. |
|
|
99
|
+
|
|
100
|
+
## Validation Modes
|
|
101
|
+
|
|
102
|
+
You can customize when validation triggers using `mode` and `reValidateMode`.
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
const form = createForm({
|
|
106
|
+
mode: 'onBlur', // Initial validation: 'onSubmit', 'onBlur', 'onTouched', 'onChange'
|
|
107
|
+
reValidateMode: 'onChange' // Re-validation: 'onChange', 'onBlur', 'onSubmit'
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- **Default mode**: `onBlur`
|
|
112
|
+
- **Default reValidateMode**: `onChange`
|
|
113
|
+
|
|
114
|
+
### Resetting the Form
|
|
115
|
+
|
|
116
|
+
You can restore the form to its `initialValues` and clear all status flags using the `reset()` method.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
<button type="button" onclick={() => form.reset()}>
|
|
120
|
+
Reset Form
|
|
121
|
+
</button>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Custom Validation
|
|
125
|
+
|
|
126
|
+
For simpler cases, you can use the `validate` function which expects you to return the errors object directly.
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
const form = createForm({
|
|
130
|
+
initialValues: { age: 0 },
|
|
131
|
+
validate: (values) => {
|
|
132
|
+
const errors = {};
|
|
133
|
+
if (values.age < 18) errors.age = 'Too young';
|
|
134
|
+
return errors;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT © [Open Tech Foundation](https://github.com/Open-Tech-Foundation)
|
package/bunfig.toml
ADDED
package/index.js
CHANGED
|
@@ -1,163 +1,377 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
signal,
|
|
4
|
+
computed,
|
|
5
|
+
batch,
|
|
6
|
+
effect,
|
|
7
|
+
untracked,
|
|
8
|
+
getCurrentInstance,
|
|
9
|
+
} from "@opentf/web";
|
|
10
|
+
|
|
11
|
+
export { signal, computed, batch, effect, untracked, getCurrentInstance };
|
|
12
|
+
|
|
13
|
+
import { set, get, unset, clone, isEql } from "@opentf/std";
|
|
2
14
|
|
|
3
15
|
/**
|
|
4
|
-
*
|
|
16
|
+
* Enterprise Form Reactivity Engine
|
|
17
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - Mount-Aware Stability Cache
|
|
20
|
+
* - Setup-Phase Subscription Isolation
|
|
21
|
+
* - Deep Reactive Proxy with Array Mutation Support
|
|
5
22
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
|
|
24
|
+
const INST_FORMS_REGISTRY = new WeakMap();
|
|
25
|
+
let isSetup = false;
|
|
26
|
+
|
|
27
|
+
export function createForm(options = {}) {
|
|
28
|
+
const inst = getCurrentInstance();
|
|
29
|
+
if (!inst) return _createForm(options);
|
|
30
|
+
|
|
31
|
+
if (!INST_FORMS_REGISTRY.has(inst)) {
|
|
32
|
+
INST_FORMS_REGISTRY.set(inst, new Map());
|
|
16
33
|
}
|
|
17
|
-
|
|
18
|
-
};
|
|
34
|
+
const forms = INST_FORMS_REGISTRY.get(inst);
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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;
|
|
36
|
+
const key = options.key || JSON.stringify({
|
|
37
|
+
initialValues: options.initialValues
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (forms.has(key)) {
|
|
41
|
+
const form = forms.get(key);
|
|
42
|
+
if (form._updateConfig) {
|
|
43
|
+
form._updateConfig(options.mode, options.reValidateMode);
|
|
38
44
|
}
|
|
39
|
-
return
|
|
45
|
+
return form;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
isSetup = true;
|
|
49
|
+
try {
|
|
50
|
+
const form = _createForm(options);
|
|
51
|
+
forms.set(key, form);
|
|
52
|
+
return form;
|
|
53
|
+
} finally {
|
|
54
|
+
isSetup = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _createForm(options = {}) {
|
|
59
|
+
const initialValuesSig = signal(clone(options.initialValues || {}));
|
|
60
|
+
const valuesSig = signal(clone(initialValuesSig.value));
|
|
61
|
+
const errorsSig = signal({});
|
|
62
|
+
const touchedSig = signal({});
|
|
63
|
+
const changedSig = signal({});
|
|
64
|
+
const isSubmittingSig = signal(false);
|
|
65
|
+
const isValidatingSig = signal(false);
|
|
66
|
+
const isSubmittedSig = signal(false);
|
|
67
|
+
const submitCountSig = signal(0);
|
|
68
|
+
const modeSig = signal(options.mode);
|
|
69
|
+
const reValidateModeSig = signal(options.reValidateMode);
|
|
70
|
+
|
|
71
|
+
const signalsCache = new Map();
|
|
72
|
+
|
|
73
|
+
function getSignal(sigType, path, defaultValue) {
|
|
74
|
+
const key = `${sigType}:${path}`;
|
|
75
|
+
if (signalsCache.has(key)) return signalsCache.get(key);
|
|
76
|
+
const initialVal = get(
|
|
77
|
+
sigType === "v" ? valuesSig.peek() : sigType === "e" ? errorsSig.peek() : sigType === "t" ? touchedSig.peek() : changedSig.peek(),
|
|
78
|
+
path
|
|
79
|
+
);
|
|
80
|
+
const sig = signal(initialVal ?? defaultValue);
|
|
81
|
+
signalsCache.set(key, sig);
|
|
82
|
+
return sig;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function notifySignals(sigType) {
|
|
86
|
+
const prefix = `${sigType}:`;
|
|
87
|
+
const fullData = sigType === "v" ? valuesSig.peek() : sigType === "e" ? errorsSig.peek() : sigType === "t" ? touchedSig.peek() : changedSig.peek();
|
|
88
|
+
|
|
89
|
+
for (const [cacheKey, sig] of signalsCache) {
|
|
90
|
+
if (!cacheKey.startsWith(prefix)) continue;
|
|
91
|
+
const path = cacheKey.slice(prefix.length);
|
|
92
|
+
const newVal = path === "" ? fullData : get(fullData, path);
|
|
93
|
+
sig.value = newVal;
|
|
50
94
|
}
|
|
51
|
-
}
|
|
95
|
+
}
|
|
52
96
|
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
const getMode = () => {
|
|
98
|
+
const m = modeSig.value;
|
|
99
|
+
return (m instanceof Object && 'value' in m ? m.value : m) || "onBlur";
|
|
100
|
+
};
|
|
101
|
+
const getReValidateMode = () => {
|
|
102
|
+
const rm = reValidateModeSig.value;
|
|
103
|
+
return (rm instanceof Object && 'value' in rm ? rm.value : rm) || "onChange";
|
|
104
|
+
};
|
|
105
|
+
const activeValidator = options.validator || options.validate;
|
|
55
106
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
107
|
+
function hasErrors(obj) {
|
|
108
|
+
if (!obj || typeof obj !== "object") return false;
|
|
109
|
+
return Object.values(obj).some((v) => {
|
|
110
|
+
if (v === undefined || v === null) return false;
|
|
111
|
+
if (typeof v === "object") return hasErrors(v);
|
|
112
|
+
return true;
|
|
61
113
|
});
|
|
62
|
-
|
|
63
|
-
});
|
|
114
|
+
}
|
|
64
115
|
|
|
65
|
-
const
|
|
116
|
+
const isValidSig = computed(() => !hasErrors(errorsSig.value));
|
|
117
|
+
const isTouchedSig = computed(() => hasErrors(touchedSig.value));
|
|
118
|
+
const isDirtySig = computed(() => !isEql(valuesSig.value, initialValuesSig.value));
|
|
66
119
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
120
|
+
function updateErrors(results, fieldPath) {
|
|
121
|
+
const currentErrors = results && results.errors ? results.errors : results || {};
|
|
122
|
+
if (fieldPath) {
|
|
123
|
+
const fieldError = get(currentErrors, fieldPath);
|
|
124
|
+
const newErrors = clone(errorsSig.value || {});
|
|
125
|
+
if (fieldError) set(newErrors, fieldPath, fieldError);
|
|
126
|
+
else unset(newErrors, fieldPath);
|
|
127
|
+
errorsSig.value = newErrors;
|
|
128
|
+
} else {
|
|
129
|
+
errorsSig.value = currentErrors || {};
|
|
75
130
|
}
|
|
76
|
-
|
|
131
|
+
notifySignals("e");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function runValidation(fieldPath) {
|
|
135
|
+
if (!activeValidator) return;
|
|
136
|
+
|
|
137
|
+
untracked(() => {
|
|
138
|
+
const results = activeValidator(valuesSig.peek());
|
|
139
|
+
if (results instanceof Promise) {
|
|
140
|
+
isValidatingSig.value = true;
|
|
141
|
+
return results.then((res) => updateErrors(res, fieldPath)).finally(() => isValidatingSig.value = false);
|
|
142
|
+
}
|
|
143
|
+
updateErrors(results, fieldPath);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
77
146
|
|
|
78
|
-
function
|
|
79
|
-
|
|
147
|
+
function updateValue(path, val) {
|
|
148
|
+
batch(() => {
|
|
149
|
+
const newState = clone(valuesSig.peek());
|
|
150
|
+
if (path === "") {
|
|
151
|
+
valuesSig.value = clone(val);
|
|
152
|
+
} else {
|
|
153
|
+
set(newState, path, val);
|
|
154
|
+
valuesSig.value = newState;
|
|
155
|
+
}
|
|
156
|
+
notifySignals("v");
|
|
157
|
+
|
|
158
|
+
const isFieldChanged = !isEql(val, path === "" ? initialValuesSig.peek() : get(initialValuesSig.peek(), path));
|
|
159
|
+
const newChanged = clone(changedSig.peek() || {});
|
|
160
|
+
if (path === "") {
|
|
161
|
+
// Root changed?
|
|
162
|
+
} else {
|
|
163
|
+
set(newChanged, path, isFieldChanged);
|
|
164
|
+
changedSig.value = newChanged;
|
|
165
|
+
}
|
|
166
|
+
notifySignals("c");
|
|
167
|
+
|
|
168
|
+
const mode = getMode();
|
|
169
|
+
const hasAnyErrors = hasErrors(errorsSig.peek());
|
|
170
|
+
const reValidateMode = getReValidateMode();
|
|
171
|
+
|
|
172
|
+
if (mode === "onChange") runValidation(path);
|
|
173
|
+
else if (hasAnyErrors && reValidateMode === "onChange") runValidation(path);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const register = (path) => {
|
|
177
|
+
const valSig = getSignal("v", path);
|
|
178
|
+
const errSig = getSignal("e", path);
|
|
179
|
+
const touchSig = getSignal("t", path, false);
|
|
180
|
+
|
|
80
181
|
return {
|
|
81
|
-
name,
|
|
82
|
-
value:
|
|
182
|
+
name: path,
|
|
183
|
+
value: valSig,
|
|
184
|
+
checked: valSig,
|
|
185
|
+
error: errSig,
|
|
186
|
+
isTouched: touchSig,
|
|
83
187
|
oninput: (e) => {
|
|
84
|
-
|
|
188
|
+
const val = e.target.type === "checkbox" ? e.target.checked : e.target.value;
|
|
189
|
+
updateValue(path, val);
|
|
85
190
|
},
|
|
86
191
|
onblur: () => {
|
|
87
|
-
|
|
88
|
-
|
|
192
|
+
const newTouched = clone(touchedSig.value || {});
|
|
193
|
+
set(newTouched, path, true);
|
|
194
|
+
touchedSig.value = newTouched;
|
|
195
|
+
notifySignals("t", path, touchedSig.value);
|
|
196
|
+
const mode = getMode();
|
|
197
|
+
if (mode === "onBlur") runValidation(path);
|
|
198
|
+
},
|
|
89
199
|
};
|
|
90
|
-
}
|
|
200
|
+
};
|
|
91
201
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
});
|
|
202
|
+
function markAllTouched(obj) {
|
|
203
|
+
if (!obj || typeof obj !== "object") return true;
|
|
204
|
+
const res = Array.isArray(obj) ? [] : {};
|
|
205
|
+
Object.keys(obj).forEach((key) => {
|
|
206
|
+
res[key] = markAllTouched(obj[key]);
|
|
207
|
+
});
|
|
208
|
+
return res;
|
|
209
|
+
}
|
|
102
210
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
211
|
+
const handleSubmit = (fn) => async (e) => {
|
|
212
|
+
if (e && e.preventDefault) e.preventDefault();
|
|
213
|
+
touchedSig.value = markAllTouched(valuesSig.value);
|
|
214
|
+
notifySignals("t");
|
|
215
|
+
isSubmittingSig.value = true;
|
|
216
|
+
submitCountSig.value++;
|
|
217
|
+
await runValidation();
|
|
218
|
+
if (isValidSig.value) {
|
|
219
|
+
try {
|
|
220
|
+
await fn(valuesSig.value);
|
|
221
|
+
isSubmittedSig.value = true;
|
|
222
|
+
} finally {
|
|
223
|
+
isSubmittingSig.value = false;
|
|
109
224
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
onSubmit(currentValues);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
}
|
|
225
|
+
} else {
|
|
226
|
+
isSubmittingSig.value = false;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
118
229
|
|
|
119
|
-
|
|
230
|
+
const reset = (newValues) => {
|
|
120
231
|
batch(() => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
232
|
+
if (newValues !== undefined) initialValuesSig.value = clone(newValues);
|
|
233
|
+
valuesSig.value = clone(initialValuesSig.value);
|
|
234
|
+
errorsSig.value = {};
|
|
235
|
+
touchedSig.value = {};
|
|
236
|
+
changedSig.value = {};
|
|
237
|
+
isSubmittedSig.value = false;
|
|
238
|
+
notifySignals("v");
|
|
239
|
+
notifySignals("e");
|
|
240
|
+
notifySignals("t");
|
|
241
|
+
notifySignals("c");
|
|
127
242
|
});
|
|
128
|
-
}
|
|
243
|
+
};
|
|
129
244
|
|
|
130
|
-
|
|
131
|
-
return computed(() => values.value[path] || []);
|
|
132
|
-
}
|
|
245
|
+
const proxyCache = new Map();
|
|
133
246
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
247
|
+
function createDeepProxy(sig, path = [], defaultValue = undefined) {
|
|
248
|
+
|
|
249
|
+
const pathStr = path.join(".");
|
|
250
|
+
// Use signal identity in cache key to avoid collisions between values/errors/etc.
|
|
251
|
+
const sigKey = sig === valuesSig ? "v" : sig === errorsSig ? "e" : sig === touchedSig ? "t" : "c";
|
|
252
|
+
const cacheKey = `${sigKey}:${pathStr}:${defaultValue}`;
|
|
253
|
+
if (proxyCache.has(cacheKey)) return proxyCache.get(cacheKey);
|
|
254
|
+
|
|
255
|
+
const val = untracked(() => (path.length === 0 ? sig.value : get(sig.value, pathStr)));
|
|
256
|
+
|
|
257
|
+
const proxy = new Proxy(val || {}, {
|
|
258
|
+
get(_, prop) {
|
|
259
|
+
const latestVal = untracked(() => (path.length === 0 ? sig.value : get(sig.value, pathStr)));
|
|
260
|
+
|
|
261
|
+
if (prop === Symbol.toStringTag) {
|
|
262
|
+
return Array.isArray(latestVal) ? "Array" : "Object";
|
|
263
|
+
}
|
|
264
|
+
if (prop === "toJSON") {
|
|
265
|
+
return () => (path.length === 0 ? sig.value : get(sig.value, pathStr));
|
|
266
|
+
}
|
|
267
|
+
if (prop === Symbol.iterator) {
|
|
268
|
+
return latestVal[Symbol.iterator].bind(latestVal);
|
|
269
|
+
}
|
|
270
|
+
if (typeof prop === "symbol") return undefined;
|
|
271
|
+
if (["__proto__", "constructor", "prototype"].includes(prop)) return undefined;
|
|
272
|
+
|
|
273
|
+
const currentPathStr = pathStr ? `${pathStr}.${prop}` : prop;
|
|
274
|
+
const value = latestVal ? latestVal[prop] : undefined;
|
|
275
|
+
|
|
276
|
+
if (Array.isArray(latestVal) && ["push", "pop", "shift", "unshift", "splice", "reverse", "sort"].includes(prop)) {
|
|
277
|
+
return (...args) => {
|
|
278
|
+
const nextArr = [...latestVal];
|
|
279
|
+
const result = nextArr[prop](...args);
|
|
280
|
+
updateValue(pathStr, nextArr);
|
|
281
|
+
return result;
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (typeof value === "function") {
|
|
286
|
+
return value.bind(latestVal);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Subscribe to path-specific signal
|
|
290
|
+
const subSig = getSignal(sigKey, currentPathStr, value ?? defaultValue);
|
|
291
|
+
if (!isSetup) subSig.value;
|
|
292
|
+
|
|
293
|
+
if (value && typeof value === "object") {
|
|
294
|
+
return createDeepProxy(sig, [...path, prop], defaultValue);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return value ?? defaultValue;
|
|
298
|
+
},
|
|
299
|
+
set(_, prop, val) {
|
|
300
|
+
|
|
301
|
+
const currentPathStr = [...path, prop].join(".");
|
|
302
|
+
if (sig === valuesSig) updateValue(currentPathStr, val);
|
|
303
|
+
else {
|
|
304
|
+
const newState = clone(sig.value);
|
|
305
|
+
set(newState, currentPathStr, val);
|
|
306
|
+
sig.value = newState;
|
|
307
|
+
}
|
|
144
308
|
return true;
|
|
145
309
|
},
|
|
146
|
-
ownKeys
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
310
|
+
ownKeys() {
|
|
311
|
+
const val = untracked(() => (path.length === 0 ? sig.value : get(sig.value, pathStr)));
|
|
312
|
+
if (!isSetup) {
|
|
313
|
+
const subSig = getSignal(sigKey, pathStr, val);
|
|
314
|
+
subSig.value;
|
|
315
|
+
}
|
|
316
|
+
const keys = Object.keys(val || {});
|
|
317
|
+
if (Array.isArray(val) && !keys.includes("length")) {
|
|
318
|
+
keys.push("length");
|
|
319
|
+
}
|
|
320
|
+
return keys;
|
|
321
|
+
},
|
|
322
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
323
|
+
const latestVal = untracked(() => (path.length === 0 ? sig.value : get(sig.value, pathStr)));
|
|
324
|
+
if (latestVal && typeof latestVal === "object" && prop in latestVal) {
|
|
325
|
+
return Object.getOwnPropertyDescriptor(latestVal, prop);
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
155
328
|
}
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
proxyCache.set(cacheKey, proxy);
|
|
332
|
+
return proxy;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const form = {
|
|
336
|
+
register,
|
|
337
|
+
handleSubmit,
|
|
338
|
+
reset,
|
|
339
|
+
_updateConfig(newMode, newReValidateMode) {
|
|
340
|
+
batch(() => {
|
|
341
|
+
modeSig.value = newMode;
|
|
342
|
+
reValidateModeSig.value = newReValidateMode;
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
_notifySignals: notifySignals,
|
|
346
|
+
get values() { return createDeepProxy(valuesSig); },
|
|
347
|
+
get errors() { return createDeepProxy(errorsSig); },
|
|
348
|
+
get touched() { return createDeepProxy(touchedSig, [], false); },
|
|
349
|
+
get changed() { return createDeepProxy(changedSig, [], false); },
|
|
350
|
+
_signals: {
|
|
351
|
+
values: valuesSig,
|
|
352
|
+
errors: errorsSig,
|
|
353
|
+
touched: touchedSig,
|
|
354
|
+
changed: changedSig,
|
|
355
|
+
initialValues: initialValuesSig,
|
|
356
|
+
}
|
|
162
357
|
};
|
|
358
|
+
|
|
359
|
+
const getSafe = (sig) => (isSetup ? untracked(() => sig.value) : sig.value);
|
|
360
|
+
|
|
361
|
+
Object.defineProperties(form, {
|
|
362
|
+
isValid: { get: () => getSafe(isValidSig), enumerable: true },
|
|
363
|
+
isChanged: { get: () => getSafe(isDirtySig), enumerable: true },
|
|
364
|
+
isTouched: { get: () => getSafe(isTouchedSig), enumerable: true },
|
|
365
|
+
isSubmitting: { get: () => getSafe(isSubmittingSig), enumerable: true },
|
|
366
|
+
isValidating: { get: () => getSafe(isValidatingSig), enumerable: true },
|
|
367
|
+
isSubmitted: { get: () => getSafe(isSubmittedSig), enumerable: true },
|
|
368
|
+
submitCount: { get: () => getSafe(submitCountSig), enumerable: true },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Initial validation (only if onChange mode)
|
|
372
|
+
if (getMode() === "onChange") {
|
|
373
|
+
runValidation();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return form;
|
|
163
377
|
}
|