using "./dom"; // A lightweight, framework-owned description of one DOM element — Render() // builds a tree of these instead of real DOM nodes, so Update() can DIFF // the new tree against the previous one and patch only what changed, // instead of discarding and rebuilding the whole real DOM subtree every // time (see src/vdom.ks for the diff/patch engine, src/component.ks for // where Render()'s return type changed from Element to VElement). // // A single shared no-op, not a fresh closure per VElement — every instance // that never sets a real handler gets this exact same function reference, // so Patch() (vdom.ks) can tell "no handler either time" apart from "a // handler changed" with a plain `!=` reference check instead of always // removing/re-adding all four DOM listeners on every patch regardless of // whether anything about them actually changed. A fresh `(Event e) => {}` // closure per instance would defeat this — two "empty" handlers would // never compare equal, even when nothing meaningful differs. void NoOpEventHandler(Event e) { } // A live child Component (or anything else that wants to be embedded as a // slot in another's VElement tree) — see VElement.Mounted below. Declared // HERE, not in vdom.ks, even though vdom.ks's Materialize/Patch are the // only real callers: VElement's own Mounted field needs to reference this // type, and vdom.ks already `using`s this file for VElement itself — // declaring it in vdom.ks would make velement.ks need to `using "./vdom"` // right back, a circular `using` KopScript rejects outright (the same // constraint that put the unrelated Flushable interface in vdom.ks // instead of component.ks — there, Component was the one doing the // referencing; here, VElement is). Component (component.ks) is the one // real implementer, via `class Component : Flushable, Mountable`. interface Mountable { // Called by the vdom engine only (public purely for interface // conformance, same convention as Flushable.FlushUpdate — not the // intended way for app code to trigger anything). Builds this // component's own tree for the first time and returns its real root // node WITHOUT inserting it anywhere; the caller (Materialize/Patch) // does that itself, the same as every other VElement content mode. Element MountAsChild(Element parent); // Re-renders this already-mounted child in place; returns the (possibly // identical) real root node. Called when a slot's Mounted reference is // the SAME instance as last render. Element PatchAsChild(); // Called once when this slot's Mounted reference disappears or is // replaced by a different instance across a re-render, before its real // node is removed — the one hook the vdom engine itself calls; see // Component's own OnUnmount for the real, overridable app-facing // extension point this delegates to. void Teardown(); } // Fixed, named fields — not a generic prop bag — because KopScript has no // object-literal syntax to build one with. Fixed, named event slots — not // an array of handlers — because KopScript has no array-of-function-values // type either. Both are real language constraints, not an oversight; see // SetAttr below for the escape hatch covering everything not common enough // to deserve its own named field. class VElement { public string Tag; // Mutually exclusive with Children and RawHtml — set at most one of the // three. TextContent/Children mirrors the same "no mixed text/element // content" rule KopScript's own templates already enforce. public string TextContent; public string ClassName; public string Id; // The one property patched via direct assignment, never setAttribute — // see Element.value's own comment in dom.ks for why (the "default value // attribute" vs "current live value property" DOM footgun — the exact // property behind the original typing bug this whole effort traces to). public string Value; public VElement[] Children; // An opaque, undiffed leaf — set instead of TextContent/Children for the // existing raw-HTML-then-wire-handlers pattern (header.ks/nav.ks). The // patch engine treats two VElements with different RawHtml as a single // innerHTML assignment, never recursing inside it — the same "opaque // blob" treatment `raw string` already gets everywhere else. public string RawHtml; // Real DOM properties, not attributes: a boolean attribute is on whenever // it's present at all, so SetAttr can never turn `disabled` back off. public bool Disabled; public bool Checked; // Each defaults to a real no-op, never null — KopScript has no nullable // *function* type to fall back on for "no handler set" (see Router.Guard // for the same default-real-function pattern already established). public (Event) => void OnClick; public (Event) => void OnInput; public (Event) => void OnBlur; public (Event) => void OnChange; // The REAL function reference the patch engine (vdom.ks) actually passed // to addEventListener for this exact real DOM node — never OnClick/ // OnInput/OnBlur/OnChange themselves. Materialize/Patch wrap each handler // in Component.RunInBatch (see component.ks) before attaching it, so the // listener genuinely registered isn't the same function value as the one // an app author wrote; removeEventListener only ever works when passed // the exact reference addEventListener received, so the patch engine // needs somewhere to remember it for the swap-when-changed path. Plain // data, same as every other field here — only vdom.ks ever reads or // writes these. public (Event) => void AttachedOnClick; public (Event) => void AttachedOnInput; public (Event) => void AttachedOnBlur; public (Event) => void AttachedOnChange; // A real HTML attribute not common enough for its own named field (href, // src, alt, placeholder, ...) — parallel arrays, since KopScript has no // Dictionary type. Never Value (see its own field comment above). Public, // read directly by the patch engine (src/vdom.ks) rather than through // accessor methods — plain data, same style as this codebase's other // plain classes (Note, Dog, ...). public string[] ExtraNames; public string[] ExtraValues; // Set only once this VElement has been materialized into (or reused as) // a real DOM node — null on a freshly-built tree from a not-yet-patched // Render() call. The patch engine reads the *previous* render's tree's // RealNode to know what to reuse/patch; it never reads the live DOM back // to rediscover this (see vdom.ks's own header comment for why). public Element? RealNode; // Set to embed a live, mounted Component (or any other Mountable) as // this VElement's entire content — mutually exclusive with // Tag/TextContent/Children/RawHtml, and stronger than RawHtml's own // "opaque leaf" treatment: Tag is unused, since no wrapping element of // Kopular's own is created for this slot at all — the child's own // rendered root IS this slot's real node (see vdom.ks's Materialize/ // Patch). null on every ordinary VElement, the only value every // VElement had before this existed, so nothing about an existing // content mode changes unless a tree opts into this one. Use // VElement.Mount(component) below rather than setting this directly. public Mountable? Mounted; constructor(string tag) { this.Tag = tag; this.TextContent = ""; this.ClassName = ""; this.Id = ""; this.Value = ""; this.Children = []; this.RawHtml = ""; this.Disabled = false; this.Checked = false; this.OnClick = NoOpEventHandler; this.OnInput = NoOpEventHandler; this.OnBlur = NoOpEventHandler; this.OnChange = NoOpEventHandler; this.AttachedOnClick = NoOpEventHandler; this.AttachedOnInput = NoOpEventHandler; this.AttachedOnBlur = NoOpEventHandler; this.AttachedOnChange = NoOpEventHandler; this.ExtraNames = []; this.ExtraValues = []; this.RealNode = null; this.Mounted = null; } public static VElement Create(string tag) { return new VElement(tag); } // Wraps a live child Component (or anything else implementing Mountable) // as a VElement slot the diff engine can create/patch/move/destroy // declaratively. Set .Id on the result afterward for a list of these to // reorder correctly, the same as any other keyed child — PatchChildren // (vdom.ks) needs no changes to support this; it already keys by Id // regardless of what a VElement's content actually is. public static VElement Mount(Mountable component) { VElement ve = new VElement(""); ve.Mounted = component; return ve; } public void AppendChild(VElement child) { this.Children = this.Children.Push(child); } // Last call for a given name wins if SetAttr is called more than once // with the same name on one VElement — the patch engine applies // ExtraNames/ExtraValues in order, so a later entry's setAttribute call // simply overwrites an earlier one for the same name, no special // dedup/replace logic needed here. public void SetAttr(string name, string value) { this.ExtraNames = this.ExtraNames.Push(name); this.ExtraValues = this.ExtraValues.Push(value); } }