what-core 0.1.1 → 0.3.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 +425 -0
- package/dist/animation.js +540 -0
- package/dist/components.js +272 -115
- package/dist/data.js +444 -0
- package/dist/dom.js +702 -427
- package/dist/form.js +441 -0
- package/dist/h.js +191 -138
- package/dist/head.js +59 -42
- package/dist/helpers.js +125 -83
- package/dist/hooks.js +226 -124
- package/dist/index.js +2 -2
- package/dist/reactive.js +165 -108
- package/dist/scheduler.js +241 -0
- package/dist/skeleton.js +363 -0
- package/dist/store.js +114 -55
- package/dist/testing.js +367 -0
- package/dist/what.js +2 -2
- package/index.d.ts +15 -0
- package/package.json +1 -1
- package/src/animation.js +11 -2
- package/src/components.js +93 -0
- package/src/data.js +19 -9
- package/src/dom.js +181 -85
- package/src/hooks.js +22 -10
- package/src/index.js +2 -2
- package/src/reactive.js +15 -1
- package/src/store.js +24 -5
package/dist/helpers.js
CHANGED
|
@@ -1,98 +1,140 @@
|
|
|
1
|
+
// What Framework - Helpers & Utilities
|
|
2
|
+
// Commonly needed patterns, zero overhead.
|
|
3
|
+
|
|
1
4
|
import { signal, effect, computed, batch } from './reactive.js';
|
|
5
|
+
|
|
6
|
+
// --- show(condition, vnode) ---
|
|
7
|
+
// Conditional rendering. More readable than ternary.
|
|
2
8
|
export function show(condition, vnode, fallback = null) {
|
|
3
|
-
return condition ? vnode : fallback;
|
|
9
|
+
return condition ? vnode : fallback;
|
|
4
10
|
}
|
|
11
|
+
|
|
12
|
+
// --- each(list, fn) ---
|
|
13
|
+
// Keyed list rendering. Optimized for reconciliation.
|
|
5
14
|
export function each(list, fn, keyFn) {
|
|
6
|
-
if (!list || list.length === 0) return [];
|
|
7
|
-
return list.map((item, index) => {
|
|
8
|
-
const vnode = fn(item, index);
|
|
9
|
-
if (keyFn && vnode && typeof vnode === 'object') {
|
|
10
|
-
vnode.key = keyFn(item, index);
|
|
11
|
-
}
|
|
12
|
-
return vnode;
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
+
if (!list || list.length === 0) return [];
|
|
16
|
+
return list.map((item, index) => {
|
|
17
|
+
const vnode = fn(item, index);
|
|
18
|
+
if (keyFn && vnode && typeof vnode === 'object') {
|
|
19
|
+
vnode.key = keyFn(item, index);
|
|
20
|
+
}
|
|
21
|
+
return vnode;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- cls(...args) ---
|
|
26
|
+
// Conditional class names. Like clsx but tiny.
|
|
27
|
+
// cls('base', condition && 'active', { hidden: isHidden, bold: isBold })
|
|
15
28
|
export function cls(...args) {
|
|
16
|
-
const classes = [];
|
|
17
|
-
for (const arg of args) {
|
|
18
|
-
if (!arg) continue;
|
|
19
|
-
if (typeof arg === 'string') {
|
|
20
|
-
classes.push(arg);
|
|
21
|
-
} else if (typeof arg === 'object') {
|
|
22
|
-
for (const [key, val] of Object.entries(arg)) {
|
|
23
|
-
if (val) classes.push(key);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return classes.join(' ');
|
|
28
|
-
}
|
|
29
|
+
const classes = [];
|
|
30
|
+
for (const arg of args) {
|
|
31
|
+
if (!arg) continue;
|
|
32
|
+
if (typeof arg === 'string') {
|
|
33
|
+
classes.push(arg);
|
|
34
|
+
} else if (typeof arg === 'object') {
|
|
35
|
+
for (const [key, val] of Object.entries(arg)) {
|
|
36
|
+
if (val) classes.push(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return classes.join(' ');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- style(obj) ---
|
|
44
|
+
// Convert a style object to a CSS string for SSR.
|
|
29
45
|
export function style(obj) {
|
|
30
|
-
if (typeof obj === 'string') return obj;
|
|
31
|
-
return Object.entries(obj)
|
|
32
|
-
.filter(([, v]) => v != null && v !== '')
|
|
33
|
-
.map(([k, v]) => `${camelToKebab(k)}:${v}`)
|
|
34
|
-
.join(';');
|
|
46
|
+
if (typeof obj === 'string') return obj;
|
|
47
|
+
return Object.entries(obj)
|
|
48
|
+
.filter(([, v]) => v != null && v !== '')
|
|
49
|
+
.map(([k, v]) => `${camelToKebab(k)}:${v}`)
|
|
50
|
+
.join(';');
|
|
35
51
|
}
|
|
52
|
+
|
|
36
53
|
function camelToKebab(str) {
|
|
37
|
-
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
54
|
+
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
38
55
|
}
|
|
56
|
+
|
|
57
|
+
// --- debounce ---
|
|
58
|
+
// Debounced signal updates.
|
|
39
59
|
export function debounce(fn, ms) {
|
|
40
|
-
let timer;
|
|
41
|
-
return (...args) => {
|
|
42
|
-
clearTimeout(timer);
|
|
43
|
-
timer = setTimeout(() => fn(...args), ms);
|
|
44
|
-
};
|
|
45
|
-
}
|
|
60
|
+
let timer;
|
|
61
|
+
return (...args) => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- throttle ---
|
|
46
68
|
export function throttle(fn, ms) {
|
|
47
|
-
let last = 0;
|
|
48
|
-
return (...args) => {
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
if (now - last >= ms) {
|
|
51
|
-
last = now;
|
|
52
|
-
fn(...args);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
69
|
+
let last = 0;
|
|
70
|
+
return (...args) => {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (now - last >= ms) {
|
|
73
|
+
last = now;
|
|
74
|
+
fn(...args);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- useMediaQuery ---
|
|
80
|
+
// Reactive media query. Returns a signal.
|
|
56
81
|
export function useMediaQuery(query) {
|
|
57
|
-
if (typeof window === 'undefined') return signal(false);
|
|
58
|
-
const mq = window.matchMedia(query);
|
|
59
|
-
const s = signal(mq.matches);
|
|
60
|
-
mq.addEventListener('change', (e) => s.set(e.matches));
|
|
61
|
-
return s;
|
|
62
|
-
}
|
|
82
|
+
if (typeof window === 'undefined') return signal(false);
|
|
83
|
+
const mq = window.matchMedia(query);
|
|
84
|
+
const s = signal(mq.matches);
|
|
85
|
+
mq.addEventListener('change', (e) => s.set(e.matches));
|
|
86
|
+
return s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- useLocalStorage ---
|
|
90
|
+
// Signal synced with localStorage.
|
|
63
91
|
export function useLocalStorage(key, initial) {
|
|
64
|
-
let stored;
|
|
65
|
-
try {
|
|
66
|
-
const raw = localStorage.getItem(key);
|
|
67
|
-
stored = raw !== null ? JSON.parse(raw) : initial;
|
|
68
|
-
} catch {
|
|
69
|
-
stored = initial;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
localStorage
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
92
|
+
let stored;
|
|
93
|
+
try {
|
|
94
|
+
const raw = localStorage.getItem(key);
|
|
95
|
+
stored = raw !== null ? JSON.parse(raw) : initial;
|
|
96
|
+
} catch {
|
|
97
|
+
stored = initial;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const s = signal(stored);
|
|
101
|
+
|
|
102
|
+
// Sync to localStorage on changes
|
|
103
|
+
effect(() => {
|
|
104
|
+
try {
|
|
105
|
+
localStorage.setItem(key, JSON.stringify(s()));
|
|
106
|
+
} catch { /* quota exceeded, etc */ }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Listen for changes from other tabs
|
|
110
|
+
if (typeof window !== 'undefined') {
|
|
111
|
+
window.addEventListener('storage', (e) => {
|
|
112
|
+
if (e.key === key && e.newValue !== null) {
|
|
113
|
+
try { s.set(JSON.parse(e.newValue)); } catch {}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- portal ---
|
|
122
|
+
// Render children into a different DOM container.
|
|
86
123
|
export function Portal({ target, children }) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
// In SSR, just return null (portals are client-only)
|
|
125
|
+
if (typeof document === 'undefined') return null;
|
|
126
|
+
const container = typeof target === 'string'
|
|
127
|
+
? document.querySelector(target)
|
|
128
|
+
: target;
|
|
129
|
+
if (!container) return null;
|
|
130
|
+
// The DOM reconciler will mount children here
|
|
131
|
+
return { tag: '__portal', props: { container }, children: Array.isArray(children) ? children : [children], _vnode: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Transition helper ---
|
|
135
|
+
// Animate elements in/out. Returns props to spread on the element.
|
|
94
136
|
export function transition(name, active) {
|
|
95
|
-
return {
|
|
96
|
-
class: active ? `${name}-enter ${name}-enter-active` : `${name}-leave ${name}-leave-active`,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
137
|
+
return {
|
|
138
|
+
class: active ? `${name}-enter ${name}-enter-active` : `${name}-leave ${name}-leave-active`,
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/hooks.js
CHANGED
|
@@ -1,156 +1,258 @@
|
|
|
1
|
+
// What Framework - Hooks
|
|
2
|
+
// React-familiar hooks backed by signals. Zero overhead when deps don't change.
|
|
3
|
+
|
|
1
4
|
import { signal, computed, effect, batch, untrack } from './reactive.js';
|
|
2
|
-
import { getCurrentComponent } from './dom.js';
|
|
5
|
+
import { getCurrentComponent, getComponentStack as _getComponentStack } from './dom.js';
|
|
6
|
+
|
|
3
7
|
function getCtx() {
|
|
4
|
-
const ctx = getCurrentComponent();
|
|
5
|
-
if (!ctx) throw new Error('Hooks must be called inside a component');
|
|
6
|
-
return ctx;
|
|
8
|
+
const ctx = getCurrentComponent();
|
|
9
|
+
if (!ctx) throw new Error('Hooks must be called inside a component');
|
|
10
|
+
return ctx;
|
|
7
11
|
}
|
|
12
|
+
|
|
8
13
|
function getHook(ctx) {
|
|
9
|
-
const index = ctx.hookIndex++;
|
|
10
|
-
return { index, exists: index < ctx.hooks.length };
|
|
14
|
+
const index = ctx.hookIndex++;
|
|
15
|
+
return { index, exists: index < ctx.hooks.length };
|
|
11
16
|
}
|
|
17
|
+
|
|
18
|
+
// --- useState ---
|
|
19
|
+
// Returns [value, setter]. Setter triggers re-render of this component only.
|
|
20
|
+
|
|
12
21
|
export function useState(initial) {
|
|
13
|
-
const ctx = getCtx();
|
|
14
|
-
const { index, exists } = getHook(ctx);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const ctx = getCtx();
|
|
23
|
+
const { index, exists } = getHook(ctx);
|
|
24
|
+
|
|
25
|
+
if (!exists) {
|
|
26
|
+
const s = signal(typeof initial === 'function' ? initial() : initial);
|
|
27
|
+
ctx.hooks[index] = s;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const s = ctx.hooks[index];
|
|
31
|
+
return [s(), s.set];
|
|
21
32
|
}
|
|
33
|
+
|
|
34
|
+
// --- useSignal ---
|
|
35
|
+
// Returns the raw signal. More powerful: read with sig(), write with sig.set(v).
|
|
36
|
+
// Avoids array destructuring overhead.
|
|
37
|
+
|
|
22
38
|
export function useSignal(initial) {
|
|
23
|
-
const ctx = getCtx();
|
|
24
|
-
const { index, exists } = getHook(ctx);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
const ctx = getCtx();
|
|
40
|
+
const { index, exists } = getHook(ctx);
|
|
41
|
+
|
|
42
|
+
if (!exists) {
|
|
43
|
+
ctx.hooks[index] = signal(typeof initial === 'function' ? initial() : initial);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ctx.hooks[index];
|
|
29
47
|
}
|
|
48
|
+
|
|
49
|
+
// --- useComputed ---
|
|
50
|
+
// Derived value. Only recomputes when signal deps change.
|
|
51
|
+
|
|
30
52
|
export function useComputed(fn) {
|
|
31
|
-
const ctx = getCtx();
|
|
32
|
-
const { index, exists } = getHook(ctx);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
const ctx = getCtx();
|
|
54
|
+
const { index, exists } = getHook(ctx);
|
|
55
|
+
|
|
56
|
+
if (!exists) {
|
|
57
|
+
ctx.hooks[index] = computed(fn);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return ctx.hooks[index];
|
|
37
61
|
}
|
|
62
|
+
|
|
63
|
+
// --- useEffect ---
|
|
64
|
+
// Side effect that runs after render. Cleanup function returned by fn is called
|
|
65
|
+
// before re-running and on unmount.
|
|
66
|
+
|
|
38
67
|
export function useEffect(fn, deps) {
|
|
39
|
-
const ctx = getCtx();
|
|
40
|
-
const { index, exists } = getHook(ctx);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (hook.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
const ctx = getCtx();
|
|
69
|
+
const { index, exists } = getHook(ctx);
|
|
70
|
+
|
|
71
|
+
if (!exists) {
|
|
72
|
+
ctx.hooks[index] = { deps: undefined, cleanup: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hook = ctx.hooks[index];
|
|
76
|
+
|
|
77
|
+
if (depsChanged(hook.deps, deps)) {
|
|
78
|
+
// Schedule after current render
|
|
79
|
+
queueMicrotask(() => {
|
|
80
|
+
if (ctx.disposed) return;
|
|
81
|
+
if (hook.cleanup) hook.cleanup();
|
|
82
|
+
hook.cleanup = fn() || null;
|
|
83
|
+
});
|
|
84
|
+
hook.deps = deps;
|
|
85
|
+
}
|
|
53
86
|
}
|
|
87
|
+
|
|
88
|
+
// --- useMemo ---
|
|
89
|
+
// Memoized value. Only recomputes when deps change.
|
|
90
|
+
|
|
54
91
|
export function useMemo(fn, deps) {
|
|
55
|
-
const ctx = getCtx();
|
|
56
|
-
const { index, exists } = getHook(ctx);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
hook
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
const ctx = getCtx();
|
|
93
|
+
const { index, exists } = getHook(ctx);
|
|
94
|
+
|
|
95
|
+
if (!exists) {
|
|
96
|
+
ctx.hooks[index] = { value: undefined, deps: undefined };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hook = ctx.hooks[index];
|
|
100
|
+
|
|
101
|
+
if (depsChanged(hook.deps, deps)) {
|
|
102
|
+
hook.value = fn();
|
|
103
|
+
hook.deps = deps;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return hook.value;
|
|
66
107
|
}
|
|
108
|
+
|
|
109
|
+
// --- useCallback ---
|
|
110
|
+
// Memoized callback. Identity-stable when deps don't change.
|
|
111
|
+
|
|
67
112
|
export function useCallback(fn, deps) {
|
|
68
|
-
return useMemo(() => fn, deps);
|
|
113
|
+
return useMemo(() => fn, deps);
|
|
69
114
|
}
|
|
115
|
+
|
|
116
|
+
// --- useRef ---
|
|
117
|
+
// Mutable ref object. Does NOT trigger re-renders.
|
|
118
|
+
|
|
70
119
|
export function useRef(initial) {
|
|
71
|
-
const ctx = getCtx();
|
|
72
|
-
const { index, exists } = getHook(ctx);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
120
|
+
const ctx = getCtx();
|
|
121
|
+
const { index, exists } = getHook(ctx);
|
|
122
|
+
|
|
123
|
+
if (!exists) {
|
|
124
|
+
ctx.hooks[index] = { current: initial };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return ctx.hooks[index];
|
|
77
128
|
}
|
|
129
|
+
|
|
130
|
+
// --- useContext ---
|
|
131
|
+
// Read from the nearest Provider in the component tree, or the default value.
|
|
132
|
+
|
|
78
133
|
export function useContext(context) {
|
|
79
|
-
|
|
134
|
+
// Walk up the component stack to find the nearest provider for this context
|
|
135
|
+
const stack = _getComponentStack();
|
|
136
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
137
|
+
const ctx = stack[i];
|
|
138
|
+
if (ctx._contextValues && ctx._contextValues.has(context)) {
|
|
139
|
+
return ctx._contextValues.get(context);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return context._defaultValue;
|
|
80
143
|
}
|
|
144
|
+
|
|
145
|
+
// --- createContext ---
|
|
146
|
+
// Tree-scoped context: Provider sets value for its subtree only.
|
|
147
|
+
// Multiple providers can coexist — each subtree sees its own value.
|
|
148
|
+
|
|
81
149
|
export function createContext(defaultValue) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
Provider: ({ value, children }) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
150
|
+
const context = {
|
|
151
|
+
_defaultValue: defaultValue,
|
|
152
|
+
Provider: ({ value, children }) => {
|
|
153
|
+
// Store context value on the current component's context
|
|
154
|
+
const ctx = getCtx();
|
|
155
|
+
if (!ctx._contextValues) ctx._contextValues = new Map();
|
|
156
|
+
ctx._contextValues.set(context, value);
|
|
157
|
+
return children;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
return context;
|
|
90
161
|
}
|
|
162
|
+
|
|
163
|
+
// --- useReducer ---
|
|
164
|
+
// State management with a reducer function (like React).
|
|
165
|
+
|
|
91
166
|
export function useReducer(reducer, initialState, init) {
|
|
92
|
-
const ctx = getCtx();
|
|
93
|
-
const { index, exists } = getHook(ctx);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
167
|
+
const ctx = getCtx();
|
|
168
|
+
const { index, exists } = getHook(ctx);
|
|
169
|
+
|
|
170
|
+
if (!exists) {
|
|
171
|
+
const initial = init ? init(initialState) : initialState;
|
|
172
|
+
const s = signal(initial);
|
|
173
|
+
const dispatch = (action) => {
|
|
174
|
+
s.set(prev => reducer(prev, action));
|
|
175
|
+
};
|
|
176
|
+
ctx.hooks[index] = { signal: s, dispatch };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const hook = ctx.hooks[index];
|
|
180
|
+
return [hook.signal(), hook.dispatch];
|
|
104
181
|
}
|
|
182
|
+
|
|
183
|
+
// --- onMount ---
|
|
184
|
+
// Run callback once when component mounts. SolidJS-style lifecycle.
|
|
185
|
+
|
|
105
186
|
export function onMount(fn) {
|
|
106
|
-
const ctx = getCtx();
|
|
107
|
-
if (!ctx.
|
|
108
|
-
ctx._mountCallbacks = ctx._mountCallbacks || [];
|
|
109
|
-
ctx._mountCallbacks.push(fn);
|
|
110
|
-
}
|
|
187
|
+
const ctx = getCtx();
|
|
188
|
+
if (!ctx.mounted) {
|
|
189
|
+
ctx._mountCallbacks = ctx._mountCallbacks || [];
|
|
190
|
+
ctx._mountCallbacks.push(fn);
|
|
191
|
+
}
|
|
111
192
|
}
|
|
193
|
+
|
|
194
|
+
// --- onCleanup ---
|
|
195
|
+
// Register cleanup function to run when component unmounts.
|
|
196
|
+
|
|
112
197
|
export function onCleanup(fn) {
|
|
113
|
-
const ctx = getCtx();
|
|
114
|
-
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
115
|
-
ctx._cleanupCallbacks.push(fn);
|
|
198
|
+
const ctx = getCtx();
|
|
199
|
+
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
200
|
+
ctx._cleanupCallbacks.push(fn);
|
|
116
201
|
}
|
|
202
|
+
|
|
203
|
+
// --- createResource ---
|
|
204
|
+
// Reactive data fetching primitive (SolidJS-style).
|
|
205
|
+
// Returns [data, { loading, error, refetch, mutate }]
|
|
206
|
+
|
|
117
207
|
export function createResource(fetcher, options = {}) {
|
|
118
|
-
const data = signal(options.initialValue ?? null);
|
|
119
|
-
const loading = signal(!options.initialValue);
|
|
120
|
-
const error = signal(null);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (currentFetch ===
|
|
135
|
-
|
|
136
|
-
loading.set(false);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
208
|
+
const data = signal(options.initialValue ?? null);
|
|
209
|
+
const loading = signal(!options.initialValue);
|
|
210
|
+
const error = signal(null);
|
|
211
|
+
|
|
212
|
+
let currentFetch = null;
|
|
213
|
+
|
|
214
|
+
const refetch = async (source) => {
|
|
215
|
+
loading.set(true);
|
|
216
|
+
error.set(null);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const fetchPromise = fetcher(source);
|
|
220
|
+
currentFetch = fetchPromise;
|
|
221
|
+
const result = await fetchPromise;
|
|
222
|
+
|
|
223
|
+
// Only update if this is still the current fetch
|
|
224
|
+
if (currentFetch === fetchPromise) {
|
|
225
|
+
data.set(result);
|
|
226
|
+
loading.set(false);
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
if (currentFetch === fetchPromise) {
|
|
230
|
+
error.set(e);
|
|
231
|
+
loading.set(false);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const mutate = (value) => {
|
|
237
|
+
data.set(typeof value === 'function' ? value(data()) : value);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Initial fetch if no initial value
|
|
241
|
+
if (!options.initialValue) {
|
|
242
|
+
refetch(options.source);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return [data, { loading, error, refetch, mutate }];
|
|
147
246
|
}
|
|
247
|
+
|
|
248
|
+
// --- Dep comparison ---
|
|
249
|
+
|
|
148
250
|
function depsChanged(oldDeps, newDeps) {
|
|
149
|
-
if (oldDeps === undefined) return true;
|
|
150
|
-
if (!oldDeps || !newDeps) return true;
|
|
151
|
-
if (oldDeps.length !== newDeps.length) return true;
|
|
152
|
-
for (let i = 0; i < oldDeps.length; i++) {
|
|
153
|
-
if (!Object.is(oldDeps[i], newDeps[i])) return true;
|
|
154
|
-
}
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
251
|
+
if (oldDeps === undefined) return true;
|
|
252
|
+
if (!oldDeps || !newDeps) return true;
|
|
253
|
+
if (oldDeps.length !== newDeps.length) return true;
|
|
254
|
+
for (let i = 0; i < oldDeps.length; i++) {
|
|
255
|
+
if (!Object.is(oldDeps[i], newDeps[i])) return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -28,10 +28,10 @@ export {
|
|
|
28
28
|
} from './hooks.js';
|
|
29
29
|
|
|
30
30
|
// Component helpers
|
|
31
|
-
export { memo, lazy, Suspense, ErrorBoundary, Show, For, Switch, Match } from './components.js';
|
|
31
|
+
export { memo, lazy, Suspense, ErrorBoundary, Show, For, Switch, Match, Island } from './components.js';
|
|
32
32
|
|
|
33
33
|
// Store
|
|
34
|
-
export { createStore, atom } from './store.js';
|
|
34
|
+
export { createStore, storeComputed, atom } from './store.js';
|
|
35
35
|
|
|
36
36
|
// Head management
|
|
37
37
|
export { Head, clearHead } from './head.js';
|