what-core 0.5.4 → 0.5.5
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/README.md +1 -1
- package/package.json +5 -5
- package/src/animation.js +3 -1
- package/src/data.js +15 -5
- package/src/dom.js +31 -5
- package/src/form.js +49 -21
- package/src/reactive.js +3 -3
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ mount(h(Counter), '#app');
|
|
|
111
111
|
## Links
|
|
112
112
|
|
|
113
113
|
- [Documentation](https://whatfw.com)
|
|
114
|
-
- [GitHub](https://github.com/
|
|
114
|
+
- [GitHub](https://github.com/CelsianJs/whatfw)
|
|
115
115
|
- [Benchmarks](https://benchmarks.whatfw.com)
|
|
116
116
|
|
|
117
117
|
## License
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "What Framework - The closest framework to vanilla JS",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "src/index.js",
|
|
7
7
|
"module": "src/index.js",
|
|
8
8
|
"types": "index.d.ts",
|
|
9
9
|
"exports": {
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"lightweight",
|
|
45
45
|
"vdom"
|
|
46
46
|
],
|
|
47
|
-
"author": "",
|
|
47
|
+
"author": "ZVN DEV (https://zvndev.com)",
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"repository": {
|
|
50
50
|
"type": "git",
|
|
51
|
-
"url": "https://github.com/
|
|
51
|
+
"url": "https://github.com/CelsianJs/whatfw"
|
|
52
52
|
},
|
|
53
53
|
"bugs": {
|
|
54
|
-
"url": "https://github.com/
|
|
54
|
+
"url": "https://github.com/CelsianJs/whatfw/issues"
|
|
55
55
|
},
|
|
56
56
|
"homepage": "https://whatfw.com"
|
|
57
57
|
}
|
package/src/animation.js
CHANGED
|
@@ -77,7 +77,9 @@ export function spring(initialValue, options = {}) {
|
|
|
77
77
|
|
|
78
78
|
function set(newTarget) {
|
|
79
79
|
target.set(newTarget);
|
|
80
|
-
|
|
80
|
+
// Use rafId check instead of isAnimating signal — signal may not have flushed
|
|
81
|
+
// after a synchronous stop() call, causing duplicate animation frames
|
|
82
|
+
if (rafId === null) {
|
|
81
83
|
isAnimating.set(true);
|
|
82
84
|
lastTime = null;
|
|
83
85
|
rafId = requestAnimationFrame(tick);
|
package/src/data.js
CHANGED
|
@@ -68,7 +68,7 @@ function subscribeToKey(key, revalidateFn) {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const inFlightRequests = new Map();
|
|
71
|
+
const inFlightRequests = new Map(); // key -> { promise, timestamp, refCount }
|
|
72
72
|
const lastFetchTimestamps = new Map(); // key -> timestamp of last completed fetch
|
|
73
73
|
|
|
74
74
|
// Create an effect scoped to the current component's lifecycle.
|
|
@@ -200,6 +200,7 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
200
200
|
if (inFlightRequests.has(key)) {
|
|
201
201
|
const existing = inFlightRequests.get(key);
|
|
202
202
|
if (now - existing.timestamp < dedupingInterval) {
|
|
203
|
+
existing.refCount++;
|
|
203
204
|
return existing.promise;
|
|
204
205
|
}
|
|
205
206
|
}
|
|
@@ -210,15 +211,20 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
210
211
|
return cacheS.peek();
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
// Abort previous request
|
|
214
|
-
if (abortController)
|
|
214
|
+
// Abort previous request only if no other subscribers are using it
|
|
215
|
+
if (abortController) {
|
|
216
|
+
const existing = inFlightRequests.get(key);
|
|
217
|
+
if (!existing || existing.refCount <= 1) {
|
|
218
|
+
abortController.abort();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
215
221
|
abortController = new AbortController();
|
|
216
222
|
const { signal: abortSignal } = abortController;
|
|
217
223
|
|
|
218
224
|
isValidating.set(true);
|
|
219
225
|
|
|
220
226
|
const promise = fetcher(key, { signal: abortSignal });
|
|
221
|
-
inFlightRequests.set(key, { promise, timestamp: now });
|
|
227
|
+
inFlightRequests.set(key, { promise, timestamp: now, refCount: 1 });
|
|
222
228
|
|
|
223
229
|
try {
|
|
224
230
|
const result = await promise;
|
|
@@ -238,7 +244,11 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
238
244
|
throw e;
|
|
239
245
|
} finally {
|
|
240
246
|
if (!abortSignal.aborted) isValidating.set(false);
|
|
241
|
-
inFlightRequests.
|
|
247
|
+
const flight = inFlightRequests.get(key);
|
|
248
|
+
if (flight) {
|
|
249
|
+
flight.refCount--;
|
|
250
|
+
if (flight.refCount <= 0) inFlightRequests.delete(key);
|
|
251
|
+
}
|
|
242
252
|
}
|
|
243
253
|
}
|
|
244
254
|
|
package/src/dom.js
CHANGED
|
@@ -78,17 +78,18 @@ function disposeComponent(ctx) {
|
|
|
78
78
|
if (ctx.disposed) return;
|
|
79
79
|
ctx.disposed = true;
|
|
80
80
|
|
|
81
|
-
// Run useEffect cleanup functions
|
|
82
|
-
for (
|
|
81
|
+
// Run useEffect cleanup functions in reverse order (last effect first, matching React)
|
|
82
|
+
for (let i = ctx.hooks.length - 1; i >= 0; i--) {
|
|
83
|
+
const hook = ctx.hooks[i];
|
|
83
84
|
if (hook && typeof hook === 'object' && 'cleanup' in hook && hook.cleanup) {
|
|
84
85
|
try { hook.cleanup(); } catch (e) { console.error('[what] cleanup error:', e); }
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
// Run onCleanup callbacks
|
|
89
|
+
// Run onCleanup callbacks in reverse order (last registered first)
|
|
89
90
|
if (ctx._cleanupCallbacks) {
|
|
90
|
-
for (
|
|
91
|
-
try {
|
|
91
|
+
for (let i = ctx._cleanupCallbacks.length - 1; i >= 0; i--) {
|
|
92
|
+
try { ctx._cleanupCallbacks[i](); } catch (e) { console.error('[what] onCleanup error:', e); }
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -111,6 +112,12 @@ export function disposeTree(node) {
|
|
|
111
112
|
if (node._dispose) {
|
|
112
113
|
try { node._dispose(); } catch (e) { /* already disposed */ }
|
|
113
114
|
}
|
|
115
|
+
// Dispose reactive prop effects (value: () => ..., class: () => ..., etc.)
|
|
116
|
+
if (node._propEffects) {
|
|
117
|
+
for (const key in node._propEffects) {
|
|
118
|
+
try { node._propEffects[key](); } catch (e) { /* already disposed */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
114
121
|
if (node.childNodes) {
|
|
115
122
|
for (const child of node.childNodes) {
|
|
116
123
|
disposeTree(child);
|
|
@@ -996,6 +1003,25 @@ function applyProps(el, newProps, oldProps, isSvg) {
|
|
|
996
1003
|
}
|
|
997
1004
|
|
|
998
1005
|
function setProp(el, key, value, isSvg) {
|
|
1006
|
+
// Reactive function props — wrap in effect() for fine-grained updates.
|
|
1007
|
+
// Applies to any non-event prop where the value is a function, e.g.:
|
|
1008
|
+
// h('input', { value: () => name(), class: () => active() ? 'on' : 'off' })
|
|
1009
|
+
// The function is called inside an effect, so signal reads create subscriptions.
|
|
1010
|
+
// When signals change, the prop is re-applied automatically.
|
|
1011
|
+
if (typeof value === 'function' && !(key.startsWith('on') && key.length > 2) && key !== 'ref') {
|
|
1012
|
+
// Store dispose functions on the element for cleanup
|
|
1013
|
+
if (!el._propEffects) el._propEffects = {};
|
|
1014
|
+
// Dispose previous effect for this prop if re-applying
|
|
1015
|
+
if (el._propEffects[key]) {
|
|
1016
|
+
try { el._propEffects[key](); } catch (e) { /* already disposed */ }
|
|
1017
|
+
}
|
|
1018
|
+
el._propEffects[key] = effect(() => {
|
|
1019
|
+
const resolved = value();
|
|
1020
|
+
setProp(el, key, resolved, isSvg);
|
|
1021
|
+
});
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
999
1025
|
// Event handlers: onClick -> click, onFocusCapture -> focus (capture phase)
|
|
1000
1026
|
// Wrap in untrack so signal reads in handlers don't create subscriptions
|
|
1001
1027
|
if (key.startsWith('on') && key.length > 2) {
|
package/src/form.js
CHANGED
|
@@ -62,6 +62,7 @@ function createFormController(options = {}) {
|
|
|
62
62
|
const isDirty = signal(false);
|
|
63
63
|
const isSubmitting = signal(false);
|
|
64
64
|
const isSubmitted = signal(false);
|
|
65
|
+
const isValidating = signal(false);
|
|
65
66
|
const submitCount = signal(0);
|
|
66
67
|
|
|
67
68
|
// Helper: get all current values as a plain object.
|
|
@@ -128,34 +129,42 @@ function createFormController(options = {}) {
|
|
|
128
129
|
async function validate(fieldName) {
|
|
129
130
|
if (!resolver) return true;
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
isValidating.set(true);
|
|
133
|
+
try {
|
|
134
|
+
const result = await resolver(getAllValues(false));
|
|
135
|
+
const nextErrors = result?.errors || {};
|
|
136
|
+
|
|
137
|
+
if (fieldName) {
|
|
138
|
+
const nextError = nextErrors[fieldName] ?? null;
|
|
139
|
+
setFieldError(fieldName, nextError);
|
|
140
|
+
return !nextError;
|
|
141
|
+
} else {
|
|
142
|
+
replaceAllErrors(nextErrors);
|
|
143
|
+
return Object.keys(nextErrors).length === 0;
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
isValidating.set(false);
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
// Register a field — only subscribes to THIS field's signal
|
|
145
151
|
function register(name, options = {}) {
|
|
146
152
|
const fieldSig = getFieldSignal(name);
|
|
147
|
-
|
|
148
|
-
name,
|
|
149
|
-
// Use getter so value is always fresh, even if register result is cached
|
|
150
|
-
get value() { return fieldSig(); },
|
|
151
|
-
onInput: (e) => {
|
|
152
|
-
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
153
|
-
setValue(name, value);
|
|
153
|
+
const isCheckbox = options.type === 'checkbox' || options.type === 'radio';
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
const handler = (e) => {
|
|
156
|
+
const value = (e.target.type === 'checkbox' || e.target.type === 'radio')
|
|
157
|
+
? e.target.checked
|
|
158
|
+
: e.target.value;
|
|
159
|
+
setValue(name, value);
|
|
160
|
+
|
|
161
|
+
if (mode === 'onChange' || (isSubmitted.peek() && reValidateMode === 'onChange')) {
|
|
162
|
+
validate(name);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const result = {
|
|
167
|
+
name,
|
|
159
168
|
onBlur: () => {
|
|
160
169
|
getTouchedSignal(name).set(true);
|
|
161
170
|
|
|
@@ -166,6 +175,24 @@ function createFormController(options = {}) {
|
|
|
166
175
|
onFocus: () => {},
|
|
167
176
|
ref: options.ref,
|
|
168
177
|
};
|
|
178
|
+
|
|
179
|
+
if (isCheckbox) {
|
|
180
|
+
// Checkbox/radio: use checked prop + onchange event
|
|
181
|
+
Object.defineProperty(result, 'checked', {
|
|
182
|
+
get() { return !!fieldSig(); },
|
|
183
|
+
enumerable: true,
|
|
184
|
+
});
|
|
185
|
+
result.onchange = handler;
|
|
186
|
+
} else {
|
|
187
|
+
// Text/select/textarea: use value prop + oninput event
|
|
188
|
+
Object.defineProperty(result, 'value', {
|
|
189
|
+
get() { return fieldSig(); },
|
|
190
|
+
enumerable: true,
|
|
191
|
+
});
|
|
192
|
+
result.oninput = handler;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
// Set single field value — only triggers re-render for components reading this field
|
|
@@ -273,6 +300,7 @@ function createFormController(options = {}) {
|
|
|
273
300
|
},
|
|
274
301
|
isDirty: () => isDirty(),
|
|
275
302
|
isValid,
|
|
303
|
+
isValidating: () => isValidating(),
|
|
276
304
|
isSubmitting: () => isSubmitting(),
|
|
277
305
|
isSubmitted: () => isSubmitted(),
|
|
278
306
|
submitCount: () => submitCount(),
|
package/src/reactive.js
CHANGED
|
@@ -279,7 +279,7 @@ function scheduleMicrotask() {
|
|
|
279
279
|
|
|
280
280
|
function flush() {
|
|
281
281
|
let iterations = 0;
|
|
282
|
-
while (pendingEffects.length > 0 && iterations <
|
|
282
|
+
while (pendingEffects.length > 0 && iterations < 25) {
|
|
283
283
|
const batch = pendingEffects;
|
|
284
284
|
pendingEffects = [];
|
|
285
285
|
for (let i = 0; i < batch.length; i++) {
|
|
@@ -289,12 +289,12 @@ function flush() {
|
|
|
289
289
|
}
|
|
290
290
|
iterations++;
|
|
291
291
|
}
|
|
292
|
-
if (iterations >=
|
|
292
|
+
if (iterations >= 25) {
|
|
293
293
|
if (__DEV__) {
|
|
294
294
|
const remaining = pendingEffects.slice(0, 3);
|
|
295
295
|
const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');
|
|
296
296
|
console.warn(
|
|
297
|
-
`[what] Possible infinite effect loop detected (
|
|
297
|
+
`[what] Possible infinite effect loop detected (25 iterations). ` +
|
|
298
298
|
`Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +
|
|
299
299
|
`Use untrack() to read signals without subscribing. ` +
|
|
300
300
|
`Looping effects: ${effectNames.join(', ')}`
|