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/components.js
CHANGED
|
@@ -1,335 +1,229 @@
|
|
|
1
|
-
// What Framework - Component Utilities
|
|
2
|
-
// memo, lazy, Suspense, ErrorBoundary
|
|
3
|
-
|
|
4
1
|
import { h } from './h.js';
|
|
5
2
|
import { signal, effect, untrack, __DEV__ } from './reactive.js';
|
|
6
|
-
|
|
7
|
-
// Legacy errorBoundaryStack removed — tree-based resolution via _parentCtx._errorBoundary
|
|
8
|
-
// is now the only mechanism. See reportError() below.
|
|
9
|
-
|
|
10
|
-
// --- memo ---
|
|
11
|
-
// Skip re-render when parent re-renders with unchanged props.
|
|
12
|
-
// Signal-safe: when an internal signal changes, the component always re-renders
|
|
13
|
-
// (memo never blocks signal-triggered updates).
|
|
14
|
-
//
|
|
15
|
-
// How it works:
|
|
16
|
-
// - In our architecture, Component(propsSignal()) is called inside an effect.
|
|
17
|
-
// - When parent re-renders: propsSignal is set to a new object → props is a NEW reference
|
|
18
|
-
// - When an internal signal changes: propsSignal unchanged → props is the SAME reference
|
|
19
|
-
// - memo only skips when: props is a new reference AND structurally equal to previous
|
|
20
|
-
|
|
21
3
|
export function memo(Component, areEqual) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
MemoWrapper.displayName = `Memo(${Component.name || 'Anonymous'})`;
|
|
46
|
-
return MemoWrapper;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Injected by dom.js
|
|
4
|
+
const compare = areEqual || shallowEqual;
|
|
5
|
+
function MemoWrapper(props) {
|
|
6
|
+
const ctx = _getCurrentComponent?.();
|
|
7
|
+
if (ctx && ctx._memoResult !== undefined) {
|
|
8
|
+
if (props === ctx._memoPropsRef) {
|
|
9
|
+
} else if (compare(ctx._memoProps, props)) {
|
|
10
|
+
ctx._memoPropsRef = props;
|
|
11
|
+
return ctx._memoResult;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (ctx) {
|
|
15
|
+
ctx._memoPropsRef = props;
|
|
16
|
+
ctx._memoProps = { ...props };
|
|
17
|
+
}
|
|
18
|
+
const result = Component(props);
|
|
19
|
+
if (ctx) ctx._memoResult = result;
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
MemoWrapper.displayName = `Memo(${Component.name || 'Anonymous'})`;
|
|
23
|
+
return MemoWrapper;
|
|
24
|
+
}
|
|
50
25
|
let _getCurrentComponent = null;
|
|
51
26
|
export function _injectGetCurrentComponent(fn) { _getCurrentComponent = fn; }
|
|
52
|
-
|
|
53
27
|
function shallowEqual(a, b) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// --- lazy ---
|
|
65
|
-
// Code-split a component. Returns a wrapper that loads on first render.
|
|
66
|
-
|
|
28
|
+
if (a === b) return true;
|
|
29
|
+
const keysA = Object.keys(a);
|
|
30
|
+
const keysB = Object.keys(b);
|
|
31
|
+
if (keysA.length !== keysB.length) return false;
|
|
32
|
+
for (const key of keysA) {
|
|
33
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
67
37
|
export function lazy(loader) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
LazyWrapper._onLoad = (fn) => {
|
|
95
|
-
if (Component) fn();
|
|
96
|
-
else listeners.add(fn);
|
|
97
|
-
};
|
|
98
|
-
return LazyWrapper;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// --- Suspense ---
|
|
102
|
-
// Show fallback while children are loading (lazy components).
|
|
103
|
-
// Works with lazy() and async components.
|
|
104
|
-
|
|
38
|
+
let Component = null;
|
|
39
|
+
let loadPromise = null;
|
|
40
|
+
let loadError = null;
|
|
41
|
+
const listeners = new Set();
|
|
42
|
+
function LazyWrapper(props) {
|
|
43
|
+
if (loadError) throw loadError;
|
|
44
|
+
if (Component) return h(Component, props);
|
|
45
|
+
if (!loadPromise) {
|
|
46
|
+
loadPromise = loader()
|
|
47
|
+
.then(mod => {
|
|
48
|
+
Component = mod.default || mod;
|
|
49
|
+
listeners.forEach(fn => fn());
|
|
50
|
+
listeners.clear();
|
|
51
|
+
})
|
|
52
|
+
.catch(err => { loadError = err; });
|
|
53
|
+
}
|
|
54
|
+
throw loadPromise;
|
|
55
|
+
}
|
|
56
|
+
LazyWrapper.displayName = 'Lazy';
|
|
57
|
+
LazyWrapper._lazy = true;
|
|
58
|
+
LazyWrapper._onLoad = (fn) => {
|
|
59
|
+
if (Component) fn();
|
|
60
|
+
else listeners.add(fn);
|
|
61
|
+
};
|
|
62
|
+
return LazyWrapper;
|
|
63
|
+
}
|
|
105
64
|
export function Suspense({ fallback, children }) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
_vnode: true,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// --- ErrorBoundary ---
|
|
133
|
-
// Catch errors in children and show fallback.
|
|
134
|
-
// Uses a signal to track error state so it works with reactive rendering.
|
|
135
|
-
|
|
65
|
+
const loading = signal(false);
|
|
66
|
+
const pendingPromises = new Set();
|
|
67
|
+
const boundary = {
|
|
68
|
+
_suspense: true,
|
|
69
|
+
onSuspend(promise) {
|
|
70
|
+
loading.set(true);
|
|
71
|
+
pendingPromises.add(promise);
|
|
72
|
+
promise.finally(() => {
|
|
73
|
+
pendingPromises.delete(promise);
|
|
74
|
+
if (pendingPromises.size === 0) {
|
|
75
|
+
loading.set(false);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
tag: '__suspense',
|
|
82
|
+
props: { boundary, fallback, loading },
|
|
83
|
+
children: Array.isArray(children) ? children : [children],
|
|
84
|
+
_vnode: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
136
87
|
export function ErrorBoundary({ fallback, children, onError }) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
props: { errorState, handleError, fallback, reset },
|
|
157
|
-
children: Array.isArray(children) ? children : [children],
|
|
158
|
-
_vnode: true,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Helper to report error to nearest boundary
|
|
163
|
-
// Walks the component context tree (not a runtime stack) so async errors are caught
|
|
88
|
+
const errorState = signal(null);
|
|
89
|
+
const handleError = (error) => {
|
|
90
|
+
errorState.set(error);
|
|
91
|
+
if (onError) {
|
|
92
|
+
try {
|
|
93
|
+
onError(error);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error('Error in onError handler:', e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const reset = () => errorState.set(null);
|
|
100
|
+
return {
|
|
101
|
+
tag: '__errorBoundary',
|
|
102
|
+
props: { errorState, handleError, fallback, reset },
|
|
103
|
+
children: Array.isArray(children) ? children : [children],
|
|
104
|
+
_vnode: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
164
107
|
export function reportError(error, startCtx) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// _getCurrentComponent is already declared above and injected via _injectGetCurrentComponent
|
|
178
|
-
|
|
179
|
-
// --- Show ---
|
|
180
|
-
// Conditional rendering component. Cleaner than ternaries.
|
|
181
|
-
|
|
108
|
+
let ctx = startCtx || _getCurrentComponent?.();
|
|
109
|
+
while (ctx) {
|
|
110
|
+
if (ctx._errorBoundary) {
|
|
111
|
+
ctx._errorBoundary(error);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
ctx = ctx._parentCtx;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
182
118
|
export function Show({ when, fallback = null, children }) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// --- For ---
|
|
189
|
-
// Efficient list rendering with keyed reconciliation.
|
|
190
|
-
|
|
119
|
+
const condition = typeof when === 'function' ? when() : when;
|
|
120
|
+
return condition ? children : fallback;
|
|
121
|
+
}
|
|
191
122
|
export function For({ each, fallback = null, children }) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
vnode.key = item;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return vnode;
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// --- Switch / Match ---
|
|
220
|
-
// Multi-condition rendering (like switch statement).
|
|
221
|
-
|
|
123
|
+
const list = typeof each === 'function' ? each() : each;
|
|
124
|
+
if (!list || list.length === 0) return fallback;
|
|
125
|
+
const renderFn = Array.isArray(children) ? children[0] : children;
|
|
126
|
+
if (typeof renderFn !== 'function') {
|
|
127
|
+
console.warn('[what] For: children must be a render function, e.g. <For each={items}>{(item) => ...}</For>');
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
return list.map((item, index) => {
|
|
131
|
+
const vnode = renderFn(item, index);
|
|
132
|
+
if (vnode && typeof vnode === 'object' && vnode.key == null) {
|
|
133
|
+
if (item != null && typeof item === 'object') {
|
|
134
|
+
if (item.id != null) vnode.key = item.id;
|
|
135
|
+
else if (item.key != null) vnode.key = item.key;
|
|
136
|
+
} else if (typeof item === 'string' || typeof item === 'number') {
|
|
137
|
+
vnode.key = item;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return vnode;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
222
143
|
export function Switch({ fallback = null, children }) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return fallback;
|
|
237
|
-
}
|
|
238
|
-
|
|
144
|
+
const kids = Array.isArray(children) ? children : [children];
|
|
145
|
+
for (const child of kids) {
|
|
146
|
+
if (child && child.tag === Match) {
|
|
147
|
+
const condition = typeof child.props.when === 'function'
|
|
148
|
+
? child.props.when()
|
|
149
|
+
: child.props.when;
|
|
150
|
+
if (condition) {
|
|
151
|
+
return child.children;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return fallback;
|
|
156
|
+
}
|
|
239
157
|
export function Match({ when, children }) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// --- Island ---
|
|
245
|
-
// Deferred hydration component for islands architecture.
|
|
246
|
-
// Usage: h(Island, { component: Counter, mode: 'idle' })
|
|
247
|
-
// The babel plugin compiles <Counter client:idle /> into this.
|
|
248
|
-
|
|
158
|
+
return { tag: Match, props: { when }, children, _vnode: true };
|
|
159
|
+
}
|
|
249
160
|
export function Island({ component: Component, mode, mediaQuery, ...props }) {
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
mq.removeEventListener('change', checkMedia);
|
|
312
|
-
doHydrate();
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
mq.addEventListener('change', checkMedia);
|
|
316
|
-
}
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
default:
|
|
321
|
-
// Unknown mode, hydrate immediately
|
|
322
|
-
queueMicrotask(doHydrate);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Use ref callback to get the DOM element and schedule hydration
|
|
327
|
-
const refCallback = (el) => {
|
|
328
|
-
if (el) scheduleHydration(el);
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
// Return: show placeholder until hydrated, then show the real component
|
|
332
|
-
return h('div', { 'data-island': Component.name || 'Island', 'data-hydrate': mode, ref: refCallback },
|
|
333
|
-
hydrated() ? wrapper() : null
|
|
334
|
-
);
|
|
161
|
+
const placeholder = h('div', { 'data-island': Component.name || 'Island', 'data-hydrate': mode });
|
|
162
|
+
const wrapper = signal(null);
|
|
163
|
+
const hydrated = signal(false);
|
|
164
|
+
function doHydrate() {
|
|
165
|
+
if (hydrated()) return;
|
|
166
|
+
hydrated.set(true);
|
|
167
|
+
wrapper.set(h(Component, props));
|
|
168
|
+
}
|
|
169
|
+
function scheduleHydration(el) {
|
|
170
|
+
switch (mode) {
|
|
171
|
+
case 'load':
|
|
172
|
+
queueMicrotask(doHydrate);
|
|
173
|
+
break;
|
|
174
|
+
case 'idle':
|
|
175
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
176
|
+
requestIdleCallback(doHydrate);
|
|
177
|
+
} else {
|
|
178
|
+
setTimeout(doHydrate, 200);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case 'visible': {
|
|
182
|
+
const observer = new IntersectionObserver((entries) => {
|
|
183
|
+
if (entries[0].isIntersecting) {
|
|
184
|
+
observer.disconnect();
|
|
185
|
+
doHydrate();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
observer.observe(el);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'interaction': {
|
|
192
|
+
const hydrate = () => {
|
|
193
|
+
el.removeEventListener('click', hydrate);
|
|
194
|
+
el.removeEventListener('focus', hydrate);
|
|
195
|
+
el.removeEventListener('mouseenter', hydrate);
|
|
196
|
+
doHydrate();
|
|
197
|
+
};
|
|
198
|
+
el.addEventListener('click', hydrate, { once: true });
|
|
199
|
+
el.addEventListener('focus', hydrate, { once: true });
|
|
200
|
+
el.addEventListener('mouseenter', hydrate, { once: true });
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'media': {
|
|
204
|
+
if (!mediaQuery) { doHydrate(); break; }
|
|
205
|
+
const mq = window.matchMedia(mediaQuery);
|
|
206
|
+
if (mq.matches) {
|
|
207
|
+
queueMicrotask(doHydrate);
|
|
208
|
+
} else {
|
|
209
|
+
const checkMedia = () => {
|
|
210
|
+
if (mq.matches) {
|
|
211
|
+
mq.removeEventListener('change', checkMedia);
|
|
212
|
+
doHydrate();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
mq.addEventListener('change', checkMedia);
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
default:
|
|
220
|
+
queueMicrotask(doHydrate);
|
|
221
|
+
}
|
|
335
222
|
}
|
|
223
|
+
const refCallback = (el) => {
|
|
224
|
+
if (el) scheduleHydration(el);
|
|
225
|
+
};
|
|
226
|
+
return h('div', { 'data-island': Component.name || 'Island', 'data-hydrate': mode, ref: refCallback },
|
|
227
|
+
hydrated() ? wrapper() : null
|
|
228
|
+
);
|
|
229
|
+
}
|