what-core 0.4.2 → 0.5.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/components.js +213 -319
- package/dist/dom.js +730 -915
- package/dist/h.js +140 -191
- package/dist/head.js +42 -59
- package/dist/helpers.js +124 -187
- package/dist/hooks.js +186 -279
- package/dist/reactive.js +244 -317
- package/dist/store.js +73 -118
- package/dist/what.js +5 -3
- package/index.d.ts +391 -152
- package/package.json +4 -3
- package/render.d.ts +11 -0
- package/src/a11y.js +52 -6
- package/src/dom.js +91 -8
- package/src/form.js +85 -54
- package/src/helpers.js +1 -12
- package/src/hooks.js +11 -0
- package/src/index.js +1 -1
- package/src/render.js +114 -51
- package/src/store.js +6 -1
package/dist/hooks.js
CHANGED
|
@@ -1,299 +1,206 @@
|
|
|
1
|
-
// What Framework - Hooks
|
|
2
|
-
// React-familiar hooks backed by signals. Zero overhead when deps don't change.
|
|
3
|
-
|
|
4
1
|
import { signal, computed, effect, batch, untrack, __DEV__ } from './reactive.js';
|
|
5
2
|
import { getCurrentComponent } from './dom.js';
|
|
6
|
-
|
|
7
3
|
function getCtx() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
4
|
+
const ctx = getCurrentComponent();
|
|
5
|
+
if (!ctx) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
'[what] Hooks must be called inside a component function. ' +
|
|
8
|
+
'If you need reactive state outside a component, use signal() directly.'
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return ctx;
|
|
12
|
+
}
|
|
18
13
|
function getHook(ctx) {
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
const index = ctx.hookIndex++;
|
|
15
|
+
return { index, exists: index < ctx.hooks.length };
|
|
21
16
|
}
|
|
22
|
-
|
|
23
|
-
// --- useState ---
|
|
24
|
-
// Returns [value, setter]. Setter triggers re-render of this component only.
|
|
25
|
-
|
|
17
|
+
let _useMemoNoDepsWarned = false;
|
|
26
18
|
export function useState(initial) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return [s(), s.set];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// --- useSignal ---
|
|
40
|
-
// Returns the raw signal. More powerful: read with sig(), write with sig.set(v).
|
|
41
|
-
// Avoids array destructuring overhead.
|
|
42
|
-
|
|
19
|
+
const ctx = getCtx();
|
|
20
|
+
const { index, exists } = getHook(ctx);
|
|
21
|
+
if (!exists) {
|
|
22
|
+
const s = signal(typeof initial === 'function' ? initial() : initial);
|
|
23
|
+
ctx.hooks[index] = s;
|
|
24
|
+
}
|
|
25
|
+
const s = ctx.hooks[index];
|
|
26
|
+
return [s(), s.set];
|
|
27
|
+
}
|
|
43
28
|
export function useSignal(initial) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return ctx.hooks[index];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// --- useComputed ---
|
|
55
|
-
// Derived value. Only recomputes when signal deps change.
|
|
56
|
-
|
|
29
|
+
const ctx = getCtx();
|
|
30
|
+
const { index, exists } = getHook(ctx);
|
|
31
|
+
if (!exists) {
|
|
32
|
+
ctx.hooks[index] = signal(typeof initial === 'function' ? initial() : initial);
|
|
33
|
+
}
|
|
34
|
+
return ctx.hooks[index];
|
|
35
|
+
}
|
|
57
36
|
export function useComputed(fn) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return ctx.hooks[index];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// --- useEffect ---
|
|
69
|
-
// Side effect that runs after render. Cleanup function returned by fn is called
|
|
70
|
-
// before re-running and on unmount.
|
|
71
|
-
|
|
37
|
+
const ctx = getCtx();
|
|
38
|
+
const { index, exists } = getHook(ctx);
|
|
39
|
+
if (!exists) {
|
|
40
|
+
ctx.hooks[index] = computed(fn);
|
|
41
|
+
}
|
|
42
|
+
return ctx.hooks[index];
|
|
43
|
+
}
|
|
72
44
|
export function useEffect(fn, deps) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
hook.deps = deps;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// --- useMemo ---
|
|
94
|
-
// Memoized value. Only recomputes when deps change.
|
|
95
|
-
|
|
45
|
+
const ctx = getCtx();
|
|
46
|
+
const { index, exists } = getHook(ctx);
|
|
47
|
+
if (!exists) {
|
|
48
|
+
ctx.hooks[index] = { deps: undefined, cleanup: null };
|
|
49
|
+
}
|
|
50
|
+
const hook = ctx.hooks[index];
|
|
51
|
+
if (depsChanged(hook.deps, deps)) {
|
|
52
|
+
queueMicrotask(() => {
|
|
53
|
+
if (ctx.disposed) return;
|
|
54
|
+
if (hook.cleanup) hook.cleanup();
|
|
55
|
+
hook.cleanup = fn() || null;
|
|
56
|
+
});
|
|
57
|
+
hook.deps = deps;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
96
60
|
export function useMemo(fn, deps) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
61
|
+
const ctx = getCtx();
|
|
62
|
+
const { index, exists } = getHook(ctx);
|
|
63
|
+
if (__DEV__ && deps === undefined && !_useMemoNoDepsWarned) {
|
|
64
|
+
_useMemoNoDepsWarned = true;
|
|
65
|
+
console.warn(
|
|
66
|
+
'[what] useMemo() called without a deps array. ' +
|
|
67
|
+
'This recomputes every render. Use useComputed() for signal-derived values, ' +
|
|
68
|
+
'or pass deps to useMemo().'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (!exists) {
|
|
72
|
+
ctx.hooks[index] = { value: undefined, deps: undefined };
|
|
73
|
+
}
|
|
74
|
+
const hook = ctx.hooks[index];
|
|
75
|
+
if (depsChanged(hook.deps, deps)) {
|
|
76
|
+
hook.value = fn();
|
|
77
|
+
hook.deps = deps;
|
|
78
|
+
}
|
|
79
|
+
return hook.value;
|
|
80
|
+
}
|
|
117
81
|
export function useCallback(fn, deps) {
|
|
118
|
-
|
|
82
|
+
return useMemo(() => fn, deps);
|
|
119
83
|
}
|
|
120
|
-
|
|
121
|
-
// --- useRef ---
|
|
122
|
-
// Mutable ref object. Does NOT trigger re-renders.
|
|
123
|
-
|
|
124
84
|
export function useRef(initial) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return ctx.hooks[index];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// --- useContext ---
|
|
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.
|
|
139
|
-
|
|
85
|
+
const ctx = getCtx();
|
|
86
|
+
const { index, exists } = getHook(ctx);
|
|
87
|
+
if (!exists) {
|
|
88
|
+
ctx.hooks[index] = { current: initial };
|
|
89
|
+
}
|
|
90
|
+
return ctx.hooks[index];
|
|
91
|
+
}
|
|
140
92
|
export function useContext(context) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return context._defaultValue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// --- createContext ---
|
|
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.
|
|
165
|
-
|
|
93
|
+
let ctx = getCurrentComponent();
|
|
94
|
+
if (__DEV__ && !ctx) {
|
|
95
|
+
console.warn(
|
|
96
|
+
`[what] useContext(${context?.displayName || 'Context'}) called outside of component render. ` +
|
|
97
|
+
'useContext must be called during component rendering, not inside effects or event handlers. ' +
|
|
98
|
+
'Store the context value in a variable during render and use that variable in your callback.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
while (ctx) {
|
|
102
|
+
if (ctx._contextValues && ctx._contextValues.has(context)) {
|
|
103
|
+
const val = ctx._contextValues.get(context);
|
|
104
|
+
return (val && val._signal) ? val() : val;
|
|
105
|
+
}
|
|
106
|
+
ctx = ctx._parentCtx;
|
|
107
|
+
}
|
|
108
|
+
return context._defaultValue;
|
|
109
|
+
}
|
|
166
110
|
export function createContext(defaultValue) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return context;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// --- useReducer ---
|
|
189
|
-
// State management with a reducer function (like React).
|
|
190
|
-
|
|
111
|
+
const context = {
|
|
112
|
+
_defaultValue: defaultValue,
|
|
113
|
+
Provider: ({ value, children }) => {
|
|
114
|
+
const ctx = getCtx();
|
|
115
|
+
if (!ctx._contextValues) ctx._contextValues = new Map();
|
|
116
|
+
if (!ctx._contextSignals) ctx._contextSignals = new Map();
|
|
117
|
+
if (!ctx._contextSignals.has(context)) {
|
|
118
|
+
const s = signal(value);
|
|
119
|
+
ctx._contextSignals.set(context, s);
|
|
120
|
+
ctx._contextValues.set(context, s);
|
|
121
|
+
} else {
|
|
122
|
+
ctx._contextSignals.get(context).set(value);
|
|
123
|
+
}
|
|
124
|
+
return children;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
return context;
|
|
128
|
+
}
|
|
191
129
|
export function useReducer(reducer, initialState, init) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return [hook.signal(), hook.dispatch];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// --- onMount ---
|
|
209
|
-
// Run callback once when component mounts. SolidJS-style lifecycle.
|
|
210
|
-
|
|
130
|
+
const ctx = getCtx();
|
|
131
|
+
const { index, exists } = getHook(ctx);
|
|
132
|
+
if (!exists) {
|
|
133
|
+
const initial = init ? init(initialState) : initialState;
|
|
134
|
+
const s = signal(initial);
|
|
135
|
+
const dispatch = (action) => {
|
|
136
|
+
s.set(prev => reducer(prev, action));
|
|
137
|
+
};
|
|
138
|
+
ctx.hooks[index] = { signal: s, dispatch };
|
|
139
|
+
}
|
|
140
|
+
const hook = ctx.hooks[index];
|
|
141
|
+
return [hook.signal(), hook.dispatch];
|
|
142
|
+
}
|
|
211
143
|
export function onMount(fn) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// --- onCleanup ---
|
|
220
|
-
// Register cleanup function to run when component unmounts.
|
|
221
|
-
|
|
144
|
+
const ctx = getCtx();
|
|
145
|
+
if (!ctx.mounted) {
|
|
146
|
+
ctx._mountCallbacks = ctx._mountCallbacks || [];
|
|
147
|
+
ctx._mountCallbacks.push(fn);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
222
150
|
export function onCleanup(fn) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- createResource ---
|
|
229
|
-
// Reactive data fetching primitive (SolidJS-style).
|
|
230
|
-
// Returns [data, { loading, error, refetch, mutate }]
|
|
231
|
-
|
|
151
|
+
const ctx = getCtx();
|
|
152
|
+
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
153
|
+
ctx._cleanupCallbacks.push(fn);
|
|
154
|
+
}
|
|
232
155
|
export function createResource(fetcher, options = {}) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (ctx) {
|
|
275
|
-
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
276
|
-
ctx._cleanupCallbacks.push(() => {
|
|
277
|
-
if (controller) controller.abort();
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Initial fetch if no initial value
|
|
282
|
-
if (!options.initialValue) {
|
|
283
|
-
refetch(options.source);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return [data, { loading, error, refetch, mutate }];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// --- Dep comparison ---
|
|
290
|
-
|
|
291
|
-
function depsChanged(oldDeps, newDeps) {
|
|
292
|
-
if (oldDeps === undefined) return true;
|
|
293
|
-
if (!oldDeps || !newDeps) return true;
|
|
294
|
-
if (oldDeps.length !== newDeps.length) return true;
|
|
295
|
-
for (let i = 0; i < oldDeps.length; i++) {
|
|
296
|
-
if (!Object.is(oldDeps[i], newDeps[i])) return true;
|
|
297
|
-
}
|
|
298
|
-
return false;
|
|
156
|
+
const data = signal(options.initialValue ?? null);
|
|
157
|
+
const loading = signal(!options.initialValue);
|
|
158
|
+
const error = signal(null);
|
|
159
|
+
let controller = null;
|
|
160
|
+
const refetch = async (source) => {
|
|
161
|
+
if (controller) controller.abort();
|
|
162
|
+
controller = new AbortController();
|
|
163
|
+
const { signal: abortSignal } = controller;
|
|
164
|
+
loading.set(true);
|
|
165
|
+
error.set(null);
|
|
166
|
+
try {
|
|
167
|
+
const result = await fetcher(source, { signal: abortSignal });
|
|
168
|
+
if (!abortSignal.aborted) {
|
|
169
|
+
batch(() => {
|
|
170
|
+
data.set(result);
|
|
171
|
+
loading.set(false);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (!abortSignal.aborted) {
|
|
176
|
+
batch(() => {
|
|
177
|
+
error.set(e);
|
|
178
|
+
loading.set(false);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const mutate = (value) => {
|
|
184
|
+
data.set(typeof value === 'function' ? value(data()) : value);
|
|
185
|
+
};
|
|
186
|
+
const ctx = getCurrentComponent?.();
|
|
187
|
+
if (ctx) {
|
|
188
|
+
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
189
|
+
ctx._cleanupCallbacks.push(() => {
|
|
190
|
+
if (controller) controller.abort();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (!options.initialValue) {
|
|
194
|
+
refetch(options.source);
|
|
195
|
+
}
|
|
196
|
+
return [data, { loading, error, refetch, mutate }];
|
|
299
197
|
}
|
|
198
|
+
function depsChanged(oldDeps, newDeps) {
|
|
199
|
+
if (oldDeps === undefined) return true;
|
|
200
|
+
if (!oldDeps || !newDeps) return true;
|
|
201
|
+
if (oldDeps.length !== newDeps.length) return true;
|
|
202
|
+
for (let i = 0; i < oldDeps.length; i++) {
|
|
203
|
+
if (!Object.is(oldDeps[i], newDeps[i])) return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|