From da683b9628d3094c72bf7c279a52d9799657373f Mon Sep 17 00:00:00 2001 From: "code@dmj.io" Date: Wed, 10 Dec 2025 14:12:44 -0600 Subject: [PATCH 01/76] Initial commit of domRef removal on vcomp --- ts/miso/context/dom.ts | 12 +- ts/miso/context/patch.ts | 15 ++- ts/miso/dom.ts | 243 +++++++++++++++++++++++++++++--------- ts/miso/event.ts | 43 +++++-- ts/miso/hydrate.ts | 33 ++++-- ts/miso/types.ts | 29 +++-- ts/spec/component.spec.ts | 79 +++++++++---- ts/spec/dom.spec.ts | 29 +++-- ts/spec/event.spec.ts | 25 ++-- ts/spec/hydrate.spec.ts | 4 +- 10 files changed, 371 insertions(+), 141 deletions(-) diff --git a/ts/miso/context/dom.ts b/ts/miso/context/dom.ts index 21cc7f794..9147ba7ec 100644 --- a/ts/miso/context/dom.ts +++ b/ts/miso/context/dom.ts @@ -7,9 +7,12 @@ import , HydrationContext , DOMRef , ComponentContext - , VTree + , VTree, + VTreeType } from '../types'; +import { drill } from '../dom'; + export const eventContext : EventContext = { addEventListener : (mount: DOMRef, event: string, listener, capture: boolean) => { mount.addEventListener(event, listener, capture); @@ -68,7 +71,12 @@ export const componentContext : ComponentContext = { export const drawingContext : DrawingContext = { nextSibling : (node: VTree) => { - return node.domRef.nextSibling as DOMRef; + switch (node.type) { + case VTreeType.VComp: + return drill(node).nextSibling as DOMRef; + default: + return node.domRef.nextSibling as DOMRef; + } }, createTextNode : (s: string) => { return document.createTextNode(s) as any; // dmj: hrm diff --git a/ts/miso/context/patch.ts b/ts/miso/context/patch.ts index 1662b5bd2..06a5ef56e 100644 --- a/ts/miso/context/patch.ts +++ b/ts/miso/context/patch.ts @@ -5,9 +5,13 @@ import { VNode, NodeId, CSS, - ComponentContext + ComponentContext, + VTree, + VTreeType, } from '../types'; +import { drill } from '../dom'; + /* This file is used to provide testing for miso-lynx, or other patch-based architectures @@ -93,8 +97,13 @@ export const componentContext : ComponentContext = { }; export const patchDrawingContext : DrawingContext = { - nextSibling : (node: VNode) => { - return node.nextSibling.domRef; + nextSibling : (node: VTree) => { + switch (node.nextSibling.type) { + case VTreeType.VComp: + return drill(node.nextSibling) as NodeId; + default: + return node.nextSibling.domRef as NodeId; + } }, createTextNode : (value : string) => { const nodeId: number = nextNodeId (); diff --git a/ts/miso/dom.ts b/ts/miso/dom.ts index ced4bf0bd..3565dbea9 100644 --- a/ts/miso/dom.ts +++ b/ts/miso/dom.ts @@ -1,23 +1,19 @@ -import { Class, DrawingContext, CSS, VNode, VText, VComp, ComponentId, VTree, Props, VTreeType } from './types'; +import { Class, DrawingContext, CSS, VNode, VText, VComp, ComponentId, VTree, Props, VTreeType, OP } from './types'; /* virtual-dom diffing algorithm, applies patches as detected */ export function diff(c: VTree, n: VTree, parent: T, context: DrawingContext): void { - if (!c && !n) - return; - else if (!c) - create(n, parent, context); - else if (!n) - destroy(c, parent, context); + if (!c && !n) return; + else if (!c) create(n, parent, context); + else if (!n) destroy(c, parent, context); else if (c.type === VTreeType.VText && n.type === VTreeType.VText) { diffVText(c, n, context); } else if (c.type === VTreeType.VComp && n.type === VTreeType.VComp) { - if (n.tag === c.tag && n.key === c.key) { - n.domRef = c.domRef; - diffAttrs(c, n, context); - } else { - replace(c, n, parent, context); + if (n.key === c.key) { + n.child = c.child; + return; } + replace(c, n, parent, context); } else if (c.type === VTreeType.VNode && n.type === VTreeType.VNode) { if (n.tag === c.tag && n.key === c.key) { @@ -28,7 +24,7 @@ export function diff(c: VTree, n: VTree, parent: T, context: DrawingCon } } else - replace(c, n, parent, context); + replace(c, n, parent, context); } function diffVText(c: VText, n: VText, context : DrawingContext): void { @@ -37,6 +33,17 @@ function diffVText(c: VText, n: VText, context : DrawingContext): vo return; } +export function drill(c: VComp): T { + while (c.type === VTreeType.VComp && c.child) { + switch (c.child.type) { + case VTreeType.VComp: + c = c.child; + break; + default: + return c.child.domRef; + } + } +} // replace everything function function replace(c: VTree, n: VTree, parent: T, context : DrawingContext): void { @@ -51,15 +58,35 @@ function replace(c: VTree, n: VTree, parent: T, context : DrawingContex switch (n.type) { case VTreeType.VText: switch (c.type) { + case VTreeType.VComp: + context.replaceChild(parent, context.createTextNode(n.text), drill(c)); + break; default: n.domRef = context.createTextNode(n.text); context.replaceChild(parent, n.domRef, c.domRef); break; } break; - default: - context.replaceChild(parent, createElement(n, context), c.domRef as T); - break; + case VTreeType.VComp: + switch (c.type) { + case VTreeType.VComp: + createElement(parent, OP.REPLACE, drill(c), n, context); + break; + default: + createElement(parent, OP.REPLACE, c.domRef, n, context); + break; + } + break; + case VTreeType.VNode: + switch (c.type) { + case VTreeType.VComp: + createElement(parent, OP.REPLACE, drill(c), n, context); + break; + default: + createElement(parent, OP.REPLACE, c.domRef, n, context); + break; + } + break; } // step 3: call destroyed hooks, call created hooks switch (c.type) { @@ -82,7 +109,15 @@ function destroy(c: VTree, parent: T, context: DrawingContext): void { break; } // step 2: destroy - context.removeChild(parent, c.domRef); + switch (c.type) { + case VTreeType.VComp: + context.removeChild(parent, drill(c)); + break; + default: + context.removeChild(parent, c.domRef); + break; + } + // step 2: destroy // step 3: invoke post-hooks for vnode and vcomp switch (c.type) { case VTreeType.VText: @@ -128,14 +163,12 @@ function callBeforeDestroyedRecursive(c: VNode | VComp): void { callBeforeDestroyedRecursive(child); } -export function diffAttrs(c: VNode | VComp | null, n: VNode | VComp, context: DrawingContext): void { +export function diffAttrs(c: VNode | null, n: VNode, context: DrawingContext): void { diffProps(c ? c.props : {}, n.props, n.domRef, n.ns === 'svg', context); diffClass(c ? c.classList : null, n.classList, n.domRef, context); diffCss(c ? c.css : {}, n.css, n.domRef, context); - if (n.type === VTreeType.VNode) { - diffChildren(c ? c.children : [], n.children, n.domRef, context); - drawCanvas(n); - } + diffChildren(c ? c.children : [], n.children, n.domRef, context); + drawCanvas(n); } export function diffClass (c: Class, n: Class, domRef: T, context: DrawingContext): void { @@ -252,7 +285,7 @@ function diffChildren(cs: Array>, ns: Array>, parent: T, co } } -function populateDomRef(c: VComp | VNode, context: DrawingContext): void { +function populateDomRef(c: VNode, context: DrawingContext): void { if (c.ns === 'svg') { c.domRef = context.createElementNS('http://www.w3.org/2000/svg', c.tag); } else if (c.ns === 'mathml') { @@ -263,34 +296,43 @@ function populateDomRef(c: VComp | VNode, context: DrawingContext): } /* used in hydrate.ts */ -export function callCreated(n: VComp | VNode, context: DrawingContext): T { +export function callCreated(parent: T, n: VComp | VNode, context: DrawingContext): void { switch (n.type) { case VTreeType.VComp: if (n.onBeforeMounted) n.onBeforeMounted(); - mountComponent(n, context); + mountComponent(OP.APPEND, parent, n, null, context); + if (n.onMounted) n.onMounted(drill(n)); break; case VTreeType.VNode: if (n.onCreated) n.onCreated(n.domRef); break; } - return n.domRef; } -function createElement(n: VComp | VNode, context: DrawingContext): T { +function createElement(parent : T, op: OP, replacing : T | null, n: VComp | VNode, context: DrawingContext): void { switch (n.type) { case VTreeType.VComp: if (n.onBeforeMounted) n.onBeforeMounted(); - populateDomRef(n, context); - mountComponent(n, context); + mountComponent(op, parent, n, replacing, context); break; case VTreeType.VNode: if (n.onBeforeCreated) n.onBeforeCreated(); populateDomRef(n, context); if (n.onCreated) n.onCreated(n.domRef); + diffAttrs(null, n, context); + switch (op) { + case OP.INSERT_BEFORE: + context.insertBefore(parent, n.domRef, replacing); + break; + case OP.APPEND: + context.appendChild(parent, n.domRef); + break; + case OP.REPLACE: + context.replaceChild(parent, n.domRef, replacing); + break; + } break; } - diffAttrs(null, n, context); - return n.domRef; } /* draw the canvas if you need to */ @@ -301,30 +343,111 @@ function drawCanvas (c: VNode) { // unmount components function unmountComponent(c: VComp): void { - if (c.onUnmounted) c.onUnmounted(c.domRef); - c.unmount(c.domRef); + if (c.onUnmounted) c.onUnmounted((drill(c))); + c.unmount(drill(c)); } // mounts vcomp by calling into Haskell side. // unmount is handled with pre-destroy recursive hooks -function mountComponent(obj: VComp, context: DrawingContext): void { - obj.mount(obj, (componentId: ComponentId, componentTree: VNode) => { +function mountComponent(op : OP, parent: T, n: VComp, replacing: T, context: DrawingContext): void { + n.mount(parent, (componentId: ComponentId, componentTree: VNode) => { // mount() gives us the VTree from the Haskell side, so we just attach it here // to tie the knot (attach to both vdom and real dom). - obj.children.push(componentTree); - context.appendChild(obj.domRef, componentTree.domRef); - if (obj.onMounted) obj.onMounted(obj.domRef); + n.componentId = componentId; + n.child = componentTree; + if (n.onMounted) n.onMounted(drill(n)); + switch (op) { + case OP.INSERT_BEFORE: + context.insertBefore(parent, drill(n), replacing); + break; + case OP.APPEND: + context.appendChild(parent, drill(n)); + break; + case OP.REPLACE: + context.replaceChild(parent, drill(n), replacing); + break; + } }); } -// creates nodes on virtual and dom (vtext, vcomp, vnode) -function create(obj: VTree, parent: T, context: DrawingContext): void { - if (obj.type === VTreeType.VText) { - obj.domRef = context.createTextNode(obj.text); - context.appendChild(parent, obj.domRef); + +// Creates nodes on virtual dom (vtext, vcomp, vnode) +function create(n: VTree, parent: T, context: DrawingContext): void { + if (n.type === VTreeType.VText) { + n.domRef = context.createTextNode(n.text); + context.appendChild(parent, n.domRef); } else { - context.appendChild(parent, createElement(obj, context)); + createElement(parent, OP.APPEND, null, n, context); } } + +function insertBefore(parent: T, n: VTree, o: VTree | null, context: DrawingContext): void { + switch (n.type) { + case VTreeType.VComp: + if (!o) { + context.insertBefore(parent, drill(n), null); + } else { + switch (o.type) { + case VTreeType.VComp: + context.insertBefore(parent, drill(n), drill(o)); + break; + default: + context.insertBefore(parent, drill(n), o.domRef); + break; + } + } + break; + default: + if (!o) { + context.insertBefore(parent, n.domRef, null); + } else { + switch (o.type) { + case VTreeType.VComp: + context.insertBefore(parent, n.domRef, drill(o)); + break; + default: + context.insertBefore(parent, n.domRef, o.domRef); + break; + } + } + } +} + +function removeChild(parent: T, n: VTree, context: DrawingContext): void { + switch (n.type) { + case VTreeType.VComp: + context.removeChild(parent, drill(n)); + break; + default: + context.removeChild(parent, n.domRef); + break; + } +} + +function swapDOMRef(oFirst: VTree, oLast: VTree, parent: T, context: DrawingContext): void { + switch (oLast.type) { + case VTreeType.VComp: + switch (oFirst.type) { + case VTreeType.VComp: + context.swapDOMRefs(drill(oLast), drill(oFirst), parent); + break; + default: + context.swapDOMRefs(drill(oLast), oFirst.domRef, parent); + break; + } + break; + default: + switch (oFirst.type) { + case VTreeType.VComp: + context.swapDOMRefs(oLast.domRef, drill(oFirst), parent); + break; + default: + context.swapDOMRefs(oLast.domRef, oFirst.domRef, parent); + break; + } + break; + } + } + /* Child reconciliation algorithm, inspired by kivi and Bobril */ function syncChildren(os: Array>, ns: Array>, parent: T, context: DrawingContext): void { var oldFirstIndex: number = 0, @@ -360,7 +483,7 @@ function syncChildren(os: Array>, ns: Array>, parent: T, co /* insertBefore's semantics will append a node if the second argument provided is `null` or `undefined`. Otherwise, it will insert node.domRef before oLast.domRef. */ - context.insertBefore(parent, nFirst.domRef, oFirst ? oFirst.domRef : null); + insertBefore(parent, nFirst, oFirst, context); os.splice(newFirstIndex, 0, nFirst); newFirstIndex++; } /* No more new nodes, delete all remaining nodes in old list @@ -370,7 +493,7 @@ function syncChildren(os: Array>, ns: Array>, parent: T, co else if (newFirstIndex > newLastIndex) { tmp = oldLastIndex; while (oldLastIndex >= oldFirstIndex) { - context.removeChild(parent, os[oldLastIndex--].domRef); + removeChild(parent, os[oldLastIndex--], context); } os.splice(oldFirstIndex, tmp - oldFirstIndex + 1); break; @@ -392,7 +515,7 @@ function syncChildren(os: Array>, ns: Array>, parent: T, co -> [ c b a ] <- new children */ else if (oFirst.key === nLast.key && nFirst.key === oLast.key) { - context.swapDOMRefs(oLast.domRef, oFirst.domRef, parent); + swapDOMRef(oLast, oFirst, parent, context); swap>(os, oldFirstIndex, oldLastIndex); diff(os[oldFirstIndex++], ns[newFirstIndex++], parent, context); diff(os[oldLastIndex--], ns[newLastIndex--], parent, context); @@ -408,7 +531,7 @@ function syncChildren(os: Array>, ns: Array>, parent: T, co and now we happy path */ else if (oFirst.key === nLast.key) { /* insertAfter */ - context.insertBefore(parent, oFirst.domRef, context.nextSibling(oLast)); + insertBefore(parent, oFirst, oLast.nextSibling, context); /* swap positions in old vdom */ os.splice(oldLastIndex, 0, os.splice(oldFirstIndex, 1)[0]); diff(os[oldLastIndex--], ns[newLastIndex--], parent, context); @@ -422,7 +545,7 @@ function syncChildren(os: Array>, ns: Array>, parent: T, co and now we happy path */ else if (oLast.key === nFirst.key) { /* insertAfter */ - context.insertBefore(parent, oLast.domRef, oFirst.domRef); + insertBefore(parent, oLast, oFirst, context); /* swap positions in old vdom */ os.splice(oldFirstIndex, 0, os.splice(oldLastIndex, 1)[0]); diff(os[oldFirstIndex++], nFirst, parent, context); @@ -463,7 +586,7 @@ function syncChildren(os: Array>, ns: Array>, parent: T, co /* optionally perform `diff` here */ diff(os[oldFirstIndex++], nFirst, parent, context); /* Swap DOM references */ - context.insertBefore(parent, node.domRef, os[oldFirstIndex].domRef); + insertBefore(parent, node, os[oldFirstIndex], context); /* increment counters */ newFirstIndex++; } /* If new key was *not* found in the old map this means it must now be created, example below @@ -480,12 +603,26 @@ function syncChildren(os: Array>, ns: Array>, parent: T, co switch (nFirst.type) { case VTreeType.VText: nFirst.domRef = context.createTextNode(nFirst.text); - context.insertBefore(parent, nFirst.domRef, oFirst.domRef); + switch (oFirst.type) { + case VTreeType.VComp: + context.insertBefore(parent, nFirst.domRef, drill(oFirst)); + break; + default: + context.insertBefore(parent, nFirst.domRef, oFirst.domRef); + break; + } break; default: - context.insertBefore(parent, createElement(nFirst, context), oFirst.domRef); - break; + switch (oFirst.type) { + case VTreeType.VComp: + createElement(parent, OP.INSERT_BEFORE, drill(oFirst), nFirst, context); + break; + case VTreeType.VNode: + createElement(parent, OP.INSERT_BEFORE, oFirst.domRef, nFirst, context); + break; } + break; + } os.splice(oldFirstIndex++, 0, nFirst); newFirstIndex++; oldLastIndex++; diff --git a/ts/miso/event.ts b/ts/miso/event.ts index bbdfc7081..fbff6ba56 100644 --- a/ts/miso/event.ts +++ b/ts/miso/event.ts @@ -1,4 +1,5 @@ -import { EventContext, VTree, EventCapture, EventObject, Options, VTreeType } from './types'; +import { EventContext, VTree, EventCapture, EventObject, Options, VTreeType, VComp } from './types'; +import { drill } from './dom'; /* event delegation algorithm */ export function delegate ( @@ -95,7 +96,19 @@ function delegateEvent ( return; } /* stack not length 1, recurse */ else if (stack.length > 1) { - if (obj.type === VTreeType.VComp || obj.type === VTreeType.VNode) { + if (obj.type === VTreeType.VText) { + return; + } + else if (obj.type === VTreeType.VComp) { + if (!obj.child) { + if (debug) { + console.warn('VComp has no child property set during event delegation', obj); + } + return; + } + return delegateEvent(event, obj.child, stack, debug, context); + } + else if (obj.type === VTreeType.VNode) { if (context.isEqual(obj.domRef, stack[0])) { const eventObj: EventObject = obj.events.captures[event.type]; if (eventObj) { @@ -111,17 +124,29 @@ function delegateEvent ( } stack.splice(0,1); } - for (const child of obj.children) { - if (child.type === VTreeType.VComp || child.type === VTreeType.VNode) { - if (context.isEqual(child.domRef, stack[0])) { - delegateEvent(event, child, stack, debug, context); - } - } + for (const child of obj.children) { + if (child.type === VTreeType.VComp) { + // For VComp, we need to drill to get the actual domRef + const childDomRef = drill(child as VComp); + if (childDomRef && context.isEqual(childDomRef, stack[0])) { + delegateEvent(event, child, stack, debug, context); + } + } else if (child.type === VTreeType.VNode) { + if (context.isEqual(child.domRef, stack[0])) { + delegateEvent(event, child, stack, debug, context); + } + } } } } else { + /* stack.length === 1, we're at the target */ + if (obj.type === VTreeType.VComp) { + /* VComp doesn't have events directly, delegate to its child */ + if (obj.child) { + delegateEvent(event, obj.child, stack, debug, context); + } + } else if (obj.type === VTreeType.VNode) { /* captures run first */ - if (obj.type === VTreeType.VNode) { const eventCaptureObj: EventObject = obj.events.captures[event.type]; if (eventCaptureObj && !event['captureStopped']) { const options: Options = eventCaptureObj.options; diff --git a/ts/miso/hydrate.ts b/ts/miso/hydrate.ts index ac81111e9..2320bd137 100644 --- a/ts/miso/hydrate.ts +++ b/ts/miso/hydrate.ts @@ -1,4 +1,4 @@ -import { callCreated, diffAttrs } from './dom'; +import { drill, callCreated, diffAttrs } from './dom'; import { DrawingContext, HydrationContext, VTree, VComp, VText, DOMRef, VTreeType } from './types'; /* prerendering / hydration / isomorphic support */ @@ -14,6 +14,14 @@ function collapseSiblingTextNodes(vs: Array>): Array return adjusted; } +function setVCompRef(vtree: VTree, node: Node): void { + if (vtree.type === VTreeType.VComp) { + setVCompRef(vtree.child, node); + } else { + vtree.domRef = node as DOMRef; + } +} + /* function to determine if