From 5f721e455d73b7f8db54ec2ea670f85752e5200f Mon Sep 17 00:00:00 2001 From: Steve Dunham Date: Sat, 4 Apr 2026 08:03:23 -0700 Subject: [PATCH] add spruce.ts (also inlined into Spruce.newt) --- src/Web/Spruce.newt | 6 +-- src/Web/spruce.ts | 119 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/Web/spruce.ts diff --git a/src/Web/Spruce.newt b/src/Web/Spruce.newt index fddfcca..cacb798 100644 --- a/src/Web/Spruce.newt +++ b/src/Web/Spruce.newt @@ -29,8 +29,7 @@ pfunc runApp : ∀ msg model. Element → model → (update : msg → model → if (node) { parent.insertBefore(child, node); parent.removeChild(node); - } - else { + } else { parent.appendChild(child); } return child; @@ -47,8 +46,7 @@ pfunc runApp : ∀ msg model. Element → model → (update : msg → model → let el; if (node instanceof Element && node.tagName.toLowerCase() == vnode.h1) { el = node; - } - else { + } else { el = document.createElement(vnode.h1); } // update node here diff --git a/src/Web/spruce.ts b/src/Web/spruce.ts new file mode 100644 index 0000000..f8a9a28 --- /dev/null +++ b/src/Web/spruce.ts @@ -0,0 +1,119 @@ +// We're going to do the VDom thing, directly off of newt data + +type Attrib = + | { tag: 0; h1: string; h2: string } + | { tag: 1; h1: string; h2: Msg } + | { tag: 2; h1: string; h2: (_: string) => Msg }; + +type List = { tag: 0 } | { tag: 1; h1: A; h2: List }; + +type VNode = + | { tag: 0; h1: string } // TNode + | { tag: 1; h1: string; h2: List>; h3: List> }; + +type FancyElement = Element & { events?: Record > }; + +// the pfunc will have two dummy arguments in front, and I dunno no node? +export function runapp( + node: Node, + init: Model, + update: (_: Msg) => (_: Model) => Model, + view: (_: Model) => VNode, +) { + function replace(parent: Node, node: Node | undefined, child: Node) { + if (node) { + parent.insertBefore(child, node); + parent.removeChild(node); + } else { + parent.appendChild(child); + } + return child; + } + // patch node, possibly returning a new node + function patch( + parentNode: Node, + node: Node | undefined, + vnode: VNode, + ): Node { + if (vnode.tag == 0) { + if (node && node.nodeType == 3) { + node.nodeValue = vnode.h1; + return node; + } + return replace(parentNode, node, document.createTextNode(vnode.h1)); + } + let el: FancyElement; + if (node instanceof Element && node.tagName.toLowerCase() == vnode.h1) { + el = node; + } else { + el = document.createElement(vnode.h1); + } + // update node here + let has: Record = {}; + for (let attrs = vnode.h2; attrs.tag == 1; attrs = attrs.h2) { + let attr = attrs.h1; + if (attr.tag == 0) { + let key = attr.h1; + has[key] = true; + if (key in el) + (el as any)[key] = attr.h2 + else + el.setAttribute(key, attr.h2); + } else { + // onBlah + let key = attr.h1.slice(2).toLowerCase(); + has[key] = true; + let events = el.events || (el.events = {}); + if (!events[key]) el.addEventListener(key, listener); + events[key] = attr; + } + } + // remove attrs, not efficient.. + for (let i = 0; i < el.attributes.length; ) { + let attr = el.attributes[i]; + if (!has[attr.name]) { + el.removeAttribute(attr.name); + } else { + i++; + } + } + if (el.events) { + for (let key of Object.keys(el.events)) { + if (!has[key]) delete el.events[key]; + } + } + let i = 0; + for (let kids = vnode.h3; kids.tag == 1; kids = kids.h2) { + patch(el, el.childNodes[i++], kids.h1); + } + while (el.childNodes[i]) el.removeChild(el.childNodes[i]); + + return node == el ? node : replace(parentNode, node, el); + } + let model = init; + let vdom = view(model); + let listener = (ev: Event) => { + console.log('listener', ev, ev.target) + let target = ev.target as FancyElement; + for (;;) { + if (!target) return + if (target.events?.[ev.type]) break + target = target.parentElement as FancyElement; + } + if (!target.events) return; + const attr = target.events[ev.type] + if (attr.tag === 2) { + // probably need to pass back the event + // we'll want to deal with keypress, change, ... + let action = attr.h2(ev.target!.value) + model = update(action)(model); + console.log('UPDATE', action, '->', model); + } else if (attr.tag === 1) { + model = update(attr.h2)(model); + console.log('UPDATE', attr.h2, '->', model); + } + vdom = view(model); + node = patch(node.parentNode!, node, vdom); + }; + node = patch(node.parentNode!, node, vdom); +}