vaderjs 2.3.11 → 2.3.13
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/index.ts +437 -164
- package/main.js +305 -152
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -30,7 +30,8 @@ interface Fiber {
|
|
|
30
30
|
effectTag?: "PLACEMENT" | "UPDATE" | "DELETION";
|
|
31
31
|
hooks?: Hook[];
|
|
32
32
|
key?: string | number | null;
|
|
33
|
-
|
|
33
|
+
ref?: any;
|
|
34
|
+
propsCache?: Record<string, any>;
|
|
34
35
|
__compareProps?: (prev: any, next: any) => boolean;
|
|
35
36
|
__skipMemo?: boolean;
|
|
36
37
|
_needsUpdate?: boolean;
|
|
@@ -43,6 +44,7 @@ export interface VNode {
|
|
|
43
44
|
[key: string]: any;
|
|
44
45
|
};
|
|
45
46
|
key?: string | number | null;
|
|
47
|
+
ref?: any; // ✅ Add this for ref support
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
interface Hook {
|
|
@@ -61,12 +63,55 @@ interface Hook {
|
|
|
61
63
|
*/
|
|
62
64
|
const isEvent = (key: string) => key.startsWith("on");
|
|
63
65
|
|
|
66
|
+
function shouldSetAsProperty(name: string, isSvg: boolean): boolean {
|
|
67
|
+
// These should always be set as properties (when possible)
|
|
68
|
+
const propertyNames = [
|
|
69
|
+
'value', 'checked', 'selected', 'disabled', 'readOnly',
|
|
70
|
+
'multiple', 'muted', 'defaultChecked', 'defaultValue'
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// These should always be set as attributes
|
|
74
|
+
const attributeNames = [
|
|
75
|
+
'aria-', 'data-', 'role', 'tabindex', 'for', 'class', 'style',
|
|
76
|
+
'id', 'name', 'type', 'placeholder', 'href', 'src', 'alt',
|
|
77
|
+
'title', 'width', 'height', 'viewBox', 'fill', 'stroke'
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Check if it's a boolean attribute
|
|
81
|
+
if (name in dom && typeof (dom as any)[name] === 'boolean') {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check property list
|
|
86
|
+
if (propertyNames.includes(name)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check attribute patterns
|
|
91
|
+
if (attributeNames.some(attr => name.startsWith(attr)) || name.includes('-')) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// For SVG, prefer attributes
|
|
96
|
+
if (isSvg) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Default to property if it exists on the DOM element
|
|
101
|
+
return name in dom;
|
|
102
|
+
}
|
|
64
103
|
/**
|
|
65
104
|
* Checks if a property key is a regular property (not children or event).
|
|
66
105
|
* @param {string} key - The property key to check.
|
|
67
106
|
* @returns {boolean} True if the key is a regular property.
|
|
68
107
|
*/
|
|
69
|
-
const isProperty = (key: string) =>
|
|
108
|
+
const isProperty = (key: string) =>
|
|
109
|
+
key !== "children" &&
|
|
110
|
+
!isEvent(key) &&
|
|
111
|
+
key !== "ref" &&
|
|
112
|
+
key !== "key" &&
|
|
113
|
+
key !== "__source" &&
|
|
114
|
+
key !== "__self";
|
|
70
115
|
|
|
71
116
|
/**
|
|
72
117
|
* Creates a function to check if a property has changed between objects.
|
|
@@ -91,11 +136,11 @@ const isGone = (prev: object, next: object) => (key: string) => !(key in next);
|
|
|
91
136
|
*/
|
|
92
137
|
function createDom(fiber: Fiber): Node {
|
|
93
138
|
let dom: Node;
|
|
139
|
+
const isSvg = isSvgElement(fiber);
|
|
94
140
|
|
|
95
141
|
if (fiber.type === "TEXT_ELEMENT") {
|
|
96
|
-
dom = document.createTextNode("");
|
|
142
|
+
dom = document.createTextNode(fiber.props.nodeValue || "");
|
|
97
143
|
} else {
|
|
98
|
-
const isSvg = isSvgElement(fiber);
|
|
99
144
|
if (isSvg) {
|
|
100
145
|
dom = document.createElementNS("http://www.w3.org/2000/svg", fiber.type as string);
|
|
101
146
|
} else {
|
|
@@ -103,13 +148,10 @@ function createDom(fiber: Fiber): Node {
|
|
|
103
148
|
}
|
|
104
149
|
}
|
|
105
150
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
fiber.props.ref.current = dom;
|
|
111
|
-
}
|
|
112
|
-
|
|
151
|
+
// Update props (attributes, events, etc.)
|
|
152
|
+
updateDom(dom, {}, fiber.props, isSvg);
|
|
153
|
+
|
|
154
|
+
fiber.dom = dom;
|
|
113
155
|
return dom;
|
|
114
156
|
}
|
|
115
157
|
|
|
@@ -126,34 +168,44 @@ function isSvgElement(fiber: Fiber): boolean {
|
|
|
126
168
|
}
|
|
127
169
|
|
|
128
170
|
|
|
129
|
-
/**
|
|
171
|
+
/**
|
|
130
172
|
* Applies updated props to a DOM node.
|
|
131
173
|
* @param {Node} dom - The DOM node to update.
|
|
132
174
|
* @param {object} prevProps - The previous properties.
|
|
133
175
|
* @param {object} nextProps - The new properties.
|
|
134
176
|
*/
|
|
135
177
|
function updateDom(dom: Node, prevProps: any, nextProps: any): void {
|
|
136
|
-
|
|
178
|
+
prevProps = prevProps || {};
|
|
137
179
|
nextProps = nextProps || {};
|
|
138
180
|
|
|
139
181
|
const isSvg = dom instanceof SVGElement;
|
|
182
|
+
|
|
183
|
+
if (dom.nodeType === Node.TEXT_NODE) {
|
|
184
|
+
if (prevProps.nodeValue !== nextProps.nodeValue) {
|
|
185
|
+
(dom as Text).nodeValue = nextProps.nodeValue;
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
140
189
|
|
|
141
|
-
// Handle ref updates
|
|
190
|
+
// Handle ref updates - IMPORTANT: This must come BEFORE event listeners
|
|
142
191
|
if (prevProps.ref && prevProps.ref !== nextProps.ref) {
|
|
143
|
-
prevProps.ref.current
|
|
192
|
+
if (prevProps.ref.current === dom) {
|
|
193
|
+
prevProps.ref.current = null;
|
|
194
|
+
}
|
|
144
195
|
}
|
|
145
196
|
if (nextProps.ref && nextProps.ref !== prevProps.ref) {
|
|
146
197
|
nextProps.ref.current = dom;
|
|
147
|
-
}
|
|
198
|
+
}
|
|
148
199
|
|
|
149
|
-
// Remove old
|
|
200
|
+
// ✅ Remove old event listeners
|
|
150
201
|
Object.keys(prevProps)
|
|
151
|
-
.filter(
|
|
202
|
+
.filter(key => key.startsWith("on"))
|
|
152
203
|
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
|
|
153
204
|
.forEach(name => {
|
|
154
205
|
const eventType = name.toLowerCase().substring(2);
|
|
155
|
-
|
|
156
|
-
|
|
206
|
+
const handler = prevProps[name];
|
|
207
|
+
if (typeof handler === 'function') {
|
|
208
|
+
(dom as Element).removeEventListener(eventType, handler);
|
|
157
209
|
}
|
|
158
210
|
});
|
|
159
211
|
|
|
@@ -166,12 +218,10 @@ function updateDom(dom: Node, prevProps: any, nextProps: any): void {
|
|
|
166
218
|
(dom as Element).removeAttribute('class');
|
|
167
219
|
} else if (name === 'style') {
|
|
168
220
|
(dom as HTMLElement).style.cssText = '';
|
|
221
|
+
} else if (name in dom && !isSvg) {
|
|
222
|
+
(dom as any)[name] = '';
|
|
169
223
|
} else {
|
|
170
|
-
|
|
171
|
-
(dom as Element).removeAttribute(name);
|
|
172
|
-
} else {
|
|
173
|
-
(dom as any)[name] = '';
|
|
174
|
-
}
|
|
224
|
+
(dom as Element).removeAttribute(name);
|
|
175
225
|
}
|
|
176
226
|
});
|
|
177
227
|
|
|
@@ -180,51 +230,52 @@ function updateDom(dom: Node, prevProps: any, nextProps: any): void {
|
|
|
180
230
|
.filter(isProperty)
|
|
181
231
|
.filter(isNew(prevProps, nextProps))
|
|
182
232
|
.forEach(name => {
|
|
233
|
+
const value = nextProps[name];
|
|
234
|
+
|
|
183
235
|
if (name === 'style') {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
(dom as HTMLElement).style[
|
|
236
|
+
if (typeof value === 'string') {
|
|
237
|
+
(dom as HTMLElement).style.cssText = value;
|
|
238
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
239
|
+
for (const [key, val] of Object.entries(value)) {
|
|
240
|
+
const cssKey = key.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
|
241
|
+
(dom as HTMLElement).style[cssKey as any] = val;
|
|
190
242
|
}
|
|
191
243
|
}
|
|
192
244
|
} else if (name === 'className' || name === 'class') {
|
|
193
|
-
(dom as Element).setAttribute('class',
|
|
194
|
-
} else {
|
|
195
|
-
if (
|
|
196
|
-
(dom as Element).setAttribute(name,
|
|
245
|
+
(dom as Element).setAttribute('class', value);
|
|
246
|
+
} else if (typeof value === 'boolean') {
|
|
247
|
+
if (value) {
|
|
248
|
+
(dom as Element).setAttribute(name, '');
|
|
197
249
|
} else {
|
|
198
|
-
(dom as
|
|
250
|
+
(dom as Element).removeAttribute(name);
|
|
199
251
|
}
|
|
252
|
+
} else if (name.includes('-') || isSvg) {
|
|
253
|
+
(dom as Element).setAttribute(name, value);
|
|
254
|
+
} else if (name in dom && !isSvg) {
|
|
255
|
+
try {
|
|
256
|
+
(dom as any)[name] = value;
|
|
257
|
+
} catch {
|
|
258
|
+
(dom as Element).setAttribute(name, value);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
(dom as Element).setAttribute(name, value);
|
|
200
262
|
}
|
|
201
263
|
});
|
|
202
264
|
|
|
203
|
-
// Add new event listeners
|
|
265
|
+
// ✅ Add new event listeners - This is the crucial part!
|
|
204
266
|
Object.keys(nextProps)
|
|
205
|
-
.filter(
|
|
206
|
-
.filter(isNew(prevProps, nextProps))
|
|
207
|
-
.forEach(name => {
|
|
208
|
-
const eventType = name.toLowerCase().substring(2);
|
|
209
|
-
const handler = nextProps[name];
|
|
210
|
-
if (typeof handler === 'function') {
|
|
211
|
-
(dom as Element).addEventListener(eventType, handler);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
Object.keys(nextProps)
|
|
216
|
-
.filter(isEvent)
|
|
267
|
+
.filter(key => key.startsWith("on"))
|
|
217
268
|
.filter(isNew(prevProps, nextProps))
|
|
218
269
|
.forEach(name => {
|
|
219
270
|
const eventType = name.toLowerCase().substring(2);
|
|
220
271
|
const handler = nextProps[name];
|
|
221
272
|
if (typeof handler === 'function') {
|
|
222
|
-
// Remove
|
|
273
|
+
// Remove any existing listener first
|
|
223
274
|
if (prevProps[name]) {
|
|
224
|
-
dom.removeEventListener(eventType, prevProps[name]);
|
|
275
|
+
(dom as Element).removeEventListener(eventType, prevProps[name]);
|
|
225
276
|
}
|
|
226
|
-
// Add new listener
|
|
227
|
-
dom.addEventListener(eventType, handler
|
|
277
|
+
// Add the new listener
|
|
278
|
+
(dom as Element).addEventListener(eventType, handler);
|
|
228
279
|
}
|
|
229
280
|
});
|
|
230
281
|
}
|
|
@@ -236,16 +287,57 @@ function updateDom(dom: Node, prevProps: any, nextProps: any): void {
|
|
|
236
287
|
function commitRoot(): void {
|
|
237
288
|
deletions.forEach(commitWork);
|
|
238
289
|
commitWork(wipRoot.child);
|
|
290
|
+
|
|
291
|
+
// Run effects after DOM is committed
|
|
292
|
+
runEffects(wipRoot);
|
|
293
|
+
|
|
239
294
|
currentRoot = wipRoot;
|
|
240
295
|
wipRoot = null;
|
|
241
296
|
isRenderScheduled = false;
|
|
242
297
|
}
|
|
243
298
|
|
|
299
|
+
function runEffects(fiber: Fiber | null): void {
|
|
300
|
+
if (!fiber) return;
|
|
301
|
+
|
|
302
|
+
// Run effects for this fiber
|
|
303
|
+
if (fiber._pendingEffects) {
|
|
304
|
+
fiber._pendingEffects.forEach(({ callback, cleanup, hookIndex }) => {
|
|
305
|
+
// Run cleanup from previous effect
|
|
306
|
+
if (cleanup) {
|
|
307
|
+
try {
|
|
308
|
+
cleanup();
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.error('Error in effect cleanup:', err);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Run the new effect
|
|
315
|
+
try {
|
|
316
|
+
const newCleanup = callback();
|
|
317
|
+
|
|
318
|
+
// Store the cleanup function in the hook
|
|
319
|
+
if (fiber.hooks && fiber.hooks[hookIndex]) {
|
|
320
|
+
fiber.hooks[hookIndex]._cleanupFn = typeof newCleanup === 'function' ? newCleanup : undefined;
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error('Error in effect:', err);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Clear pending effects
|
|
328
|
+
fiber._pendingEffects = [];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Recursively run effects for children
|
|
332
|
+
runEffects(fiber.child);
|
|
333
|
+
runEffects(fiber.sibling);
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
/**
|
|
245
337
|
* Recursively commits a fiber and its children to the DOM.
|
|
246
338
|
* @param {Fiber} fiber - The fiber to commit.
|
|
247
339
|
*/
|
|
248
|
-
function commitWork(fiber: Fiber | null): void {
|
|
340
|
+
function commitWork(fiber: Fiber | null): void {
|
|
249
341
|
if (!fiber) return;
|
|
250
342
|
|
|
251
343
|
let domParentFiber = fiber.parent;
|
|
@@ -255,46 +347,69 @@ function commitWork(fiber: Fiber | null): void {
|
|
|
255
347
|
const domParent = domParentFiber?.dom ?? null;
|
|
256
348
|
|
|
257
349
|
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
|
|
258
|
-
if (domParent)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
350
|
+
if (domParent) {
|
|
351
|
+
domParent.appendChild(fiber.dom);
|
|
352
|
+
}
|
|
353
|
+
// ✅ Assign ref from fiber
|
|
354
|
+
if (fiber.ref && fiber.dom) {
|
|
355
|
+
assignRef(fiber.ref, fiber.dom);
|
|
263
356
|
}
|
|
357
|
+
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
|
|
358
|
+
const prevProps = fiber.alternate?.props ?? {};
|
|
359
|
+
const nextProps = fiber.props;
|
|
360
|
+
updateDom(fiber.dom, prevProps, nextProps);
|
|
264
361
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
362
|
+
// ✅ Handle ref updates from fiber
|
|
363
|
+
const prevRef = fiber.alternate?.ref;
|
|
364
|
+
const nextRef = fiber.ref;
|
|
365
|
+
|
|
366
|
+
if (prevRef !== nextRef) {
|
|
367
|
+
if (prevRef) {
|
|
368
|
+
assignRef(prevRef, null);
|
|
272
369
|
}
|
|
370
|
+
if (nextRef && fiber.dom) {
|
|
371
|
+
assignRef(nextRef, fiber.dom);
|
|
372
|
+
}
|
|
373
|
+
} else if (nextRef && fiber.dom) {
|
|
374
|
+
// Ensure ref is still set
|
|
375
|
+
assignRef(nextRef, fiber.dom);
|
|
273
376
|
}
|
|
274
|
-
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
|
|
275
|
-
updateDom(fiber.dom, fiber.alternate?.props ?? {}, fiber.props);
|
|
276
377
|
} else if (fiber.effectTag === "DELETION") {
|
|
277
378
|
commitDeletion(fiber);
|
|
379
|
+
return;
|
|
278
380
|
}
|
|
279
381
|
|
|
280
382
|
commitWork(fiber.child);
|
|
281
383
|
commitWork(fiber.sibling);
|
|
282
384
|
}
|
|
283
385
|
|
|
386
|
+
// ✅ Helper function to assign refs safely
|
|
387
|
+
function assignRef(ref: any, dom: Node | null): void {
|
|
388
|
+
if (typeof ref === 'function') {
|
|
389
|
+
ref(dom);
|
|
390
|
+
} else if (ref && typeof ref === 'object') {
|
|
391
|
+
ref.current = dom;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
284
395
|
|
|
285
396
|
/**
|
|
286
397
|
* Recursively removes a fiber and its children from the DOM.
|
|
287
398
|
* @param {Fiber} fiber - The fiber to remove.
|
|
288
399
|
*/
|
|
289
|
-
|
|
290
|
-
if (!fiber)
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
400
|
+
function commitDeletion(fiber: Fiber | null): void {
|
|
401
|
+
if (!fiber) return;
|
|
293
402
|
|
|
294
|
-
// Clear refs
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
403
|
+
// Clear refs recursively
|
|
404
|
+
const clearRefs = (f: Fiber) => {
|
|
405
|
+
if (f.ref) {
|
|
406
|
+
assignRef(f.ref, null);
|
|
407
|
+
}
|
|
408
|
+
if (f.child) clearRefs(f.child);
|
|
409
|
+
if (f.sibling) clearRefs(f.sibling);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
clearRefs(fiber);
|
|
298
413
|
|
|
299
414
|
if (fiber.dom) {
|
|
300
415
|
if (fiber.dom.parentNode) {
|
|
@@ -329,7 +444,8 @@ export function render(element: VNode, container: Node): void {
|
|
|
329
444
|
* The main work loop for rendering and reconciliation.
|
|
330
445
|
*/
|
|
331
446
|
function workLoop(): void {
|
|
332
|
-
|
|
447
|
+
// If there's a scheduled render but no wipRoot, create one
|
|
448
|
+
if (!nextUnitOfWork && !wipRoot && currentRoot) {
|
|
333
449
|
wipRoot = {
|
|
334
450
|
dom: currentRoot.dom,
|
|
335
451
|
props: currentRoot.props,
|
|
@@ -338,11 +454,13 @@ function workLoop(): void {
|
|
|
338
454
|
deletions = [];
|
|
339
455
|
nextUnitOfWork = wipRoot;
|
|
340
456
|
}
|
|
341
|
-
|
|
457
|
+
|
|
458
|
+
// Perform work
|
|
342
459
|
while (nextUnitOfWork) {
|
|
343
460
|
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
344
461
|
}
|
|
345
462
|
|
|
463
|
+
// Commit changes if we've finished all work
|
|
346
464
|
if (!nextUnitOfWork && wipRoot) {
|
|
347
465
|
commitRoot();
|
|
348
466
|
}
|
|
@@ -382,22 +500,50 @@ function performUnitOfWork(fiber: Fiber): Fiber | null {
|
|
|
382
500
|
* @param {Fiber} fiber - The function component fiber to update.
|
|
383
501
|
*/
|
|
384
502
|
function updateFunctionComponent(fiber: Fiber) {
|
|
503
|
+
// Store the current fiber globally for hooks
|
|
385
504
|
wipFiber = fiber;
|
|
386
505
|
hookIndex = 0;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
506
|
+
|
|
507
|
+
// Initialize hooks array if needed
|
|
508
|
+
if (!fiber.hooks) {
|
|
509
|
+
fiber.hooks = [];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Copy hooks from alternate but ensure they're fresh
|
|
513
|
+
if (fiber.alternate?.hooks) {
|
|
514
|
+
// Create new hooks array with same structure
|
|
515
|
+
fiber.hooks = fiber.alternate.hooks.map(altHook => {
|
|
516
|
+
// Create a shallow copy to avoid mutation issues
|
|
517
|
+
const newHook = { ...altHook };
|
|
518
|
+
return newHook;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
391
521
|
|
|
392
|
-
|
|
522
|
+
// Call the component function
|
|
523
|
+
const children = (fiber.type as Function)(fiber.props);
|
|
524
|
+
|
|
525
|
+
// Normalize and reconcile children
|
|
526
|
+
const normalizedChildren = normalizeChildren(children, fiber);
|
|
527
|
+
reconcileChildren(fiber, normalizedChildren);
|
|
528
|
+
|
|
529
|
+
// Clean up
|
|
530
|
+
wipFiber = null;
|
|
393
531
|
}
|
|
394
532
|
function normalizeChildren(children: any, parentFiber: Fiber): VNode[] {
|
|
395
533
|
if (!children) return [];
|
|
534
|
+
|
|
535
|
+
// Handle arrays, single elements, and conditional rendering
|
|
396
536
|
let arr = Array.isArray(children) ? children : [children];
|
|
397
|
-
|
|
537
|
+
|
|
398
538
|
return arr.flatMap((child, index) => {
|
|
399
|
-
|
|
400
|
-
if (
|
|
539
|
+
// Skip null, undefined, false, true (conditional rendering)
|
|
540
|
+
if (child == null || typeof child === "boolean") {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
545
|
+
return [createTextElement(String(child))];
|
|
546
|
+
}
|
|
401
547
|
|
|
402
548
|
// Ensure every child has a stable key
|
|
403
549
|
const key = child.key ?? child.props?.id ?? `${parentFiber.key ?? "root"}-${index}`;
|
|
@@ -425,63 +571,82 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
|
|
|
425
571
|
let oldFiber = wipFiber.alternate?.child;
|
|
426
572
|
let prevSibling: Fiber | null = null;
|
|
427
573
|
|
|
574
|
+
// Build a map of existing fibers by key
|
|
428
575
|
const existingFibers = new Map<string | number | null, Fiber>();
|
|
429
576
|
while (oldFiber) {
|
|
430
|
-
const key = oldFiber.key ?? index
|
|
577
|
+
const key = oldFiber.key ?? `index-${index}`;
|
|
431
578
|
existingFibers.set(key, oldFiber);
|
|
432
579
|
oldFiber = oldFiber.sibling;
|
|
433
580
|
index++;
|
|
434
581
|
}
|
|
435
582
|
|
|
436
583
|
index = 0;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
584
|
+
let newChildFiber: Fiber | null = null;
|
|
585
|
+
|
|
586
|
+
// Process each element
|
|
587
|
+
for (let i = 0; i < elements.length; i++) {
|
|
588
|
+
const element = elements[i];
|
|
589
|
+
|
|
590
|
+
// Skip null/false/undefined (conditional rendering)
|
|
591
|
+
if (element == null) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const key = element.key ?? `index-${index}`;
|
|
440
596
|
const oldFiber = existingFibers.get(key);
|
|
441
|
-
|
|
442
|
-
const sameType = oldFiber && element
|
|
443
|
-
|
|
444
|
-
|
|
597
|
+
|
|
598
|
+
const sameType = oldFiber && element.type === oldFiber.type;
|
|
599
|
+
|
|
445
600
|
if (sameType) {
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
601
|
+
// Update existing fiber
|
|
602
|
+
newChildFiber = {
|
|
603
|
+
type: oldFiber.type,
|
|
449
604
|
props: element.props,
|
|
605
|
+
dom: oldFiber.dom,
|
|
450
606
|
parent: wipFiber,
|
|
451
607
|
alternate: oldFiber,
|
|
452
608
|
effectTag: "UPDATE",
|
|
453
|
-
|
|
609
|
+
key,
|
|
610
|
+
ref: element.ref,
|
|
611
|
+
hooks: oldFiber.hooks,
|
|
454
612
|
};
|
|
613
|
+
|
|
455
614
|
existingFibers.delete(key);
|
|
456
|
-
} else
|
|
615
|
+
} else {
|
|
457
616
|
// Create new fiber
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
617
|
+
if (element) {
|
|
618
|
+
newChildFiber = {
|
|
619
|
+
type: element.type,
|
|
620
|
+
props: element.props,
|
|
621
|
+
dom: null,
|
|
622
|
+
parent: wipFiber,
|
|
623
|
+
alternate: null,
|
|
624
|
+
effectTag: "PLACEMENT",
|
|
625
|
+
key,
|
|
626
|
+
ref: element.ref,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Mark old fiber for deletion if it exists
|
|
631
|
+
if (oldFiber) {
|
|
632
|
+
oldFiber.effectTag = "DELETION";
|
|
633
|
+
deletions.push(oldFiber);
|
|
634
|
+
}
|
|
473
635
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
636
|
+
|
|
637
|
+
// Link the new fiber into the tree
|
|
638
|
+
if (newChildFiber) {
|
|
639
|
+
if (index === 0) {
|
|
640
|
+
wipFiber.child = newChildFiber;
|
|
641
|
+
} else if (prevSibling) {
|
|
642
|
+
prevSibling.sibling = newChildFiber;
|
|
643
|
+
}
|
|
644
|
+
prevSibling = newChildFiber;
|
|
645
|
+
index++;
|
|
479
646
|
}
|
|
480
|
-
|
|
481
|
-
if (newFiber) prevSibling = newFiber;
|
|
482
647
|
}
|
|
483
|
-
|
|
484
|
-
// Mark remaining old fibers for deletion
|
|
648
|
+
|
|
649
|
+
// Mark any remaining old fibers for deletion
|
|
485
650
|
existingFibers.forEach(fiber => {
|
|
486
651
|
fiber.effectTag = "DELETION";
|
|
487
652
|
deletions.push(fiber);
|
|
@@ -495,20 +660,33 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
|
|
|
495
660
|
* @param {...any} children - The element's children.
|
|
496
661
|
* @returns {VNode} The created virtual DOM element.
|
|
497
662
|
*/
|
|
498
|
-
|
|
663
|
+
export function createElement(type: string | Function, props?: any, ...children: any[]): VNode {
|
|
499
664
|
const rawChildren = children.flat().filter(c => c != null && typeof c !== "boolean");
|
|
500
665
|
const normalizedChildren = rawChildren.map((child, i) => {
|
|
501
666
|
if (typeof child === "object") return child;
|
|
502
667
|
return createTextElement(String(child));
|
|
503
668
|
});
|
|
504
669
|
|
|
670
|
+
// Extract ref from props (if it exists)
|
|
671
|
+
const ref = props?.ref;
|
|
672
|
+
|
|
673
|
+
// Create a new props object without the ref
|
|
674
|
+
const elementProps = { ...props };
|
|
675
|
+
if ('ref' in elementProps) {
|
|
676
|
+
delete elementProps.ref;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Add children back
|
|
680
|
+
elementProps.children = normalizedChildren;
|
|
681
|
+
|
|
505
682
|
return {
|
|
506
683
|
type,
|
|
507
|
-
props:
|
|
684
|
+
props: elementProps,
|
|
508
685
|
key: props?.key ?? props?.id ?? null,
|
|
686
|
+
// Store the ref separately on the VNode
|
|
687
|
+
ref,
|
|
509
688
|
};
|
|
510
689
|
}
|
|
511
|
-
|
|
512
690
|
/**
|
|
513
691
|
* Creates a text virtual DOM element.
|
|
514
692
|
* @param {string} text - The text content.
|
|
@@ -524,50 +702,122 @@ function createTextElement(text: string): VNode {
|
|
|
524
702
|
};
|
|
525
703
|
}
|
|
526
704
|
|
|
705
|
+
export function useStableRef<T>(initialValue: T | null = null): { current: T | null } {
|
|
706
|
+
const ref = useRef(initialValue);
|
|
707
|
+
|
|
708
|
+
// Use effect to ensure ref is cleaned up on unmount
|
|
709
|
+
useEffect(() => {
|
|
710
|
+
return () => {
|
|
711
|
+
// Cleanup ref when component unmounts
|
|
712
|
+
if (ref.current !== null) {
|
|
713
|
+
ref.current = null;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}, []);
|
|
717
|
+
|
|
718
|
+
return ref;
|
|
719
|
+
}
|
|
527
720
|
/**
|
|
528
721
|
* A React-like useState hook for managing component state.
|
|
529
722
|
* @template T
|
|
530
723
|
* @param {T|(() => T)} initial - The initial state value or initializer function.
|
|
531
724
|
* @returns {[T, (action: T | ((prevState: T) => T)) => void]} A stateful value and a function to update it.
|
|
532
725
|
*/
|
|
533
|
-
|
|
534
726
|
|
|
535
|
-
|
|
536
|
-
export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
|
|
727
|
+
export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
|
|
537
728
|
if (!wipFiber) {
|
|
538
729
|
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
539
730
|
}
|
|
540
731
|
|
|
541
|
-
|
|
732
|
+
const currentHookIndex = hookIndex;
|
|
733
|
+
const currentFiber = wipFiber; // Capture current fiber
|
|
734
|
+
|
|
735
|
+
let hook = currentFiber.hooks[currentHookIndex];
|
|
736
|
+
|
|
542
737
|
if (!hook) {
|
|
543
738
|
hook = {
|
|
544
739
|
state: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
545
740
|
queue: [],
|
|
546
741
|
_needsUpdate: false
|
|
547
742
|
};
|
|
548
|
-
|
|
743
|
+
currentFiber.hooks[currentHookIndex] = hook;
|
|
549
744
|
}
|
|
550
745
|
|
|
746
|
+
// Create setState that captures current hook and fiber
|
|
551
747
|
const setState = (action: T | ((prevState: T) => T)) => {
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
hook.state = newState;
|
|
748
|
+
// Use setTimeout to ensure we're outside the current render cycle
|
|
749
|
+
setTimeout(() => {
|
|
750
|
+
// Re-find the hook in the current fiber (in case it moved)
|
|
751
|
+
const fiber = currentRoot || wipRoot;
|
|
752
|
+
if (!fiber) return;
|
|
558
753
|
|
|
559
|
-
//
|
|
754
|
+
// Find the component fiber that owns this hook
|
|
755
|
+
let targetFiber = findFiberWithHook(fiber, currentFiber, currentHookIndex);
|
|
756
|
+
if (!targetFiber || !targetFiber.hooks) return;
|
|
560
757
|
|
|
561
|
-
|
|
562
|
-
|
|
758
|
+
const targetHook = targetFiber.hooks[currentHookIndex];
|
|
759
|
+
if (!targetHook) return;
|
|
563
760
|
|
|
564
|
-
//
|
|
565
|
-
|
|
761
|
+
// Calculate new state
|
|
762
|
+
const newState = typeof action === "function"
|
|
763
|
+
? (action as (prevState: T) => T)(targetHook.state)
|
|
764
|
+
: action;
|
|
765
|
+
|
|
766
|
+
if (!Object.is(targetHook.state, newState)) {
|
|
767
|
+
targetHook.state = newState;
|
|
768
|
+
targetHook._needsUpdate = true;
|
|
769
|
+
|
|
770
|
+
// Schedule a re-render
|
|
771
|
+
scheduleRender();
|
|
772
|
+
}
|
|
773
|
+
}, 0);
|
|
566
774
|
};
|
|
567
775
|
|
|
568
776
|
hookIndex++;
|
|
569
777
|
return [hook.state, setState];
|
|
570
778
|
}
|
|
779
|
+
|
|
780
|
+
// Helper to find the fiber containing a specific hook
|
|
781
|
+
function findFiberWithHook(root: Fiber, targetFiber: Fiber, hookIndex: number): Fiber | null {
|
|
782
|
+
// Simple BFS to find the fiber
|
|
783
|
+
let queue: Fiber[] = [root];
|
|
784
|
+
|
|
785
|
+
while (queue.length > 0) {
|
|
786
|
+
const fiber = queue.shift()!;
|
|
787
|
+
|
|
788
|
+
// Check if this is our target fiber
|
|
789
|
+
if (fiber === targetFiber ||
|
|
790
|
+
(fiber.type === targetFiber.type &&
|
|
791
|
+
fiber.key === targetFiber.key)) {
|
|
792
|
+
return fiber;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Add children to queue
|
|
796
|
+
if (fiber.child) queue.push(fiber.child);
|
|
797
|
+
if (fiber.sibling) queue.push(fiber.sibling);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Schedules a re-render of the entire app
|
|
805
|
+
*/
|
|
806
|
+
function scheduleRender(): void {
|
|
807
|
+
if (!currentRoot || !currentRoot.dom) return;
|
|
808
|
+
|
|
809
|
+
// Schedule a new render starting from the current root
|
|
810
|
+
wipRoot = {
|
|
811
|
+
dom: currentRoot.dom,
|
|
812
|
+
props: currentRoot.props,
|
|
813
|
+
alternate: currentRoot,
|
|
814
|
+
};
|
|
815
|
+
deletions = [];
|
|
816
|
+
nextUnitOfWork = wipRoot;
|
|
817
|
+
|
|
818
|
+
// Start the work loop on next animation frame
|
|
819
|
+
requestAnimationFrame(workLoop);
|
|
820
|
+
}
|
|
571
821
|
|
|
572
822
|
/**
|
|
573
823
|
* A React-like useEffect hook for side effects.
|
|
@@ -590,15 +840,14 @@ export function useEffect(callback: Function, deps?: any[]): void {
|
|
|
590
840
|
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
|
|
591
841
|
|
|
592
842
|
if (hasChanged) {
|
|
593
|
-
|
|
594
|
-
hook._cleanupFn();
|
|
595
|
-
}
|
|
843
|
+
// Schedule effect to run after render
|
|
596
844
|
setTimeout(() => {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
845
|
+
if (hook._cleanupFn) {
|
|
846
|
+
hook._cleanupFn();
|
|
847
|
+
}
|
|
848
|
+
const cleanup = callback();
|
|
849
|
+
if (typeof cleanup === 'function') {
|
|
850
|
+
hook._cleanupFn = cleanup;
|
|
602
851
|
}
|
|
603
852
|
}, 0);
|
|
604
853
|
}
|
|
@@ -637,13 +886,23 @@ export function Match({ when, children }: { when: boolean, children: VNode[] }):
|
|
|
637
886
|
* @param {Function} fn - The function component to wrap.
|
|
638
887
|
* @returns {Function} A function that creates a VNode when called with props.
|
|
639
888
|
*/
|
|
640
|
-
export function component<P extends object>(
|
|
641
|
-
fn: (props: P) => VNode | VNode[]
|
|
642
|
-
): (props: P & { key?: string | number }) => VNode {
|
|
643
|
-
return (props: P & { key?: string | number }) => {
|
|
644
|
-
|
|
889
|
+
export function component<P extends object>(
|
|
890
|
+
fn: (props: P & { children?: any }) => VNode | VNode[]
|
|
891
|
+
): (props: P & { key?: string | number; children?: any }) => VNode {
|
|
892
|
+
return (props: P & { key?: string | number; children?: any }) => {
|
|
893
|
+
// Merge key if needed
|
|
894
|
+
const { key, ...rest } = props;
|
|
895
|
+
// Call the component function directly
|
|
896
|
+
const vnode = fn(rest as P & { children?: any });
|
|
897
|
+
|
|
898
|
+
// Attach the key to the VNode
|
|
899
|
+
if (vnode && typeof vnode === "object") {
|
|
900
|
+
vnode.key = key;
|
|
901
|
+
}
|
|
902
|
+
return vnode;
|
|
645
903
|
};
|
|
646
904
|
}
|
|
905
|
+
|
|
647
906
|
export function Show({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
|
|
648
907
|
return when ? children : null;
|
|
649
908
|
}
|
|
@@ -669,21 +928,22 @@ declare global {
|
|
|
669
928
|
* @param {T} initial - The initial reference value.
|
|
670
929
|
* @returns {{current: T}} A mutable ref object.
|
|
671
930
|
*/
|
|
672
|
-
export function useRef<T>(initial: T): { current: T } {
|
|
931
|
+
export function useRef<T>(initial: T | null = null): { current: T | null } {
|
|
673
932
|
if (!wipFiber) {
|
|
674
933
|
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
675
934
|
}
|
|
676
935
|
|
|
677
936
|
let hook = wipFiber.hooks[hookIndex];
|
|
678
937
|
if (!hook) {
|
|
679
|
-
hook = { current: initial
|
|
938
|
+
hook = { current: initial };
|
|
680
939
|
wipFiber.hooks[hookIndex] = hook;
|
|
681
940
|
}
|
|
682
941
|
|
|
683
942
|
hookIndex++;
|
|
684
|
-
return hook;
|
|
943
|
+
return hook as { current: T | null };
|
|
685
944
|
}
|
|
686
945
|
|
|
946
|
+
|
|
687
947
|
/**
|
|
688
948
|
* A React-like useLayoutEffect hook that runs synchronously after DOM mutations.
|
|
689
949
|
* @param {Function} callback - The effect callback.
|
|
@@ -1047,14 +1307,27 @@ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T)
|
|
|
1047
1307
|
export function useOnClickOutside(ref: { current: HTMLElement | null }, handler: Function): void {
|
|
1048
1308
|
useEffect(() => {
|
|
1049
1309
|
const listener = (event: MouseEvent) => {
|
|
1050
|
-
|
|
1310
|
+
// Create a stable reference to the current ref value
|
|
1311
|
+
const currentRef = ref.current;
|
|
1312
|
+
console.log(currentRef)
|
|
1313
|
+
|
|
1314
|
+
if (!currentRef || !event.target) {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
console.log(currentRef)
|
|
1319
|
+
|
|
1320
|
+
// Check if click is outside
|
|
1321
|
+
if (!currentRef.contains(event.target as Node)) {
|
|
1051
1322
|
handler(event);
|
|
1052
1323
|
}
|
|
1053
1324
|
};
|
|
1054
|
-
|
|
1325
|
+
|
|
1326
|
+
// Use capture phase to ensure we catch the event
|
|
1327
|
+
document.addEventListener("mousedown", listener, true);
|
|
1328
|
+
|
|
1055
1329
|
return () => {
|
|
1056
|
-
document.removeEventListener("mousedown", listener);
|
|
1330
|
+
document.removeEventListener("mousedown", listener, true);
|
|
1057
1331
|
};
|
|
1058
|
-
}, [ref, handler]);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1332
|
+
}, [ref, handler]); // Keep ref in dependencies
|
|
1333
|
+
}
|