what-core 0.2.0 → 0.4.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/dist/a11y.js +22 -7
- package/dist/animation.js +20 -3
- package/dist/components.js +61 -23
- package/dist/data.js +272 -68
- package/dist/dom.js +325 -89
- package/dist/form.js +112 -44
- package/dist/helpers.js +73 -10
- package/dist/hooks.js +75 -22
- package/dist/index.js +6 -2
- package/dist/reactive.js +202 -28
- package/dist/render.js +716 -0
- package/dist/scheduler.js +10 -5
- package/dist/store.js +19 -8
- package/package.json +4 -1
- package/src/a11y.js +22 -7
- package/src/animation.js +20 -3
- package/src/components.js +61 -23
- package/src/data.js +272 -68
- package/src/dom.js +325 -89
- package/src/form.js +112 -44
- package/src/helpers.js +73 -10
- package/src/hooks.js +75 -22
- package/src/index.js +6 -2
- package/src/reactive.js +202 -28
- package/src/render.js +716 -0
- package/src/scheduler.js +10 -5
- package/src/store.js +19 -8
package/dist/form.js
CHANGED
|
@@ -15,23 +15,70 @@ export function useForm(options = {}) {
|
|
|
15
15
|
resolver,
|
|
16
16
|
} = options;
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
18
|
+
// Per-field signals for granular reactivity (avoids full-form re-renders on each keystroke)
|
|
19
|
+
const fieldSignals = {};
|
|
20
|
+
const errorSignals = {};
|
|
21
|
+
const touchedSignals = {};
|
|
22
|
+
|
|
23
|
+
function getFieldSignal(name) {
|
|
24
|
+
if (!fieldSignals[name]) {
|
|
25
|
+
fieldSignals[name] = signal(defaultValues[name] ?? '');
|
|
26
|
+
}
|
|
27
|
+
return fieldSignals[name];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getErrorSignal(name) {
|
|
31
|
+
if (!errorSignals[name]) {
|
|
32
|
+
errorSignals[name] = signal(null);
|
|
33
|
+
}
|
|
34
|
+
return errorSignals[name];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getTouchedSignal(name) {
|
|
38
|
+
if (!touchedSignals[name]) {
|
|
39
|
+
touchedSignals[name] = signal(false);
|
|
40
|
+
}
|
|
41
|
+
return touchedSignals[name];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Aggregate signals for bulk operations
|
|
22
45
|
const isDirty = signal(false);
|
|
23
46
|
const isSubmitting = signal(false);
|
|
24
47
|
const isSubmitted = signal(false);
|
|
25
48
|
const submitCount = signal(0);
|
|
26
49
|
|
|
50
|
+
// Helper: get all current values as a plain object
|
|
51
|
+
function getAllValues() {
|
|
52
|
+
const result = { ...defaultValues };
|
|
53
|
+
for (const [name, sig] of Object.entries(fieldSignals)) {
|
|
54
|
+
result[name] = sig.peek();
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Helper: get all current errors as a plain object
|
|
60
|
+
function getAllErrors() {
|
|
61
|
+
const result = {};
|
|
62
|
+
for (const [name, sig] of Object.entries(errorSignals)) {
|
|
63
|
+
const err = sig.peek();
|
|
64
|
+
if (err) result[name] = err;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
// Computed states
|
|
28
|
-
const isValid = computed(() =>
|
|
70
|
+
const isValid = computed(() => {
|
|
71
|
+
for (const sig of Object.values(errorSignals)) {
|
|
72
|
+
if (sig()) return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
|
|
29
77
|
const dirtyFields = computed(() => {
|
|
30
78
|
const dirty = {};
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
dirty[key] = true;
|
|
79
|
+
for (const [name, sig] of Object.entries(fieldSignals)) {
|
|
80
|
+
if (sig() !== (defaultValues[name] ?? '')) {
|
|
81
|
+
dirty[name] = true;
|
|
35
82
|
}
|
|
36
83
|
}
|
|
37
84
|
return dirty;
|
|
@@ -41,31 +88,41 @@ export function useForm(options = {}) {
|
|
|
41
88
|
async function validate(fieldName) {
|
|
42
89
|
if (!resolver) return true;
|
|
43
90
|
|
|
44
|
-
const result = await resolver(
|
|
91
|
+
const result = await resolver(getAllValues());
|
|
45
92
|
|
|
46
93
|
if (fieldName) {
|
|
47
|
-
// Validate single field
|
|
94
|
+
// Validate single field — only update that field's error signal
|
|
95
|
+
const errSig = getErrorSignal(fieldName);
|
|
48
96
|
if (result.errors[fieldName]) {
|
|
49
|
-
|
|
97
|
+
errSig.set(result.errors[fieldName]);
|
|
50
98
|
return false;
|
|
51
99
|
} else {
|
|
52
|
-
|
|
53
|
-
delete newErrors[fieldName];
|
|
54
|
-
errors.set(newErrors);
|
|
100
|
+
errSig.set(null);
|
|
55
101
|
return true;
|
|
56
102
|
}
|
|
57
103
|
} else {
|
|
58
104
|
// Validate all fields
|
|
59
|
-
|
|
105
|
+
batch(() => {
|
|
106
|
+
// Clear existing errors
|
|
107
|
+
for (const sig of Object.values(errorSignals)) {
|
|
108
|
+
sig.set(null);
|
|
109
|
+
}
|
|
110
|
+
// Set new errors
|
|
111
|
+
for (const [name, err] of Object.entries(result.errors || {})) {
|
|
112
|
+
getErrorSignal(name).set(err);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
60
115
|
return Object.keys(result.errors || {}).length === 0;
|
|
61
116
|
}
|
|
62
117
|
}
|
|
63
118
|
|
|
64
|
-
// Register a field
|
|
119
|
+
// Register a field — only subscribes to THIS field's signal
|
|
65
120
|
function register(name, options = {}) {
|
|
121
|
+
const fieldSig = getFieldSignal(name);
|
|
66
122
|
return {
|
|
67
123
|
name,
|
|
68
|
-
value
|
|
124
|
+
// Use getter so value is always fresh, even if register result is cached
|
|
125
|
+
get value() { return fieldSig(); },
|
|
69
126
|
onInput: (e) => {
|
|
70
127
|
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
71
128
|
setValue(name, value);
|
|
@@ -75,7 +132,7 @@ export function useForm(options = {}) {
|
|
|
75
132
|
}
|
|
76
133
|
},
|
|
77
134
|
onBlur: () => {
|
|
78
|
-
|
|
135
|
+
getTouchedSignal(name).set(true);
|
|
79
136
|
|
|
80
137
|
if (mode === 'onBlur' || (isSubmitted.peek() && reValidateMode === 'onBlur')) {
|
|
81
138
|
validate(name);
|
|
@@ -86,16 +143,12 @@ export function useForm(options = {}) {
|
|
|
86
143
|
};
|
|
87
144
|
}
|
|
88
145
|
|
|
89
|
-
// Set single field value
|
|
146
|
+
// Set single field value — only triggers re-render for components reading this field
|
|
90
147
|
function setValue(name, value, options = {}) {
|
|
91
148
|
const { shouldValidate = false, shouldDirty = true } = options;
|
|
92
149
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (shouldDirty) {
|
|
96
|
-
isDirty.set(true);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
150
|
+
getFieldSignal(name).set(value);
|
|
151
|
+
if (shouldDirty) isDirty.set(true);
|
|
99
152
|
|
|
100
153
|
if (shouldValidate) {
|
|
101
154
|
validate(name);
|
|
@@ -104,32 +157,40 @@ export function useForm(options = {}) {
|
|
|
104
157
|
|
|
105
158
|
// Get single field value
|
|
106
159
|
function getValue(name) {
|
|
107
|
-
return
|
|
160
|
+
return getFieldSignal(name)();
|
|
108
161
|
}
|
|
109
162
|
|
|
110
163
|
// Set error for a field
|
|
111
164
|
function setError(name, error) {
|
|
112
|
-
|
|
165
|
+
getErrorSignal(name).set(error);
|
|
113
166
|
}
|
|
114
167
|
|
|
115
168
|
// Clear error for a field
|
|
116
169
|
function clearError(name) {
|
|
117
|
-
|
|
118
|
-
delete newErrors[name];
|
|
119
|
-
errors.set(newErrors);
|
|
170
|
+
getErrorSignal(name).set(null);
|
|
120
171
|
}
|
|
121
172
|
|
|
122
173
|
// Clear all errors
|
|
123
174
|
function clearErrors() {
|
|
124
|
-
|
|
175
|
+
batch(() => {
|
|
176
|
+
for (const sig of Object.values(errorSignals)) {
|
|
177
|
+
sig.set(null);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
125
180
|
}
|
|
126
181
|
|
|
127
182
|
// Reset form
|
|
128
183
|
function reset(newValues = defaultValues) {
|
|
129
184
|
batch(() => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
for (const [name, sig] of Object.entries(fieldSignals)) {
|
|
186
|
+
sig.set(newValues[name] ?? '');
|
|
187
|
+
}
|
|
188
|
+
for (const sig of Object.values(errorSignals)) {
|
|
189
|
+
sig.set(null);
|
|
190
|
+
}
|
|
191
|
+
for (const sig of Object.values(touchedSignals)) {
|
|
192
|
+
sig.set(false);
|
|
193
|
+
}
|
|
133
194
|
isDirty.set(false);
|
|
134
195
|
isSubmitted.set(false);
|
|
135
196
|
});
|
|
@@ -147,21 +208,22 @@ export function useForm(options = {}) {
|
|
|
147
208
|
const isFormValid = await validate();
|
|
148
209
|
|
|
149
210
|
if (isFormValid) {
|
|
150
|
-
await onValid(
|
|
211
|
+
await onValid(getAllValues());
|
|
151
212
|
} else if (onInvalid) {
|
|
152
|
-
onInvalid(
|
|
213
|
+
onInvalid(getAllErrors());
|
|
153
214
|
}
|
|
154
215
|
|
|
155
216
|
isSubmitting.set(false);
|
|
156
217
|
};
|
|
157
218
|
}
|
|
158
219
|
|
|
159
|
-
// Watch a field
|
|
220
|
+
// Watch a field — returns a computed that subscribes only to this field
|
|
160
221
|
function watch(name) {
|
|
161
222
|
if (name) {
|
|
162
|
-
return computed(() =>
|
|
223
|
+
return computed(() => getFieldSignal(name)());
|
|
163
224
|
}
|
|
164
|
-
return
|
|
225
|
+
// Watch all: return a computed that reads all field signals
|
|
226
|
+
return computed(() => getAllValues());
|
|
165
227
|
}
|
|
166
228
|
|
|
167
229
|
return {
|
|
@@ -175,11 +237,17 @@ export function useForm(options = {}) {
|
|
|
175
237
|
reset,
|
|
176
238
|
watch,
|
|
177
239
|
validate,
|
|
178
|
-
// Form state
|
|
240
|
+
// Form state — uses getters for errors/touched to enable per-field granularity
|
|
179
241
|
formState: {
|
|
180
|
-
values
|
|
181
|
-
errors
|
|
182
|
-
touched
|
|
242
|
+
get values() { return getAllValues(); },
|
|
243
|
+
get errors() { return getAllErrors(); },
|
|
244
|
+
get touched() {
|
|
245
|
+
const result = {};
|
|
246
|
+
for (const [name, sig] of Object.entries(touchedSignals)) {
|
|
247
|
+
if (sig()) result[name] = true;
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
},
|
|
183
251
|
isDirty: () => isDirty(),
|
|
184
252
|
isValid,
|
|
185
253
|
isSubmitting: () => isSubmitting(),
|
package/dist/helpers.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
// What Framework - Helpers & Utilities
|
|
2
2
|
// Commonly needed patterns, zero overhead.
|
|
3
3
|
|
|
4
|
-
import { signal, effect, computed, batch } from './reactive.js';
|
|
4
|
+
import { signal, effect, computed, batch, __DEV__ } from './reactive.js';
|
|
5
5
|
|
|
6
|
-
// --- show(condition, vnode) ---
|
|
6
|
+
// --- show(condition, vnode) --- [DEPRECATED: use <Show> component instead]
|
|
7
7
|
// Conditional rendering. More readable than ternary.
|
|
8
|
+
let _showWarned = false;
|
|
8
9
|
export function show(condition, vnode, fallback = null) {
|
|
10
|
+
if (!_showWarned) {
|
|
11
|
+
_showWarned = true;
|
|
12
|
+
console.warn('[what] show() is deprecated. Use the <Show> component or ternary expressions instead.');
|
|
13
|
+
}
|
|
9
14
|
return condition ? vnode : fallback;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
// --- each(list, fn) ---
|
|
17
|
+
// --- each(list, fn) --- [DEPRECATED: use <For> component or .map() instead]
|
|
13
18
|
// Keyed list rendering. Optimized for reconciliation.
|
|
19
|
+
let _eachWarned = false;
|
|
14
20
|
export function each(list, fn, keyFn) {
|
|
21
|
+
if (!_eachWarned) {
|
|
22
|
+
_eachWarned = true;
|
|
23
|
+
console.warn('[what] each() is deprecated. Use the <For> component or Array.map() instead.');
|
|
24
|
+
}
|
|
15
25
|
if (!list || list.length === 0) return [];
|
|
16
26
|
return list.map((item, index) => {
|
|
17
27
|
const vnode = fn(item, index);
|
|
@@ -76,18 +86,31 @@ export function throttle(fn, ms) {
|
|
|
76
86
|
};
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
// Component context ref — injected by dom.js to avoid circular imports
|
|
90
|
+
let _getCurrentComponentRef = null;
|
|
91
|
+
export function _setComponentRef(fn) { _getCurrentComponentRef = fn; }
|
|
92
|
+
|
|
79
93
|
// --- useMediaQuery ---
|
|
80
|
-
// Reactive media query. Returns a signal.
|
|
94
|
+
// Reactive media query. Returns a signal. Cleans up listener on component unmount.
|
|
81
95
|
export function useMediaQuery(query) {
|
|
82
96
|
if (typeof window === 'undefined') return signal(false);
|
|
83
97
|
const mq = window.matchMedia(query);
|
|
84
98
|
const s = signal(mq.matches);
|
|
85
|
-
|
|
99
|
+
const handler = (e) => s.set(e.matches);
|
|
100
|
+
mq.addEventListener('change', handler);
|
|
101
|
+
|
|
102
|
+
// Register cleanup if inside a component context
|
|
103
|
+
const ctx = _getCurrentComponentRef?.();
|
|
104
|
+
if (ctx) {
|
|
105
|
+
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
106
|
+
ctx._cleanupCallbacks.push(() => mq.removeEventListener('change', handler));
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
return s;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
// --- useLocalStorage ---
|
|
90
|
-
// Signal synced with localStorage.
|
|
113
|
+
// Signal synced with localStorage. Cleans up listeners on component unmount.
|
|
91
114
|
export function useLocalStorage(key, initial) {
|
|
92
115
|
let stored;
|
|
93
116
|
try {
|
|
@@ -100,18 +123,34 @@ export function useLocalStorage(key, initial) {
|
|
|
100
123
|
const s = signal(stored);
|
|
101
124
|
|
|
102
125
|
// Sync to localStorage on changes
|
|
103
|
-
effect(() => {
|
|
126
|
+
const dispose = effect(() => {
|
|
104
127
|
try {
|
|
105
128
|
localStorage.setItem(key, JSON.stringify(s()));
|
|
106
|
-
} catch {
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (__DEV__) console.warn('[what] localStorage write failed (quota exceeded?):', e);
|
|
131
|
+
}
|
|
107
132
|
});
|
|
108
133
|
|
|
109
134
|
// Listen for changes from other tabs
|
|
135
|
+
let storageHandler = null;
|
|
110
136
|
if (typeof window !== 'undefined') {
|
|
111
|
-
|
|
137
|
+
storageHandler = (e) => {
|
|
112
138
|
if (e.key === key && e.newValue !== null) {
|
|
113
|
-
try { s.set(JSON.parse(e.newValue)); } catch {
|
|
139
|
+
try { s.set(JSON.parse(e.newValue)); } catch (err) {
|
|
140
|
+
if (__DEV__) console.warn('[what] localStorage parse failed:', err);
|
|
141
|
+
}
|
|
114
142
|
}
|
|
143
|
+
};
|
|
144
|
+
window.addEventListener('storage', storageHandler);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Register cleanup if inside a component context
|
|
148
|
+
const ctx = _getCurrentComponentRef?.();
|
|
149
|
+
if (ctx) {
|
|
150
|
+
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
151
|
+
ctx._cleanupCallbacks.push(() => {
|
|
152
|
+
dispose();
|
|
153
|
+
if (storageHandler) window.removeEventListener('storage', storageHandler);
|
|
115
154
|
});
|
|
116
155
|
}
|
|
117
156
|
|
|
@@ -131,6 +170,30 @@ export function Portal({ target, children }) {
|
|
|
131
170
|
return { tag: '__portal', props: { container }, children: Array.isArray(children) ? children : [children], _vnode: true };
|
|
132
171
|
}
|
|
133
172
|
|
|
173
|
+
// --- useClickOutside ---
|
|
174
|
+
// Detect clicks outside a ref'd element. Essential for dropdowns, modals, popovers.
|
|
175
|
+
export function useClickOutside(ref, handler) {
|
|
176
|
+
if (typeof document === 'undefined') return;
|
|
177
|
+
|
|
178
|
+
const listener = (e) => {
|
|
179
|
+
const el = ref.current || ref;
|
|
180
|
+
if (!el || el.contains(e.target)) return;
|
|
181
|
+
handler(e);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
document.addEventListener('mousedown', listener);
|
|
185
|
+
document.addEventListener('touchstart', listener);
|
|
186
|
+
|
|
187
|
+
const ctx = _getCurrentComponentRef?.();
|
|
188
|
+
if (ctx) {
|
|
189
|
+
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
190
|
+
ctx._cleanupCallbacks.push(() => {
|
|
191
|
+
document.removeEventListener('mousedown', listener);
|
|
192
|
+
document.removeEventListener('touchstart', listener);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
134
197
|
// --- Transition helper ---
|
|
135
198
|
// Animate elements in/out. Returns props to spread on the element.
|
|
136
199
|
export function transition(name, active) {
|
package/dist/hooks.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// What Framework - Hooks
|
|
2
2
|
// React-familiar hooks backed by signals. Zero overhead when deps don't change.
|
|
3
3
|
|
|
4
|
-
import { signal, computed, effect, batch, untrack } from './reactive.js';
|
|
4
|
+
import { signal, computed, effect, batch, untrack, __DEV__ } from './reactive.js';
|
|
5
5
|
import { getCurrentComponent } from './dom.js';
|
|
6
6
|
|
|
7
7
|
function getCtx() {
|
|
8
8
|
const ctx = getCurrentComponent();
|
|
9
|
-
if (!ctx)
|
|
9
|
+
if (!ctx) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
'[what] Hooks must be called inside a component function. ' +
|
|
12
|
+
'If you need reactive state outside a component, use signal() directly.'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
10
15
|
return ctx;
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -128,24 +133,56 @@ export function useRef(initial) {
|
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
// --- useContext ---
|
|
131
|
-
// Read from
|
|
136
|
+
// Read from the nearest Provider in the component tree, or the default value.
|
|
137
|
+
// Uses _parentCtx chain (persistent tree) instead of componentStack (runtime stack)
|
|
138
|
+
// so context works correctly in re-renders, effects, and event handlers.
|
|
132
139
|
|
|
133
140
|
export function useContext(context) {
|
|
134
|
-
|
|
141
|
+
// Walk up the _parentCtx chain to find the nearest provider
|
|
142
|
+
let ctx = getCurrentComponent();
|
|
143
|
+
if (__DEV__ && !ctx) {
|
|
144
|
+
console.warn(
|
|
145
|
+
`[what] useContext(${context?.displayName || 'Context'}) called outside of component render. ` +
|
|
146
|
+
'useContext must be called during component rendering, not inside effects or event handlers. ' +
|
|
147
|
+
'Store the context value in a variable during render and use that variable in your callback.'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
while (ctx) {
|
|
151
|
+
if (ctx._contextValues && ctx._contextValues.has(context)) {
|
|
152
|
+
const val = ctx._contextValues.get(context);
|
|
153
|
+
// If the stored value is a signal, read it to subscribe
|
|
154
|
+
return (val && val._signal) ? val() : val;
|
|
155
|
+
}
|
|
156
|
+
ctx = ctx._parentCtx;
|
|
157
|
+
}
|
|
158
|
+
return context._defaultValue;
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
// --- createContext ---
|
|
138
|
-
//
|
|
162
|
+
// Tree-scoped context: Provider sets value for its subtree only.
|
|
163
|
+
// Multiple providers can coexist — each subtree sees its own value.
|
|
164
|
+
// Context values are wrapped in signals so consumers re-render when values change.
|
|
139
165
|
|
|
140
166
|
export function createContext(defaultValue) {
|
|
141
|
-
const
|
|
142
|
-
|
|
167
|
+
const context = {
|
|
168
|
+
_defaultValue: defaultValue,
|
|
143
169
|
Provider: ({ value, children }) => {
|
|
144
|
-
ctx
|
|
170
|
+
const ctx = getCtx();
|
|
171
|
+
if (!ctx._contextValues) ctx._contextValues = new Map();
|
|
172
|
+
if (!ctx._contextSignals) ctx._contextSignals = new Map();
|
|
173
|
+
|
|
174
|
+
// Create or update the context signal
|
|
175
|
+
if (!ctx._contextSignals.has(context)) {
|
|
176
|
+
const s = signal(value);
|
|
177
|
+
ctx._contextSignals.set(context, s);
|
|
178
|
+
ctx._contextValues.set(context, s);
|
|
179
|
+
} else {
|
|
180
|
+
ctx._contextSignals.get(context).set(value);
|
|
181
|
+
}
|
|
145
182
|
return children;
|
|
146
183
|
},
|
|
147
184
|
};
|
|
148
|
-
return
|
|
185
|
+
return context;
|
|
149
186
|
}
|
|
150
187
|
|
|
151
188
|
// --- useReducer ---
|
|
@@ -173,7 +210,7 @@ export function useReducer(reducer, initialState, init) {
|
|
|
173
210
|
|
|
174
211
|
export function onMount(fn) {
|
|
175
212
|
const ctx = getCtx();
|
|
176
|
-
if (!ctx.
|
|
213
|
+
if (!ctx.mounted) {
|
|
177
214
|
ctx._mountCallbacks = ctx._mountCallbacks || [];
|
|
178
215
|
ctx._mountCallbacks.push(fn);
|
|
179
216
|
}
|
|
@@ -197,26 +234,33 @@ export function createResource(fetcher, options = {}) {
|
|
|
197
234
|
const loading = signal(!options.initialValue);
|
|
198
235
|
const error = signal(null);
|
|
199
236
|
|
|
200
|
-
let
|
|
237
|
+
let controller = null;
|
|
201
238
|
|
|
202
239
|
const refetch = async (source) => {
|
|
240
|
+
// Abort previous request
|
|
241
|
+
if (controller) controller.abort();
|
|
242
|
+
controller = new AbortController();
|
|
243
|
+
const { signal: abortSignal } = controller;
|
|
244
|
+
|
|
203
245
|
loading.set(true);
|
|
204
246
|
error.set(null);
|
|
205
247
|
|
|
206
248
|
try {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
249
|
+
const result = await fetcher(source, { signal: abortSignal });
|
|
250
|
+
|
|
251
|
+
// Only update if not aborted
|
|
252
|
+
if (!abortSignal.aborted) {
|
|
253
|
+
batch(() => {
|
|
254
|
+
data.set(result);
|
|
255
|
+
loading.set(false);
|
|
256
|
+
});
|
|
215
257
|
}
|
|
216
258
|
} catch (e) {
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
259
|
+
if (!abortSignal.aborted) {
|
|
260
|
+
batch(() => {
|
|
261
|
+
error.set(e);
|
|
262
|
+
loading.set(false);
|
|
263
|
+
});
|
|
220
264
|
}
|
|
221
265
|
}
|
|
222
266
|
};
|
|
@@ -225,6 +269,15 @@ export function createResource(fetcher, options = {}) {
|
|
|
225
269
|
data.set(typeof value === 'function' ? value(data()) : value);
|
|
226
270
|
};
|
|
227
271
|
|
|
272
|
+
// Register cleanup with component lifecycle: abort on unmount
|
|
273
|
+
const ctx = getCurrentComponent?.();
|
|
274
|
+
if (ctx) {
|
|
275
|
+
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
276
|
+
ctx._cleanupCallbacks.push(() => {
|
|
277
|
+
if (controller) controller.abort();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
228
281
|
// Initial fetch if no initial value
|
|
229
282
|
if (!options.initialValue) {
|
|
230
283
|
refetch(options.source);
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// The closest framework to vanilla JS.
|
|
3
3
|
|
|
4
4
|
// Reactive primitives
|
|
5
|
-
export { signal, computed, effect, batch, untrack } from './reactive.js';
|
|
5
|
+
export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot } from './reactive.js';
|
|
6
|
+
|
|
7
|
+
// Fine-grained rendering primitives
|
|
8
|
+
export { template, insert, mapArray, spread, delegateEvents, on, classList } from './render.js';
|
|
6
9
|
|
|
7
10
|
// Virtual DOM
|
|
8
11
|
export { h, Fragment, html } from './h.js';
|
|
@@ -31,7 +34,7 @@ export {
|
|
|
31
34
|
export { memo, lazy, Suspense, ErrorBoundary, Show, For, Switch, Match, Island } from './components.js';
|
|
32
35
|
|
|
33
36
|
// Store
|
|
34
|
-
export { createStore, storeComputed, atom } from './store.js';
|
|
37
|
+
export { createStore, derived, storeComputed, atom } from './store.js';
|
|
35
38
|
|
|
36
39
|
// Head management
|
|
37
40
|
export { Head, clearHead } from './head.js';
|
|
@@ -46,6 +49,7 @@ export {
|
|
|
46
49
|
throttle,
|
|
47
50
|
useMediaQuery,
|
|
48
51
|
useLocalStorage,
|
|
52
|
+
useClickOutside,
|
|
49
53
|
Portal,
|
|
50
54
|
transition,
|
|
51
55
|
} from './helpers.js';
|