vaderjs 2.3.10 → 2.3.12
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 +342 -152
- package/main.js +304 -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);
|
|
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);
|
|
199
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(
|
|
267
|
+
.filter(key => key.startsWith("on"))
|
|
206
268
|
.filter(isNew(prevProps, nextProps))
|
|
207
269
|
.forEach(name => {
|
|
208
270
|
const eventType = name.toLowerCase().substring(2);
|
|
209
271
|
const handler = nextProps[name];
|
|
210
272
|
if (typeof handler === 'function') {
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
Object.keys(nextProps)
|
|
216
|
-
.filter(isEvent)
|
|
217
|
-
.filter(isNew(prevProps, nextProps))
|
|
218
|
-
.forEach(name => {
|
|
219
|
-
const eventType = name.toLowerCase().substring(2);
|
|
220
|
-
const handler = nextProps[name];
|
|
221
|
-
if (typeof handler === 'function') {
|
|
222
|
-
// Remove old listener first if it exists
|
|
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
|
}
|
|
@@ -245,7 +296,7 @@ function commitRoot(): void {
|
|
|
245
296
|
* Recursively commits a fiber and its children to the DOM.
|
|
246
297
|
* @param {Fiber} fiber - The fiber to commit.
|
|
247
298
|
*/
|
|
248
|
-
function commitWork(fiber: Fiber | null): void {
|
|
299
|
+
function commitWork(fiber: Fiber | null): void {
|
|
249
300
|
if (!fiber) return;
|
|
250
301
|
|
|
251
302
|
let domParentFiber = fiber.parent;
|
|
@@ -255,46 +306,69 @@ function commitWork(fiber: Fiber | null): void {
|
|
|
255
306
|
const domParent = domParentFiber?.dom ?? null;
|
|
256
307
|
|
|
257
308
|
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
|
|
258
|
-
if (domParent)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
309
|
+
if (domParent) {
|
|
310
|
+
domParent.appendChild(fiber.dom);
|
|
311
|
+
}
|
|
312
|
+
// ✅ Assign ref from fiber
|
|
313
|
+
if (fiber.ref && fiber.dom) {
|
|
314
|
+
assignRef(fiber.ref, fiber.dom);
|
|
263
315
|
}
|
|
316
|
+
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
|
|
317
|
+
const prevProps = fiber.alternate?.props ?? {};
|
|
318
|
+
const nextProps = fiber.props;
|
|
319
|
+
updateDom(fiber.dom, prevProps, nextProps);
|
|
264
320
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
321
|
+
// ✅ Handle ref updates from fiber
|
|
322
|
+
const prevRef = fiber.alternate?.ref;
|
|
323
|
+
const nextRef = fiber.ref;
|
|
324
|
+
|
|
325
|
+
if (prevRef !== nextRef) {
|
|
326
|
+
if (prevRef) {
|
|
327
|
+
assignRef(prevRef, null);
|
|
328
|
+
}
|
|
329
|
+
if (nextRef && fiber.dom) {
|
|
330
|
+
assignRef(nextRef, fiber.dom);
|
|
272
331
|
}
|
|
332
|
+
} else if (nextRef && fiber.dom) {
|
|
333
|
+
// Ensure ref is still set
|
|
334
|
+
assignRef(nextRef, fiber.dom);
|
|
273
335
|
}
|
|
274
|
-
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
|
|
275
|
-
updateDom(fiber.dom, fiber.alternate?.props ?? {}, fiber.props);
|
|
276
336
|
} else if (fiber.effectTag === "DELETION") {
|
|
277
337
|
commitDeletion(fiber);
|
|
338
|
+
return;
|
|
278
339
|
}
|
|
279
340
|
|
|
280
341
|
commitWork(fiber.child);
|
|
281
342
|
commitWork(fiber.sibling);
|
|
282
343
|
}
|
|
283
344
|
|
|
345
|
+
// ✅ Helper function to assign refs safely
|
|
346
|
+
function assignRef(ref: any, dom: Node | null): void {
|
|
347
|
+
if (typeof ref === 'function') {
|
|
348
|
+
ref(dom);
|
|
349
|
+
} else if (ref && typeof ref === 'object') {
|
|
350
|
+
ref.current = dom;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
284
354
|
|
|
285
355
|
/**
|
|
286
356
|
* Recursively removes a fiber and its children from the DOM.
|
|
287
357
|
* @param {Fiber} fiber - The fiber to remove.
|
|
288
358
|
*/
|
|
289
|
-
|
|
290
|
-
if (!fiber)
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
359
|
+
function commitDeletion(fiber: Fiber | null): void {
|
|
360
|
+
if (!fiber) return;
|
|
293
361
|
|
|
294
|
-
// Clear refs
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
362
|
+
// Clear refs recursively
|
|
363
|
+
const clearRefs = (f: Fiber) => {
|
|
364
|
+
if (f.ref) {
|
|
365
|
+
assignRef(f.ref, null);
|
|
366
|
+
}
|
|
367
|
+
if (f.child) clearRefs(f.child);
|
|
368
|
+
if (f.sibling) clearRefs(f.sibling);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
clearRefs(fiber);
|
|
298
372
|
|
|
299
373
|
if (fiber.dom) {
|
|
300
374
|
if (fiber.dom.parentNode) {
|
|
@@ -329,7 +403,8 @@ export function render(element: VNode, container: Node): void {
|
|
|
329
403
|
* The main work loop for rendering and reconciliation.
|
|
330
404
|
*/
|
|
331
405
|
function workLoop(): void {
|
|
332
|
-
|
|
406
|
+
// If there's a scheduled render but no wipRoot, create one
|
|
407
|
+
if (!nextUnitOfWork && !wipRoot && currentRoot) {
|
|
333
408
|
wipRoot = {
|
|
334
409
|
dom: currentRoot.dom,
|
|
335
410
|
props: currentRoot.props,
|
|
@@ -338,11 +413,13 @@ function workLoop(): void {
|
|
|
338
413
|
deletions = [];
|
|
339
414
|
nextUnitOfWork = wipRoot;
|
|
340
415
|
}
|
|
341
|
-
|
|
416
|
+
|
|
417
|
+
// Perform work
|
|
342
418
|
while (nextUnitOfWork) {
|
|
343
419
|
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
344
420
|
}
|
|
345
421
|
|
|
422
|
+
// Commit changes if we've finished all work
|
|
346
423
|
if (!nextUnitOfWork && wipRoot) {
|
|
347
424
|
commitRoot();
|
|
348
425
|
}
|
|
@@ -382,22 +459,45 @@ function performUnitOfWork(fiber: Fiber): Fiber | null {
|
|
|
382
459
|
* @param {Fiber} fiber - The function component fiber to update.
|
|
383
460
|
*/
|
|
384
461
|
function updateFunctionComponent(fiber: Fiber) {
|
|
462
|
+
// Store the current fiber globally for hooks
|
|
385
463
|
wipFiber = fiber;
|
|
386
464
|
hookIndex = 0;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
465
|
+
|
|
466
|
+
// Initialize hooks array if needed
|
|
467
|
+
if (!fiber.hooks) {
|
|
468
|
+
fiber.hooks = [];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Copy hooks from alternate if it exists
|
|
472
|
+
if (fiber.alternate?.hooks) {
|
|
473
|
+
fiber.hooks = fiber.alternate.hooks.map(hook => ({ ...hook }));
|
|
474
|
+
}
|
|
391
475
|
|
|
392
|
-
|
|
476
|
+
// Call the component function
|
|
477
|
+
const children = (fiber.type as Function)(fiber.props);
|
|
478
|
+
|
|
479
|
+
// Normalize and reconcile children
|
|
480
|
+
const normalizedChildren = normalizeChildren(children, fiber);
|
|
481
|
+
reconcileChildren(fiber, normalizedChildren);
|
|
482
|
+
|
|
483
|
+
// Clean up
|
|
484
|
+
wipFiber = null;
|
|
393
485
|
}
|
|
394
486
|
function normalizeChildren(children: any, parentFiber: Fiber): VNode[] {
|
|
395
487
|
if (!children) return [];
|
|
488
|
+
|
|
489
|
+
// Handle arrays, single elements, and conditional rendering
|
|
396
490
|
let arr = Array.isArray(children) ? children : [children];
|
|
397
|
-
|
|
491
|
+
|
|
398
492
|
return arr.flatMap((child, index) => {
|
|
399
|
-
|
|
400
|
-
if (
|
|
493
|
+
// Skip null, undefined, false, true (conditional rendering)
|
|
494
|
+
if (child == null || typeof child === "boolean") {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
499
|
+
return [createTextElement(String(child))];
|
|
500
|
+
}
|
|
401
501
|
|
|
402
502
|
// Ensure every child has a stable key
|
|
403
503
|
const key = child.key ?? child.props?.id ?? `${parentFiber.key ?? "root"}-${index}`;
|
|
@@ -425,63 +525,82 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
|
|
|
425
525
|
let oldFiber = wipFiber.alternate?.child;
|
|
426
526
|
let prevSibling: Fiber | null = null;
|
|
427
527
|
|
|
528
|
+
// Build a map of existing fibers by key
|
|
428
529
|
const existingFibers = new Map<string | number | null, Fiber>();
|
|
429
530
|
while (oldFiber) {
|
|
430
|
-
const key = oldFiber.key ?? index
|
|
531
|
+
const key = oldFiber.key ?? `index-${index}`;
|
|
431
532
|
existingFibers.set(key, oldFiber);
|
|
432
533
|
oldFiber = oldFiber.sibling;
|
|
433
534
|
index++;
|
|
434
535
|
}
|
|
435
536
|
|
|
436
537
|
index = 0;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
538
|
+
let newChildFiber: Fiber | null = null;
|
|
539
|
+
|
|
540
|
+
// Process each element
|
|
541
|
+
for (let i = 0; i < elements.length; i++) {
|
|
542
|
+
const element = elements[i];
|
|
543
|
+
|
|
544
|
+
// Skip null/false/undefined (conditional rendering)
|
|
545
|
+
if (element == null) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const key = element.key ?? `index-${index}`;
|
|
440
550
|
const oldFiber = existingFibers.get(key);
|
|
441
|
-
|
|
442
|
-
const sameType = oldFiber && element
|
|
443
|
-
|
|
444
|
-
|
|
551
|
+
|
|
552
|
+
const sameType = oldFiber && element.type === oldFiber.type;
|
|
553
|
+
|
|
445
554
|
if (sameType) {
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
555
|
+
// Update existing fiber
|
|
556
|
+
newChildFiber = {
|
|
557
|
+
type: oldFiber.type,
|
|
449
558
|
props: element.props,
|
|
559
|
+
dom: oldFiber.dom,
|
|
450
560
|
parent: wipFiber,
|
|
451
561
|
alternate: oldFiber,
|
|
452
562
|
effectTag: "UPDATE",
|
|
453
|
-
|
|
563
|
+
key,
|
|
564
|
+
ref: element.ref,
|
|
565
|
+
hooks: oldFiber.hooks,
|
|
454
566
|
};
|
|
567
|
+
|
|
455
568
|
existingFibers.delete(key);
|
|
456
|
-
} else
|
|
569
|
+
} else {
|
|
457
570
|
// Create new fiber
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
571
|
+
if (element) {
|
|
572
|
+
newChildFiber = {
|
|
573
|
+
type: element.type,
|
|
574
|
+
props: element.props,
|
|
575
|
+
dom: null,
|
|
576
|
+
parent: wipFiber,
|
|
577
|
+
alternate: null,
|
|
578
|
+
effectTag: "PLACEMENT",
|
|
579
|
+
key,
|
|
580
|
+
ref: element.ref,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Mark old fiber for deletion if it exists
|
|
585
|
+
if (oldFiber) {
|
|
586
|
+
oldFiber.effectTag = "DELETION";
|
|
587
|
+
deletions.push(oldFiber);
|
|
588
|
+
}
|
|
473
589
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
590
|
+
|
|
591
|
+
// Link the new fiber into the tree
|
|
592
|
+
if (newChildFiber) {
|
|
593
|
+
if (index === 0) {
|
|
594
|
+
wipFiber.child = newChildFiber;
|
|
595
|
+
} else if (prevSibling) {
|
|
596
|
+
prevSibling.sibling = newChildFiber;
|
|
597
|
+
}
|
|
598
|
+
prevSibling = newChildFiber;
|
|
599
|
+
index++;
|
|
479
600
|
}
|
|
480
|
-
|
|
481
|
-
if (newFiber) prevSibling = newFiber;
|
|
482
601
|
}
|
|
483
|
-
|
|
484
|
-
// Mark remaining old fibers for deletion
|
|
602
|
+
|
|
603
|
+
// Mark any remaining old fibers for deletion
|
|
485
604
|
existingFibers.forEach(fiber => {
|
|
486
605
|
fiber.effectTag = "DELETION";
|
|
487
606
|
deletions.push(fiber);
|
|
@@ -495,20 +614,33 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
|
|
|
495
614
|
* @param {...any} children - The element's children.
|
|
496
615
|
* @returns {VNode} The created virtual DOM element.
|
|
497
616
|
*/
|
|
498
|
-
|
|
617
|
+
export function createElement(type: string | Function, props?: any, ...children: any[]): VNode {
|
|
499
618
|
const rawChildren = children.flat().filter(c => c != null && typeof c !== "boolean");
|
|
500
619
|
const normalizedChildren = rawChildren.map((child, i) => {
|
|
501
620
|
if (typeof child === "object") return child;
|
|
502
621
|
return createTextElement(String(child));
|
|
503
622
|
});
|
|
504
623
|
|
|
624
|
+
// Extract ref from props (if it exists)
|
|
625
|
+
const ref = props?.ref;
|
|
626
|
+
|
|
627
|
+
// Create a new props object without the ref
|
|
628
|
+
const elementProps = { ...props };
|
|
629
|
+
if ('ref' in elementProps) {
|
|
630
|
+
delete elementProps.ref;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Add children back
|
|
634
|
+
elementProps.children = normalizedChildren;
|
|
635
|
+
|
|
505
636
|
return {
|
|
506
637
|
type,
|
|
507
|
-
props:
|
|
638
|
+
props: elementProps,
|
|
508
639
|
key: props?.key ?? props?.id ?? null,
|
|
640
|
+
// Store the ref separately on the VNode
|
|
641
|
+
ref,
|
|
509
642
|
};
|
|
510
643
|
}
|
|
511
|
-
|
|
512
644
|
/**
|
|
513
645
|
* Creates a text virtual DOM element.
|
|
514
646
|
* @param {string} text - The text content.
|
|
@@ -524,6 +656,21 @@ function createTextElement(text: string): VNode {
|
|
|
524
656
|
};
|
|
525
657
|
}
|
|
526
658
|
|
|
659
|
+
export function useStableRef<T>(initialValue: T | null = null): { current: T | null } {
|
|
660
|
+
const ref = useRef(initialValue);
|
|
661
|
+
|
|
662
|
+
// Use effect to ensure ref is cleaned up on unmount
|
|
663
|
+
useEffect(() => {
|
|
664
|
+
return () => {
|
|
665
|
+
// Cleanup ref when component unmounts
|
|
666
|
+
if (ref.current !== null) {
|
|
667
|
+
ref.current = null;
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}, []);
|
|
671
|
+
|
|
672
|
+
return ref;
|
|
673
|
+
}
|
|
527
674
|
/**
|
|
528
675
|
* A React-like useState hook for managing component state.
|
|
529
676
|
* @template T
|
|
@@ -533,41 +680,60 @@ function createTextElement(text: string): VNode {
|
|
|
533
680
|
|
|
534
681
|
|
|
535
682
|
|
|
536
|
-
|
|
683
|
+
// Add this to your useState hook to ensure re-renders
|
|
684
|
+
export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
|
|
537
685
|
if (!wipFiber) {
|
|
538
686
|
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
539
687
|
}
|
|
540
688
|
|
|
541
|
-
|
|
689
|
+
const currentHookIndex = hookIndex;
|
|
690
|
+
let hook = wipFiber.hooks[currentHookIndex];
|
|
691
|
+
|
|
542
692
|
if (!hook) {
|
|
543
693
|
hook = {
|
|
544
694
|
state: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
545
695
|
queue: [],
|
|
546
696
|
_needsUpdate: false
|
|
547
697
|
};
|
|
548
|
-
wipFiber.hooks[
|
|
698
|
+
wipFiber.hooks[currentHookIndex] = hook;
|
|
549
699
|
}
|
|
550
700
|
|
|
551
701
|
const setState = (action: T | ((prevState: T) => T)) => {
|
|
552
|
-
// Calculate new state
|
|
702
|
+
// Calculate new state
|
|
553
703
|
const newState = typeof action === "function"
|
|
554
704
|
? (action as (prevState: T) => T)(hook.state)
|
|
555
705
|
: action;
|
|
556
706
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
// Reset work-in-progress root to trigger re-r
|
|
560
|
-
|
|
561
|
-
deletions = [];
|
|
562
|
-
nextUnitOfWork = wipRoot;
|
|
707
|
+
if (!Object.is(hook.state, newState)) {
|
|
708
|
+
hook.state = newState;
|
|
563
709
|
|
|
564
|
-
//
|
|
565
|
-
|
|
710
|
+
// ✅ Schedule a re-render starting from the component's fiber
|
|
711
|
+
scheduleRender();
|
|
712
|
+
}
|
|
566
713
|
};
|
|
567
714
|
|
|
568
715
|
hookIndex++;
|
|
569
716
|
return [hook.state, setState];
|
|
570
717
|
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Schedules a re-render of the entire app
|
|
721
|
+
*/
|
|
722
|
+
function scheduleRender(): void {
|
|
723
|
+
if (!currentRoot || !currentRoot.dom) return;
|
|
724
|
+
|
|
725
|
+
// Schedule a new render starting from the current root
|
|
726
|
+
wipRoot = {
|
|
727
|
+
dom: currentRoot.dom,
|
|
728
|
+
props: currentRoot.props,
|
|
729
|
+
alternate: currentRoot,
|
|
730
|
+
};
|
|
731
|
+
deletions = [];
|
|
732
|
+
nextUnitOfWork = wipRoot;
|
|
733
|
+
|
|
734
|
+
// Start the work loop on next animation frame
|
|
735
|
+
requestAnimationFrame(workLoop);
|
|
736
|
+
}
|
|
571
737
|
|
|
572
738
|
/**
|
|
573
739
|
* A React-like useEffect hook for side effects.
|
|
@@ -637,13 +803,23 @@ export function Match({ when, children }: { when: boolean, children: VNode[] }):
|
|
|
637
803
|
* @param {Function} fn - The function component to wrap.
|
|
638
804
|
* @returns {Function} A function that creates a VNode when called with props.
|
|
639
805
|
*/
|
|
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
|
-
|
|
806
|
+
export function component<P extends object>(
|
|
807
|
+
fn: (props: P & { children?: any }) => VNode | VNode[]
|
|
808
|
+
): (props: P & { key?: string | number; children?: any }) => VNode {
|
|
809
|
+
return (props: P & { key?: string | number; children?: any }) => {
|
|
810
|
+
// Merge key if needed
|
|
811
|
+
const { key, ...rest } = props;
|
|
812
|
+
// Call the component function directly
|
|
813
|
+
const vnode = fn(rest as P & { children?: any });
|
|
814
|
+
|
|
815
|
+
// Attach the key to the VNode
|
|
816
|
+
if (vnode && typeof vnode === "object") {
|
|
817
|
+
vnode.key = key;
|
|
818
|
+
}
|
|
819
|
+
return vnode;
|
|
645
820
|
};
|
|
646
821
|
}
|
|
822
|
+
|
|
647
823
|
export function Show({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
|
|
648
824
|
return when ? children : null;
|
|
649
825
|
}
|
|
@@ -669,21 +845,22 @@ declare global {
|
|
|
669
845
|
* @param {T} initial - The initial reference value.
|
|
670
846
|
* @returns {{current: T}} A mutable ref object.
|
|
671
847
|
*/
|
|
672
|
-
export function useRef<T>(initial: T): { current: T } {
|
|
848
|
+
export function useRef<T>(initial: T | null = null): { current: T | null } {
|
|
673
849
|
if (!wipFiber) {
|
|
674
850
|
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
675
851
|
}
|
|
676
852
|
|
|
677
853
|
let hook = wipFiber.hooks[hookIndex];
|
|
678
854
|
if (!hook) {
|
|
679
|
-
hook = { current: initial
|
|
855
|
+
hook = { current: initial };
|
|
680
856
|
wipFiber.hooks[hookIndex] = hook;
|
|
681
857
|
}
|
|
682
858
|
|
|
683
859
|
hookIndex++;
|
|
684
|
-
return hook;
|
|
860
|
+
return hook as { current: T | null };
|
|
685
861
|
}
|
|
686
862
|
|
|
863
|
+
|
|
687
864
|
/**
|
|
688
865
|
* A React-like useLayoutEffect hook that runs synchronously after DOM mutations.
|
|
689
866
|
* @param {Function} callback - The effect callback.
|
|
@@ -1047,14 +1224,27 @@ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T)
|
|
|
1047
1224
|
export function useOnClickOutside(ref: { current: HTMLElement | null }, handler: Function): void {
|
|
1048
1225
|
useEffect(() => {
|
|
1049
1226
|
const listener = (event: MouseEvent) => {
|
|
1050
|
-
|
|
1227
|
+
// Create a stable reference to the current ref value
|
|
1228
|
+
const currentRef = ref.current;
|
|
1229
|
+
console.log(currentRef)
|
|
1230
|
+
|
|
1231
|
+
if (!currentRef || !event.target) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
console.log(currentRef)
|
|
1236
|
+
|
|
1237
|
+
// Check if click is outside
|
|
1238
|
+
if (!currentRef.contains(event.target as Node)) {
|
|
1051
1239
|
handler(event);
|
|
1052
1240
|
}
|
|
1053
1241
|
};
|
|
1054
|
-
|
|
1242
|
+
|
|
1243
|
+
// Use capture phase to ensure we catch the event
|
|
1244
|
+
document.addEventListener("mousedown", listener, true);
|
|
1245
|
+
|
|
1055
1246
|
return () => {
|
|
1056
|
-
document.removeEventListener("mousedown", listener);
|
|
1247
|
+
document.removeEventListener("mousedown", listener, true);
|
|
1057
1248
|
};
|
|
1058
|
-
}, [ref, handler]);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1249
|
+
}, [ref, handler]); // Keep ref in dependencies
|
|
1250
|
+
}
|