kopular 1.3.2 → 1.4.0
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/GUIDE.md +38 -26
- package/LLM.md +4 -5
- package/package.json +2 -2
- package/src/component.js +1 -1
- package/src/dom.js.map +1 -1
- package/src/dom.ks +5 -0
- package/src/kopular.ks +4 -0
- package/src/vdom.js +30 -0
- package/src/vdom.js.map +1 -1
- package/src/vdom.ks +34 -0
- package/src/velement.js +12 -0
- package/src/velement.js.map +1 -1
- package/src/velement.ks +22 -0
package/GUIDE.md
CHANGED
|
@@ -6,30 +6,45 @@ own `LLM.md` and `node_modules/kopscript/LLM.md`.
|
|
|
6
6
|
|
|
7
7
|
## Setup
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
**A fresh `kp new` project's `src/` is exactly these three files — you've now seen all of
|
|
10
|
+
it, so don't open them.** `index.html` loads the compiled `src/app.js` and `package.json`
|
|
11
|
+
has the scripts; neither ever needs editing or reading.
|
|
12
12
|
|
|
13
13
|
```ks
|
|
14
|
+
// src/app.ks — the entry point
|
|
14
15
|
using "kopular";
|
|
15
16
|
using "./counter";
|
|
16
17
|
|
|
17
18
|
Counter app = new Counter();
|
|
18
19
|
app.Mount(document.body);
|
|
19
20
|
```
|
|
21
|
+
```ks
|
|
22
|
+
// src/counter.ks
|
|
23
|
+
using "kopular";
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
class Counter : Component {
|
|
26
|
+
public state<number> Count;
|
|
27
|
+
constructor() : base() { this.Count = state(0); }
|
|
28
|
+
public void Increment() { this.Count.Value = this.Count.Value + 1; }
|
|
29
|
+
template from "./counter.html";
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
```html
|
|
33
|
+
<!-- src/counter.html -->
|
|
34
|
+
<button (click)="Increment()">Count: {{ Count.Value }}</button>
|
|
35
|
+
```
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
`app.ks`
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
To build your app, in one command: delete `counter.ks`/`counter.html`, write your own
|
|
38
|
+
files, rewrite `app.ks` to `using` and mount your root component, and run the tests.
|
|
39
|
+
|
|
40
|
+
Every file starts with `using "kopular";` for the DOM and every Kopular type, plus
|
|
41
|
+
`using "./other_file";` per project file it needs (`using` isn't transitive — list every
|
|
42
|
+
file you reference, not just direct dependencies' dependencies).
|
|
43
|
+
|
|
44
|
+
**Verify with one command, not two:** whatever runs your app or its tests builds first, and
|
|
45
|
+
a compile failure stops it right there with every error and its `fix:` hint — so running a
|
|
46
|
+
separate build before it only adds a round-trip. `ks check src/app.ks` type-checks without
|
|
47
|
+
building, for when you only want the errors.
|
|
33
48
|
|
|
34
49
|
## The language you need
|
|
35
50
|
|
|
@@ -96,17 +111,8 @@ the task, then write your files.
|
|
|
96
111
|
|
|
97
112
|
## Components
|
|
98
113
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
public state<number> Count;
|
|
102
|
-
constructor() : base() { this.Count = state(0); }
|
|
103
|
-
public void Increment() { this.Count.Value = this.Count.Value + 1; }
|
|
104
|
-
template from "./counter.html";
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
```html
|
|
108
|
-
<button (click)="Increment()">Count: {{ Count.Value }}</button>
|
|
109
|
-
```
|
|
114
|
+
A component is a class extending `Component` with a `template from` its HTML — `Counter`
|
|
115
|
+
above is a complete one.
|
|
110
116
|
|
|
111
117
|
A `state<T>` field the template reads auto-subscribes — no manual `Subscribe` needed —
|
|
112
118
|
whether it's named directly (`{{ Count.Value }}`) or read through the component's own
|
|
@@ -116,7 +122,9 @@ handlers don't count. State on *another* object does still need one:
|
|
|
116
122
|
|
|
117
123
|
**Template bindings**: `{{ expr }}` text, `[prop]="expr"` (real fields for
|
|
118
124
|
`id`/`className`/`value`/`disabled`/`checked`; anything else is a plain attribute),
|
|
119
|
-
`(click)`/`(input)`/`(blur)`/`(change)` events
|
|
125
|
+
`(click)`/`(input)`/`(blur)`/`(change)`/`(submit)` events (`(submit)` calls `preventDefault()`
|
|
126
|
+
for you, and Enter in a form's input fires it), text mixed with elements
|
|
127
|
+
(`<label>Email <input></label>`), `*if="expr"`, `*for="Type v of expr"`
|
|
120
128
|
(element type required, no inference), `*mount="expr"` (embeds a live child component,
|
|
121
129
|
composes with `*for`).
|
|
122
130
|
|
|
@@ -210,6 +218,10 @@ nav.SetGuard("/login", (string path) => {
|
|
|
210
218
|
nav.Navigate("/dogs/1");
|
|
211
219
|
nav.Mount(document.body);
|
|
212
220
|
```
|
|
221
|
+
- This goes in `app.ks`: the router *is* the root component, rendering whichever page
|
|
222
|
+
matches the current URL; back/forward are handled for you. A page navigates with the
|
|
223
|
+
`Router` its constructor received: `<button (click)="Nav.Navigate('/about')">` — there's
|
|
224
|
+
no link component, and a plain `<a href>` would reload the page.
|
|
213
225
|
- Routes hold already-built `Component`s, not factories — built once, state survives
|
|
214
226
|
navigating away and back.
|
|
215
227
|
- `nav.Param` — the first `:name` segment, a plain `string` (not `state<T>`; the outlet
|
package/LLM.md
CHANGED
|
@@ -441,8 +441,8 @@ class Counter : Component {
|
|
|
441
441
|
`*for="Type varName of expr"` — all real KopScript, checked at compile time, desugared
|
|
442
442
|
to the exact same `VElement.Create`/`.AppendChild`/`.TextContent`/named-event-field
|
|
443
443
|
calls a hand-written `Render()` would use. `(event)` only accepts `click`/`input`/
|
|
444
|
-
`blur`/`change` — `VElement`'s own fixed set — anything else is a compile error
|
|
445
|
-
(`KS5016`). `[prop]`/static `attr="..."` assign directly for `id`/`className`/`value`
|
|
444
|
+
`blur`/`change`/`submit` — `VElement`'s own fixed set — anything else is a compile error
|
|
445
|
+
(`KS5016`). `submit` calls `preventDefault()` before your handler. `[prop]`/static `attr="..."` assign directly for `id`/`className`/`value`
|
|
446
446
|
**and the two real bool fields, `disabled`/`checked`** (the compiler's own
|
|
447
447
|
`BOOL_FIELD_NAMES` map — a boolean attribute is present-or-absent, so only a real
|
|
448
448
|
property can turn one back off); anything else goes through `SetAttr` instead.
|
|
@@ -460,9 +460,8 @@ class Counter : Component {
|
|
|
460
460
|
- **Two-way binding**: `[(value)]="Field"` desugars to `[value]="Field"` +
|
|
461
461
|
`(input)="Field = e.target.value"` — `value` only, and `Field` must be a bare name or
|
|
462
462
|
`this.Field` (see KopScript's own LLM.md `KS5017`/`KS5018` for the two rejected cases).
|
|
463
|
-
- One top-level element per template (hard error otherwise);
|
|
464
|
-
|
|
465
|
-
`.TextContent`); no pipes, at most one of `*if`/`*for` per element (`*mount` is
|
|
463
|
+
- One top-level element per template (hard error otherwise); text and elements can mix
|
|
464
|
+
under one element (each text run becomes a `VElement.Text` leaf); no pipes, at most one of `*if`/`*for` per element (`*mount` is
|
|
466
465
|
orthogonal to both — see above — and isn't included in that limit).
|
|
467
466
|
- This is entirely a KopScript compiler feature (parsed/desugared before type-checking
|
|
468
467
|
runs) — Kopular's own framework code (`component.ks`, `dom.ks`) is unmodified and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@types/jsdom": "^30.0.0",
|
|
67
67
|
"@types/node": "^20.14.0",
|
|
68
68
|
"jsdom": "^25.0.1",
|
|
69
|
-
"kopscript": "^1.
|
|
69
|
+
"kopscript": "^1.3.0",
|
|
70
70
|
"typescript": "^5.5.0",
|
|
71
71
|
"vitest": "^4.1.11"
|
|
72
72
|
},
|
package/src/component.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Event, Element, Document, Location, History, Window, document, location, history, window } from "./dom.js";
|
|
2
2
|
import { VElement, NoOpEventHandler } from "./velement.js";
|
|
3
|
-
import { Batching, ScopedStyles, Materialize, Patch, UnmountPrevious, PatchChildren, UnmountTree } from "./vdom.js";
|
|
3
|
+
import { Batching, ScopedStyles, SubmitListener, Materialize, Patch, UnmountPrevious, PatchChildren, UnmountTree } from "./vdom.js";
|
|
4
4
|
|
|
5
5
|
export class Component {
|
|
6
6
|
constructor() {
|
package/src/dom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.js","sources":["dom.ks"],"sourcesContent":["// Minimal browser DOM bindings. All ambient (no `from` clause) — document,\n// Element, and Event genuinely exist as globals in a browser, no import\n// needed. Member names use the real JS casing exactly (camelCase), since\n// extern declarations describe an existing external contract rather than\n// idiomatic KopScript code — there's no per-member rename mechanism. `extern\n// class` declarations end in `;`, like the other two extern forms.\n\nextern class Event {\n Element target { get; }\n void preventDefault();\n};\n\nextern class Element {\n string textContent { get; set; }\n string id { get; set; }\n string className { get; set; }\n // The one property the vdom patch engine (src/vdom.ks) always sets via\n // direct property assignment, never setAttribute — setAttribute(\"value\",\n // x) sets the DEFAULT value attribute, not the current live one, a real\n // DOM footgun (and the exact property behind the original typing bug\n // this whole diffing effort traces back to).\n string value { get; set; }\n // Only ever assigned by the vdom patch engine for a VElement.RawHtml leaf\n // — never diffed into, an opaque blob the same way `raw string` is.\n string innerHTML { get; set; }\n void appendChild(Element child);\n void replaceChild(Element newChild, Element oldChild);\n // `referenceChild` is nullable — real DOM insertBefore(node, null) means\n // \"append at the end,\" used by the patch engine's child-reordering step.\n void insertBefore(Element newChild, Element? referenceChild);\n void removeChild(Element child);\n // The generic escape hatch VElement.SetAttr's ExtraNames/ExtraValues\n // patch through — real HTML attributes only (href, src, alt,\n // placeholder, ...), never `value` (see above).\n void setAttribute(string name, string value);\n void removeAttribute(string name);\n bool disabled { get; set; }\n bool checked { get; set; }\n void addEventListener(string eventType, (Event) => void handler);\n void removeEventListener(string eventType, (Event) => void handler);\n};\n\nextern class Document {\n Element createElement(string tagName);\n Element getElementById(string id);\n Element body { get; }\n // Where ScopedStyles.Inject (vdom.ks) puts a component type's own\n // rewritten <style> — a scoped stylesheet is global infrastructure\n // (injected once per type, never removed), not page content, so it\n // belongs in <head>, not wherever a given instance happens to mount.\n Element head { get; }\n};\n\nextern class Location {\n string pathname { get; }\n // The real query string including its leading \"?\" (e.g. \"?sort=name\"),\n // or \"\" if the current URL has none — see Router.ParseQuery/Query.\n string search { get; }\n};\n\nextern class History {\n // Param named `historyState`, not `state` — `state` is a KopScript\n // keyword (state<T>), not a valid parameter name.\n void pushState(string historyState, string title, string url);\n};\n\nextern class Window {\n void addEventListener(string eventType, (Event) => void handler);\n};\n\nextern Document document;\nextern Location location;\nextern History history;\nextern Window window;\n"],"names":[],"mappings":"AAOA;AAKA;AA8BA;
|
|
1
|
+
{"version":3,"file":"dom.js","sources":["dom.ks"],"sourcesContent":["// Minimal browser DOM bindings. All ambient (no `from` clause) — document,\n// Element, and Event genuinely exist as globals in a browser, no import\n// needed. Member names use the real JS casing exactly (camelCase), since\n// extern declarations describe an existing external contract rather than\n// idiomatic KopScript code — there's no per-member rename mechanism. `extern\n// class` declarations end in `;`, like the other two extern forms.\n\nextern class Event {\n Element target { get; }\n void preventDefault();\n};\n\nextern class Element {\n string textContent { get; set; }\n string id { get; set; }\n string className { get; set; }\n // The one property the vdom patch engine (src/vdom.ks) always sets via\n // direct property assignment, never setAttribute — setAttribute(\"value\",\n // x) sets the DEFAULT value attribute, not the current live one, a real\n // DOM footgun (and the exact property behind the original typing bug\n // this whole diffing effort traces back to).\n string value { get; set; }\n // Only ever assigned by the vdom patch engine for a VElement.RawHtml leaf\n // — never diffed into, an opaque blob the same way `raw string` is.\n string innerHTML { get; set; }\n void appendChild(Element child);\n void replaceChild(Element newChild, Element oldChild);\n // `referenceChild` is nullable — real DOM insertBefore(node, null) means\n // \"append at the end,\" used by the patch engine's child-reordering step.\n void insertBefore(Element newChild, Element? referenceChild);\n void removeChild(Element child);\n // The generic escape hatch VElement.SetAttr's ExtraNames/ExtraValues\n // patch through — real HTML attributes only (href, src, alt,\n // placeholder, ...), never `value` (see above).\n void setAttribute(string name, string value);\n void removeAttribute(string name);\n bool disabled { get; set; }\n bool checked { get; set; }\n void addEventListener(string eventType, (Event) => void handler);\n void removeEventListener(string eventType, (Event) => void handler);\n};\n\nextern class Document {\n Element createElement(string tagName);\n // Really returns a Text node, typed as Element because the patch engine\n // only ever uses the members the two share (textContent, and being passed\n // to appendChild/replaceChild/insertBefore/removeChild). Only vdom.ks\n // calls it, for a VElement.Text leaf.\n Element createTextNode(string data);\n Element getElementById(string id);\n Element body { get; }\n // Where ScopedStyles.Inject (vdom.ks) puts a component type's own\n // rewritten <style> — a scoped stylesheet is global infrastructure\n // (injected once per type, never removed), not page content, so it\n // belongs in <head>, not wherever a given instance happens to mount.\n Element head { get; }\n};\n\nextern class Location {\n string pathname { get; }\n // The real query string including its leading \"?\" (e.g. \"?sort=name\"),\n // or \"\" if the current URL has none — see Router.ParseQuery/Query.\n string search { get; }\n};\n\nextern class History {\n // Param named `historyState`, not `state` — `state` is a KopScript\n // keyword (state<T>), not a valid parameter name.\n void pushState(string historyState, string title, string url);\n};\n\nextern class Window {\n void addEventListener(string eventType, (Event) => void handler);\n};\n\nextern Document document;\nextern Location location;\nextern History history;\nextern Window window;\n"],"names":[],"mappings":"AAOA;AAKA;AA8BA;AAgBA;AAOA;AAMA;AAIA;AACA;AACA;AACA"}
|
package/src/dom.ks
CHANGED
|
@@ -42,6 +42,11 @@ extern class Element {
|
|
|
42
42
|
|
|
43
43
|
extern class Document {
|
|
44
44
|
Element createElement(string tagName);
|
|
45
|
+
// Really returns a Text node, typed as Element because the patch engine
|
|
46
|
+
// only ever uses the members the two share (textContent, and being passed
|
|
47
|
+
// to appendChild/replaceChild/insertBefore/removeChild). Only vdom.ks
|
|
48
|
+
// calls it, for a VElement.Text leaf.
|
|
49
|
+
Element createTextNode(string data);
|
|
45
50
|
Element getElementById(string id);
|
|
46
51
|
Element body { get; }
|
|
47
52
|
// Where ScopedStyles.Inject (vdom.ks) puts a component type's own
|
package/src/kopular.ks
CHANGED
|
@@ -85,6 +85,8 @@ extern class VElement {
|
|
|
85
85
|
static VElement Create(string tag);
|
|
86
86
|
// Embeds a live child Component as this slot's content.
|
|
87
87
|
static VElement Mount(Component component);
|
|
88
|
+
// A bare text node, for text next to elements in one parent.
|
|
89
|
+
static VElement Text(string content);
|
|
88
90
|
string TextContent { get; set; }
|
|
89
91
|
string ClassName { get; set; }
|
|
90
92
|
string Id { get; set; }
|
|
@@ -96,6 +98,8 @@ extern class VElement {
|
|
|
96
98
|
(Event) => void OnInput { get; set; }
|
|
97
99
|
(Event) => void OnBlur { get; set; }
|
|
98
100
|
(Event) => void OnChange { get; set; }
|
|
101
|
+
// Calls preventDefault() for you — a form submit never reloads the page.
|
|
102
|
+
(Event) => void OnSubmit { get; set; }
|
|
99
103
|
void AppendChild(VElement child);
|
|
100
104
|
void SetAttr(string name, string value);
|
|
101
105
|
} from "kopular/velement";
|
package/src/vdom.js
CHANGED
|
@@ -45,6 +45,12 @@ export class ScopedStyles {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
export function SubmitListener(handler) {
|
|
49
|
+
return (e) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
Batching.Run(() => (handler(e)));
|
|
52
|
+
};
|
|
53
|
+
}
|
|
48
54
|
export function Materialize(tree, parent) {
|
|
49
55
|
let maybeMounted = tree.Mounted;
|
|
50
56
|
if ((maybeMounted != null)) {
|
|
@@ -53,6 +59,11 @@ export function Materialize(tree, parent) {
|
|
|
53
59
|
tree.RealNode = mountedRoot;
|
|
54
60
|
return mountedRoot;
|
|
55
61
|
}
|
|
62
|
+
if (tree.IsText()) {
|
|
63
|
+
let text = document.createTextNode(tree.TextContent);
|
|
64
|
+
tree.RealNode = text;
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
56
67
|
let el = document.createElement(tree.Tag);
|
|
57
68
|
if ((tree.RawHtml.length > 0)) {
|
|
58
69
|
el.innerHTML = tree.RawHtml;
|
|
@@ -95,6 +106,11 @@ export function Materialize(tree, parent) {
|
|
|
95
106
|
tree.AttachedOnChange = wrapped;
|
|
96
107
|
el.addEventListener("change", wrapped);
|
|
97
108
|
}
|
|
109
|
+
if ((tree.OnSubmit !== NoOpEventHandler)) {
|
|
110
|
+
let wrapped = SubmitListener(tree.OnSubmit);
|
|
111
|
+
tree.AttachedOnSubmit = wrapped;
|
|
112
|
+
el.addEventListener("submit", wrapped);
|
|
113
|
+
}
|
|
98
114
|
tree.RealNode = el;
|
|
99
115
|
return el;
|
|
100
116
|
}
|
|
@@ -136,6 +152,12 @@ export function Patch(parent, old, updated) {
|
|
|
136
152
|
let created = Materialize(updated, parent);
|
|
137
153
|
parent.replaceChild(created, realNode);
|
|
138
154
|
return created;
|
|
155
|
+
} else if (updated.IsText()) {
|
|
156
|
+
updated.RealNode = realNode;
|
|
157
|
+
if ((updated.TextContent !== oldTree.TextContent)) {
|
|
158
|
+
realNode.textContent = updated.TextContent;
|
|
159
|
+
}
|
|
160
|
+
return realNode;
|
|
139
161
|
} else {
|
|
140
162
|
updated.RealNode = realNode;
|
|
141
163
|
if (((updated.RawHtml.length > 0) || (oldTree.RawHtml.length > 0))) {
|
|
@@ -216,6 +238,14 @@ export function Patch(parent, old, updated) {
|
|
|
216
238
|
} else {
|
|
217
239
|
updated.AttachedOnChange = oldTree.AttachedOnChange;
|
|
218
240
|
}
|
|
241
|
+
if ((updated.OnSubmit !== oldTree.OnSubmit)) {
|
|
242
|
+
realNode.removeEventListener("submit", oldTree.AttachedOnSubmit);
|
|
243
|
+
let wrappedSubmit = SubmitListener(updated.OnSubmit);
|
|
244
|
+
updated.AttachedOnSubmit = wrappedSubmit;
|
|
245
|
+
realNode.addEventListener("submit", wrappedSubmit);
|
|
246
|
+
} else {
|
|
247
|
+
updated.AttachedOnSubmit = oldTree.AttachedOnSubmit;
|
|
248
|
+
}
|
|
219
249
|
return realNode;
|
|
220
250
|
}
|
|
221
251
|
} else {
|
package/src/vdom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vdom.js","sources":["vdom.ks"],"sourcesContent":["using \"./dom\";\nusing \"./velement\";\n\n// The diff/patch engine behind real vdom diffing: Component.Update() (see\n// component.ks) calls Patch() with the PREVIOUS render's VElement tree\n// (which carries each node's real, live DOM counterpart via its own\n// RealNode field) and the NEW tree Render() just produced, and gets back\n// real DOM mutated/reused in place wherever possible instead of a full\n// subtree rebuild.\n//\n// Deliberately never reads the live DOM back to rediscover structure (no\n// \"get current children\"/\"get current tag\" API exists, or is needed) —\n// the retained *previous* VElement tree already records everything Patch()\n// needs to know about what's currently there. This is what makes the whole\n// approach work without a generic children/attributes read-back API that\n// KopScript's narrow, curated DOM binding doesn't have.\n\n// A component-like thing Batching (below) can flush once every DOM-event-\n// triggered Update() call inside one batch has registered itself. Declared\n// HERE, not in component.ks, specifically so THIS file's own Materialize/\n// Patch — which need to start/stop a batch around every real event\n// dispatch — never have to `using \"./component\"`: component.ks already\n// `using`s this file for Materialize/Patch themselves, and KopScript\n// rejects a circular `using` outright. Component (component.ks) is the\n// one real implementer, via `class Component : Flushable`.\ninterface Flushable {\n void FlushUpdate();\n}\n\n// Coalesces every Update() call made while a real DOM event handler\n// Kopular itself attached (see Materialize/Patch below, both of which wrap\n// each listener in Batching.Run before attaching it) into one flush per\n// affected component, applied once that handler returns — not one flush\n// per state<T> write inside it. See Component.Update()'s own comment\n// (component.ks) for the full rationale; this is just the mechanism.\n//\n// Deliberately class-level, shared across every batch/component rather\n// than per-instance — a single click can trigger Update() on more than one\n// component (e.g. a shared service's state<T> notifying two sibling\n// pages), and all of them need to flush together, once, when that one\n// handler finishes.\nclass Batching {\n private static number Depth = 0;\n private static Flushable[] Pending = [];\n\n public static bool IsActive() {\n return Batching.Depth > 0;\n }\n\n public static void Defer(Flushable f) {\n if (!Batching.Pending.Includes(f)) {\n Batching.Pending = Batching.Pending.Push(f);\n }\n }\n\n // `try`/`finally`, not a bare sequence: a handler that throws must still\n // decrement Depth and flush whatever already-triggered renders are\n // pending, or one uncaught exception would wedge every future click/\n // input on the page into \"always batching, never rendering.\" The\n // exception itself still propagates unchanged — this never catches it,\n // only guarantees the cleanup runs.\n public static void Run(() => void action) {\n Batching.Depth = Batching.Depth + 1;\n try {\n action();\n } finally {\n Batching.Depth = Batching.Depth - 1;\n if (Batching.Depth == 0) {\n Flushable[] toFlush = Batching.Pending;\n Batching.Pending = [];\n foreach (Flushable f in toFlush) {\n f.FlushUpdate();\n }\n }\n }\n }\n}\n\n// The runtime half of kopscript's `styles from \"<path>.css\";` (see its own\n// README/LLM.md \"Templates\" section) — the compiler rewrites a class's own\n// stylesheet at compile time (every selector scoped to that class's\n// `data-kop-scope=\"<id>\"` attribute) and splices exactly one\n// `ScopedStyles.Inject(id, css);` call into its constructor. Same\n// static-array-registry-with-Includes-dedup shape as Batching above,\n// deliberately: dedup here is per component TYPE, not per instance — every\n// instance's constructor calls Inject with the same `id`/`css` (both\n// compile-time constants for that class), so only the first one actually\n// creates a <style> tag; the rest are no-ops. Never removed once injected\n// — a scoped stylesheet is global infrastructure for as long as the page\n// lives, not per-instance content Teardown() would ever need to clean up.\nclass ScopedStyles {\n private static string[] Injected = [];\n\n public static void Inject(string scopeId, string css) {\n if (!ScopedStyles.Injected.Includes(scopeId)) {\n ScopedStyles.Injected = ScopedStyles.Injected.Push(scopeId);\n Element style = document.createElement(\"style\");\n style.setAttribute(\"data-kop-scope-sheet\", scopeId);\n style.textContent = css;\n document.head.appendChild(style);\n }\n }\n}\n\n// Builds a brand-new, fully real DOM subtree from a VElement tree with no\n// diffing at all — first mount, or whenever Patch() decides a subtree must\n// be replaced outright (no previous node to reuse, or the tag changed).\n// Mutates `tree.RealNode` (and recursively every descendant's) as a side\n// effect, so the tree this was called on becomes the new \"previous tree\"\n// the next Patch() call diffs against. Takes `parent` — every call site\n// already knows it (either Patch's own `parent` parameter, or the real\n// element a recursive child call is about to be appendChild'd into) —\n// solely to hand to a Mounted slot's own MountAsChild, which needs to\n// remember its real parent for THAT component's own future self-triggered\n// re-renders; an ordinary (non-Mounted) VElement never uses it.\nElement Materialize(VElement tree, Element parent) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n Element mountedRoot = m.MountAsChild(parent);\n tree.RealNode = mountedRoot;\n return mountedRoot;\n }\n\n Element el = document.createElement(tree.Tag);\n\n if (tree.RawHtml.Length > 0) {\n el.innerHTML = tree.RawHtml;\n } else if (tree.Children.Length > 0) {\n foreach (VElement child in tree.Children) {\n el.appendChild(Materialize(child, el));\n }\n } else {\n el.textContent = tree.TextContent;\n }\n\n el.className = tree.ClassName;\n el.id = tree.Id;\n el.value = tree.Value;\n\n for (number i = 0; i < tree.ExtraNames.Length; i = i + 1) {\n el.setAttribute(tree.ExtraNames[i], tree.ExtraValues[i]);\n }\n if (tree.Disabled) {\n el.disabled = true;\n }\n if (tree.Checked) {\n el.checked = true;\n }\n\n // Skip attaching VElement's own shared no-op (see velement.ks) — it does\n // nothing when invoked, so registering it costs real work (a listener\n // list entry, held onto for nothing) for zero benefit. A real handler\n // (never equal to the shared no-op) always gets attached as before —\n // wrapped in Batching.Run so every Update() it triggers coalesces with\n // any others from the same dispatch (see Component.Update()'s own\n // comment). The wrapper itself, not tree.OnClick, is what actually gets\n // registered — recorded on tree.AttachedOnClick (velement.ks) so a later\n // Patch() can remove this exact reference, not the handler value itself.\n if (tree.OnClick != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnClick(e));\n tree.AttachedOnClick = wrapped;\n el.addEventListener(\"click\", wrapped);\n }\n if (tree.OnInput != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnInput(e));\n tree.AttachedOnInput = wrapped;\n el.addEventListener(\"input\", wrapped);\n }\n if (tree.OnBlur != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnBlur(e));\n tree.AttachedOnBlur = wrapped;\n el.addEventListener(\"blur\", wrapped);\n }\n if (tree.OnChange != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnChange(e));\n tree.AttachedOnChange = wrapped;\n el.addEventListener(\"change\", wrapped);\n }\n\n tree.RealNode = el;\n return el;\n}\n\n// Diffs `updated` against `old` (the previous render's tree for this exact\n// position, or null if there is none — first mount) and returns the real\n// DOM node now representing `updated`, reusing `old`'s real node in place\n// whenever the tag matches. `parent` is only used to attach/replace at the\n// top of whatever subtree Patch() is called on — child-level attach/replace\n// happens inside PatchChildren.\n//\n// Deliberately all positive-branch `if (x != null) { ... } else { ... }`,\n// never an early-return guard clause — KopScript's nullable narrowing is\n// scope-based, not reachability-based, so `if (x == null) { return; }\n// use(x);` would NOT narrow `x` afterward (see KopScript's own LLM.md \"Common\n// mistakes\"). Every nullable member-access path (`oldTree.RealNode`,\n// never narrows directly either) is read into a local first for the same\n// reason.\nElement Patch(Element parent, VElement? old, VElement updated) {\n Mountable? newMounted = updated.Mounted;\n Mountable? oldMounted = null;\n if (old != null) {\n VElement oldTreeForMount = old;\n oldMounted = oldTreeForMount.Mounted;\n }\n\n // A live child slot (VElement.Mounted — see velement.ks) is handled\n // entirely separately from the ordinary Tag-based logic below: it's not\n // Kopular's own DOM element to create/reuse at all, and its \"same node\n // or replace\" decision is instance identity (== on the Mountable itself,\n // real reference equality — interfaces erase to the underlying object),\n // not Tag equality.\n if (newMounted != null) {\n Mountable nm = newMounted;\n if (oldMounted != null) {\n Mountable om = oldMounted;\n if (om == nm) {\n // Same instance still in this slot: patch it in place, don't touch\n // the DOM position at all (its own real node, reused or not, is\n // already exactly where it needs to be).\n Element reused = nm.PatchAsChild();\n updated.RealNode = reused;\n return reused;\n }\n }\n // First time this slot has anything mounted, or a DIFFERENT instance\n // took over the slot: release whatever was here, then mount fresh.\n UnmountPrevious(parent, old, oldMounted);\n Element created = nm.MountAsChild(parent);\n parent.appendChild(created);\n updated.RealNode = created;\n return created;\n }\n\n if (oldMounted != null) {\n // Slot reverted from a live component back to plain content: release\n // the old one, then fall through to an ordinary fresh materialize.\n UnmountPrevious(parent, old, oldMounted);\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element realNode = maybeOldNode;\n if (oldTree.Tag != updated.Tag) {\n Element created = Materialize(updated, parent);\n parent.replaceChild(created, realNode);\n return created;\n } else {\n updated.RealNode = realNode;\n\n if (updated.RawHtml.Length > 0 || oldTree.RawHtml.Length > 0) {\n if (updated.RawHtml != oldTree.RawHtml) {\n realNode.innerHTML = updated.RawHtml;\n }\n } else if (updated.Children.Length > 0) {\n // Children own this node's content now. If the old tree was a\n // plain text leaf (no tracked children of its own), any raw text\n // Materialize/a previous Patch left directly in the real node is\n // untracked by PatchChildren (which only knows about VElement-\n // backed children) — clear it first, or the new children would\n // just be appended alongside stray leftover text.\n if (oldTree.Children.Length == 0) {\n realNode.textContent = \"\";\n }\n PatchChildren(realNode, oldTree.Children, updated.Children);\n } else {\n // updated is a plain text leaf (or empty). Any of oldTree's own\n // real children need tearing down/removing properly first\n // (nested Mounted components included, via PatchChildren's own\n // Pass 4) — textContent's native browser side effect would\n // otherwise silently detach them without running their own\n // OnUnmount(), and a later PatchChildren call against those now-\n // orphaned nodes would throw trying to remove an already-detached\n // child.\n if (oldTree.Children.Length > 0) {\n PatchChildren(realNode, oldTree.Children, []);\n }\n if (updated.TextContent != oldTree.TextContent) {\n realNode.textContent = updated.TextContent;\n }\n }\n\n if (updated.ClassName != oldTree.ClassName) {\n realNode.className = updated.ClassName;\n }\n if (updated.Id != oldTree.Id) {\n realNode.id = updated.Id;\n }\n // Always assigned, never conditionally on updated.Value != oldTree.Value\n // — unlike TextContent/ClassName/Id, an <input>/<select>'s live value\n // can diverge from the last-recorded VElement.Value purely through\n // user interaction (typing, picking an option) with no Update() ever\n // running in between (a real, deliberate pattern — see KopularDemo's\n // dogs_page.ks, which never Update()s on input/change). The recorded\n // oldTree.Value only reflects the tree as of the last actual render,\n // so comparing against it can't tell \"genuinely unchanged\" apart from\n // \"changed live in the DOM since then, framework never told\" — the\n // same reason a real \"controlled input\" (React's own term for this)\n // always writes value on every render rather than diffing it.\n realNode.value = updated.Value;\n\n // Same-length is the overwhelmingly common case (the same Render()\n // code path calls SetAttr the same number of times, in the same\n // order, on every call) — compare aligned by index and only touch\n // the real DOM for an entry that actually changed, rather than\n // reapplying every extra attribute on every patch regardless. A\n // length mismatch (the rarer case: a SetAttr call was added,\n // removed, or made conditional between renders) falls back to\n // reapplying everything, since index-aligned comparison isn't\n // meaningful once the two arrays don't correspond entry-for-entry.\n if (updated.ExtraNames.Length == oldTree.ExtraNames.Length) {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n if (updated.ExtraNames[i] != oldTree.ExtraNames[i] || updated.ExtraValues[i] != oldTree.ExtraValues[i]) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n } else {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n foreach (string oldName in oldTree.ExtraNames) {\n if (!updated.ExtraNames.Includes(oldName)) {\n realNode.removeAttribute(oldName);\n }\n }\n\n if (updated.Disabled != oldTree.Disabled) {\n realNode.disabled = updated.Disabled;\n }\n // Like Value above, a checkbox's live checked state changes on click\n // with no render in between, so a checked one is re-asserted every\n // patch rather than diffed against the previous tree.\n if (updated.Checked || oldTree.Checked) {\n realNode.checked = updated.Checked;\n }\n\n // Swap a listener only when the handler reference actually changed.\n // A node that never sets a real OnClick/OnInput/OnBlur/OnChange\n // keeps VElement's own shared NoOpEventHandler reference on both\n // sides (see velement.ks) — comparing by `!=` costs nothing and\n // skips two real DOM API calls per event per node for the (common)\n // case of \"this node has no handler of this kind either time,\"\n // which matters a lot on a list with hundreds/thousands of rows\n // most of which set at most one or two of the four. A node WITH a\n // real handler still gets a fresh closure every render (it\n // captures per-render values, like a loop's own item), so it still\n // swaps every time — correctly, since the old closure really is\n // stale.\n //\n // The actually-attached listener is always a Batching.Run wrapper\n // (see Materialize above), never `oldTree.OnClick`/`updated.OnClick`\n // themselves — removeEventListener only works when passed the exact\n // reference addEventListener received, so removal always goes\n // through `oldTree.AttachedOnClick` (the wrapper Materialize/a\n // previous Patch actually registered), and a real swap builds a\n // fresh wrapper the same way Materialize does. When nothing swaps,\n // `updated.AttachedOnClick` still needs to carry `oldTree`'s\n // forward — `updated` is a brand-new VElement whose own\n // AttachedOnClick starts back at the shared no-op (velement.ks's\n // constructor), and it becomes the retained \"old tree\" the very\n // next Patch() call diffs against.\n if (updated.OnClick != oldTree.OnClick) {\n realNode.removeEventListener(\"click\", oldTree.AttachedOnClick);\n (Event) => void wrappedClick = (Event e) => Batching.Run(() => updated.OnClick(e));\n updated.AttachedOnClick = wrappedClick;\n realNode.addEventListener(\"click\", wrappedClick);\n } else {\n updated.AttachedOnClick = oldTree.AttachedOnClick;\n }\n if (updated.OnInput != oldTree.OnInput) {\n realNode.removeEventListener(\"input\", oldTree.AttachedOnInput);\n (Event) => void wrappedInput = (Event e) => Batching.Run(() => updated.OnInput(e));\n updated.AttachedOnInput = wrappedInput;\n realNode.addEventListener(\"input\", wrappedInput);\n } else {\n updated.AttachedOnInput = oldTree.AttachedOnInput;\n }\n if (updated.OnBlur != oldTree.OnBlur) {\n realNode.removeEventListener(\"blur\", oldTree.AttachedOnBlur);\n (Event) => void wrappedBlur = (Event e) => Batching.Run(() => updated.OnBlur(e));\n updated.AttachedOnBlur = wrappedBlur;\n realNode.addEventListener(\"blur\", wrappedBlur);\n } else {\n updated.AttachedOnBlur = oldTree.AttachedOnBlur;\n }\n if (updated.OnChange != oldTree.OnChange) {\n realNode.removeEventListener(\"change\", oldTree.AttachedOnChange);\n (Event) => void wrappedChange = (Event e) => Batching.Run(() => updated.OnChange(e));\n updated.AttachedOnChange = wrappedChange;\n realNode.addEventListener(\"change\", wrappedChange);\n } else {\n updated.AttachedOnChange = oldTree.AttachedOnChange;\n }\n\n return realNode;\n }\n } else {\n // Shouldn't happen in practice (every previously-rendered tree has a\n // real node by the time a second render diffs against it) — treated\n // as \"nothing to reuse\" rather than a crash, same defensive spirit\n // as Component's own IsMounted guard.\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n } else {\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n}\n\n// Releases whatever was previously mounted in a slot (calling its\n// Teardown, which — for a Component — cascades into anything IT mounted\n// too, however many levels deep, before its own app-facing OnUnmount runs)\n// and removes its real node from the DOM, before a different Mountable (or\n// plain content) takes over that slot. A no-op when there was nothing\n// mounted here before — the common \"first render of this slot\" case.\nvoid UnmountPrevious(Element parent, VElement? old, Mountable? oldMounted) {\n // Teardown() only applies when there really was a Mounted instance here\n // before (nothing to tear down for plain content) — but the real node\n // itself needs removing whenever `old` had one, Mounted or not: a plain\n // VElement's slot turning into a Mounted one is exactly as much a\n // wholesale replacement as the reverse direction (handled by falling\n // through to Materialize below in Patch), and both need the OLD node\n // gone before the NEW one is appended, not left behind as a stray\n // sibling.\n if (oldMounted != null) {\n Mountable m = oldMounted;\n m.Teardown();\n }\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Keyed reconciliation: each VElement's own Id is its key when non-empty —\n// a real, existing DOM convention, needing no new API or syntax. A new\n// child whose Id matches an old child's Id is patched against that old\n// child (reusing its real node) regardless of position; a new child with\n// no Id, or an Id not present among the old children, falls back to\n// pairing positionally against whatever old children are still unconsumed,\n// in order. DOCUMENTED, REAL LIMITATION: without stable Ids, a reordered\n// list still produces the correct final output, but a given item's real\n// DOM node (and anything stateful attached to it, like focus) isn't\n// guaranteed to follow its data across the reorder — give list items a\n// stable Id for that guarantee.\nvoid PatchChildren(Element parent, VElement[] oldChildren, VElement[] newChildren) {\n // Map, not a Push loop — Push is deliberately non-mutating (a real\n // spread-copy every call, see KopScript's own README), so building an\n // n-length array by Push-ing once per element in a loop is an\n // accidental O(n^2) on every single PatchChildren call, however small\n // the actual diff. Map is a real, single O(n) pass straight to\n // Array.prototype.map.\n bool[] oldConsumed = oldChildren.Map((VElement c) => false);\n number[] matchedOldIndex = newChildren.Map((VElement c) => -1);\n\n // Pass 1: keyed matches, by Id.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (newChildren[i].Id.Length == 0) { continue; }\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (!oldConsumed[j] && oldChildren[j].Id == newChildren[i].Id) {\n matchedOldIndex[i] = j;\n oldConsumed[j] = true;\n break;\n }\n }\n }\n\n // Pass 2: positional fallback for everything Pass 1 didn't match —\n // pair each remaining new child against the next still-unconsumed old\n // child, in order.\n number nextOld = 0;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] >= 0) { continue; }\n while (nextOld < oldChildren.Length && oldConsumed[nextOld]) {\n nextOld = nextOld + 1;\n }\n if (nextOld < oldChildren.Length) {\n matchedOldIndex[i] = nextOld;\n oldConsumed[nextOld] = true;\n nextOld = nextOld + 1;\n }\n }\n\n // Whether anything is actually moving at all — same length, and every\n // new position matched the *same* old position. The extremely common\n // case for a list that's only had some of its rows' own content change\n // (e.g. \"update every 10th row\"), where reconciliation still has real\n // work to do (see Pass 1/2 above and Patch() itself) but nothing needs\n // to physically move in the DOM at all.\n bool needsReorder = oldChildren.Length != newChildren.Length;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] != i) {\n needsReorder = true;\n break;\n }\n }\n\n // Pass 3: patch/create each new child in order, then — only if the list\n // actually needs reordering — move it into its correct final position.\n // appendChild on a node already attached elsewhere in the DOM MOVES it\n // (real DOM semantics), so processing new children in their final\n // desired order and always appending naturally builds up the correct\n // sequence, no separate insertBefore/reference-node bookkeeping needed.\n // Safe because VElement.Children is always the COMPLETE list of a\n // node's children — nothing else ever shares `parent`. Skipping the\n // move entirely when `needsReorder` is false avoids a real DOM API call\n // per child for the common no-reorder case — Patch() itself already\n // updates or replaces a reused/changed node exactly in place either way.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n VElement? matchedOld = null;\n if (matchedOldIndex[i] >= 0) {\n matchedOld = oldChildren[matchedOldIndex[i]];\n }\n Element childNode = Patch(parent, matchedOld, newChildren[i]);\n if (needsReorder) {\n parent.appendChild(childNode);\n }\n }\n\n // Pass 4: remove whatever old children never got reused — releasing\n // anything Mounted first (see velement.ks) so a child genuinely dropped\n // from a list (not replaced in the same slot by a different one, which\n // Patch() itself already handles via UnmountPrevious — removed outright)\n // still gets torn down. This is the one place THAT case is reachable\n // from, since Patch() only ever sees one slot at a time, never a slot\n // disappearing entirely.\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (oldConsumed[j]) { continue; }\n Mountable? maybeOldMounted = oldChildren[j].Mounted;\n if (maybeOldMounted != null) {\n Mountable m = maybeOldMounted;\n m.Teardown();\n }\n Element? maybeOldNode = oldChildren[j].RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Recursively finds and tears down every Mounted node within `tree` (tree\n// itself included) — used by Component's own Teardown (see component.ks)\n// so removing ONE component transitively releases everything IT rendered,\n// however many Mounted levels deep, not only the outermost one. A pure\n// data walk over the retained VElement tree; never touches the real DOM\n// itself (the caller already owns removing whatever real node is actually\n// attached).\nvoid UnmountTree(VElement tree) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n m.Teardown();\n }\n foreach (VElement child in tree.Children) {\n UnmountTree(child);\n }\n}\n"],"names":[],"mappings":";;;AAyCA;EACiB;;EACA;;EAED;IACZ;;;EAGY;IACZ;MACmB;;;;EAUP;IACG;IACf;MACQ;;MAES;MACf;QACE;QACiB;QACjB;UACe;;;;;;AAmBvB;EACiB;;EAED;IACZ;MACwB;MACtB;MACkB;MACA;MACO;;;;AAgB/B;EACE;EACA;IACE;IACA;IACc;IACd;;EAGF;EAEA;IACe;;IAEb;MACgB;;;IAGD;;EAGJ;EACP;EACG;EAET;IACiB;;EAEjB;IACc;;EAEd;IACa;;EAYb;IACE;IACqB;IACF;;EAErB;IACE;IACqB;IACF;;EAErB;IACE;IACoB;IACD;;EAErB;IACE;IACsB;IACH;;EAGP;EACd;;AAiBF;EACE;EACA;EACA;IACE;IACW;;EASb;IACE;IACA;MACE;MACA;QAIE;QACiB;QACjB;;;IAKW;IACf;IACkB;IACD;IACjB;;EAGF;IAGiB;IACf;IACkB;IAClB;;EAGF;IACE;IACA;IACA;MACE;MACA;QACE;QACmB;QACnB;;QAEiB;QAEjB;UACE;YACqB;;;UASrB;YACuB;;UAEV;;UAUb;YACe;;UAEf;YACuB;;;QAIzB;UACqB;;QAErB;UACc;;QAaC;QAWf;UACE;YACE;cACuB;;;;UAIzB;YACuB;;;QAGzB;UACE;YAC0B;;;QAI5B;UACoB;;QAKpB;UACmB;;QA4BnB;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACuB;UACE;;UAEF;;QAEzB;UAC8B;UAC5B;UACyB;UACA;;UAEA;;QAG3B;;;MAOF;MACkB;MAClB;;;IAGF;IACkB;IAClB;;;AAUJ;EASE;IACE;IACU;;EAEZ;IACE;IACA;IACA;MACE;MACkB;;;;AAgBxB;EAOE;EACA;EAGA;IACE;MAAqC;;IACrC;MACE;QACqB;QACJ;QACf;;;;EAQN;EACA;IACE;MAA+B;;IAC/B;MACU;;IAEV;MACqB;MACE;MACb;;;EAUZ;EACA;IACE;MACe;MACb;;;EAeJ;IACE;IACA;MACa;;IAEb;IACA;MACoB;;;EAWtB;IACE;MAAsB;;IACtB;IACA;MACE;MACU;;IAEZ;IACA;MACE;MACkB;;;;AAYxB;EACE;EACA;IACE;IACU;;EAEZ;IACa"}
|
|
1
|
+
{"version":3,"file":"vdom.js","sources":["vdom.ks"],"sourcesContent":["using \"./dom\";\nusing \"./velement\";\n\n// The diff/patch engine behind real vdom diffing: Component.Update() (see\n// component.ks) calls Patch() with the PREVIOUS render's VElement tree\n// (which carries each node's real, live DOM counterpart via its own\n// RealNode field) and the NEW tree Render() just produced, and gets back\n// real DOM mutated/reused in place wherever possible instead of a full\n// subtree rebuild.\n//\n// Deliberately never reads the live DOM back to rediscover structure (no\n// \"get current children\"/\"get current tag\" API exists, or is needed) —\n// the retained *previous* VElement tree already records everything Patch()\n// needs to know about what's currently there. This is what makes the whole\n// approach work without a generic children/attributes read-back API that\n// KopScript's narrow, curated DOM binding doesn't have.\n\n// A component-like thing Batching (below) can flush once every DOM-event-\n// triggered Update() call inside one batch has registered itself. Declared\n// HERE, not in component.ks, specifically so THIS file's own Materialize/\n// Patch — which need to start/stop a batch around every real event\n// dispatch — never have to `using \"./component\"`: component.ks already\n// `using`s this file for Materialize/Patch themselves, and KopScript\n// rejects a circular `using` outright. Component (component.ks) is the\n// one real implementer, via `class Component : Flushable`.\ninterface Flushable {\n void FlushUpdate();\n}\n\n// Coalesces every Update() call made while a real DOM event handler\n// Kopular itself attached (see Materialize/Patch below, both of which wrap\n// each listener in Batching.Run before attaching it) into one flush per\n// affected component, applied once that handler returns — not one flush\n// per state<T> write inside it. See Component.Update()'s own comment\n// (component.ks) for the full rationale; this is just the mechanism.\n//\n// Deliberately class-level, shared across every batch/component rather\n// than per-instance — a single click can trigger Update() on more than one\n// component (e.g. a shared service's state<T> notifying two sibling\n// pages), and all of them need to flush together, once, when that one\n// handler finishes.\nclass Batching {\n private static number Depth = 0;\n private static Flushable[] Pending = [];\n\n public static bool IsActive() {\n return Batching.Depth > 0;\n }\n\n public static void Defer(Flushable f) {\n if (!Batching.Pending.Includes(f)) {\n Batching.Pending = Batching.Pending.Push(f);\n }\n }\n\n // `try`/`finally`, not a bare sequence: a handler that throws must still\n // decrement Depth and flush whatever already-triggered renders are\n // pending, or one uncaught exception would wedge every future click/\n // input on the page into \"always batching, never rendering.\" The\n // exception itself still propagates unchanged — this never catches it,\n // only guarantees the cleanup runs.\n public static void Run(() => void action) {\n Batching.Depth = Batching.Depth + 1;\n try {\n action();\n } finally {\n Batching.Depth = Batching.Depth - 1;\n if (Batching.Depth == 0) {\n Flushable[] toFlush = Batching.Pending;\n Batching.Pending = [];\n foreach (Flushable f in toFlush) {\n f.FlushUpdate();\n }\n }\n }\n }\n}\n\n// The runtime half of kopscript's `styles from \"<path>.css\";` (see its own\n// README/LLM.md \"Templates\" section) — the compiler rewrites a class's own\n// stylesheet at compile time (every selector scoped to that class's\n// `data-kop-scope=\"<id>\"` attribute) and splices exactly one\n// `ScopedStyles.Inject(id, css);` call into its constructor. Same\n// static-array-registry-with-Includes-dedup shape as Batching above,\n// deliberately: dedup here is per component TYPE, not per instance — every\n// instance's constructor calls Inject with the same `id`/`css` (both\n// compile-time constants for that class), so only the first one actually\n// creates a <style> tag; the rest are no-ops. Never removed once injected\n// — a scoped stylesheet is global infrastructure for as long as the page\n// lives, not per-instance content Teardown() would ever need to clean up.\nclass ScopedStyles {\n private static string[] Injected = [];\n\n public static void Inject(string scopeId, string css) {\n if (!ScopedStyles.Injected.Includes(scopeId)) {\n ScopedStyles.Injected = ScopedStyles.Injected.Push(scopeId);\n Element style = document.createElement(\"style\");\n style.setAttribute(\"data-kop-scope-sheet\", scopeId);\n style.textContent = css;\n document.head.appendChild(style);\n }\n }\n}\n\n// A submit listener: preventDefault() first (no full-page navigation, ever),\n// then the handler, batched like every other event (see Materialize).\n(Event) => void SubmitListener((Event) => void handler) {\n return (Event e) => {\n e.preventDefault();\n Batching.Run(() => handler(e));\n };\n}\n\n// Builds a brand-new, fully real DOM subtree from a VElement tree with no\n// diffing at all — first mount, or whenever Patch() decides a subtree must\n// be replaced outright (no previous node to reuse, or the tag changed).\n// Mutates `tree.RealNode` (and recursively every descendant's) as a side\n// effect, so the tree this was called on becomes the new \"previous tree\"\n// the next Patch() call diffs against. Takes `parent` — every call site\n// already knows it (either Patch's own `parent` parameter, or the real\n// element a recursive child call is about to be appendChild'd into) —\n// solely to hand to a Mounted slot's own MountAsChild, which needs to\n// remember its real parent for THAT component's own future self-triggered\n// re-renders; an ordinary (non-Mounted) VElement never uses it.\nElement Materialize(VElement tree, Element parent) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n Element mountedRoot = m.MountAsChild(parent);\n tree.RealNode = mountedRoot;\n return mountedRoot;\n }\n\n if (tree.IsText()) {\n Element text = document.createTextNode(tree.TextContent);\n tree.RealNode = text;\n return text;\n }\n\n Element el = document.createElement(tree.Tag);\n\n if (tree.RawHtml.Length > 0) {\n el.innerHTML = tree.RawHtml;\n } else if (tree.Children.Length > 0) {\n foreach (VElement child in tree.Children) {\n el.appendChild(Materialize(child, el));\n }\n } else {\n el.textContent = tree.TextContent;\n }\n\n el.className = tree.ClassName;\n el.id = tree.Id;\n el.value = tree.Value;\n\n for (number i = 0; i < tree.ExtraNames.Length; i = i + 1) {\n el.setAttribute(tree.ExtraNames[i], tree.ExtraValues[i]);\n }\n if (tree.Disabled) {\n el.disabled = true;\n }\n if (tree.Checked) {\n el.checked = true;\n }\n\n // Skip attaching VElement's own shared no-op (see velement.ks) — it does\n // nothing when invoked, so registering it costs real work (a listener\n // list entry, held onto for nothing) for zero benefit. A real handler\n // (never equal to the shared no-op) always gets attached as before —\n // wrapped in Batching.Run so every Update() it triggers coalesces with\n // any others from the same dispatch (see Component.Update()'s own\n // comment). The wrapper itself, not tree.OnClick, is what actually gets\n // registered — recorded on tree.AttachedOnClick (velement.ks) so a later\n // Patch() can remove this exact reference, not the handler value itself.\n if (tree.OnClick != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnClick(e));\n tree.AttachedOnClick = wrapped;\n el.addEventListener(\"click\", wrapped);\n }\n if (tree.OnInput != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnInput(e));\n tree.AttachedOnInput = wrapped;\n el.addEventListener(\"input\", wrapped);\n }\n if (tree.OnBlur != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnBlur(e));\n tree.AttachedOnBlur = wrapped;\n el.addEventListener(\"blur\", wrapped);\n }\n if (tree.OnChange != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnChange(e));\n tree.AttachedOnChange = wrapped;\n el.addEventListener(\"change\", wrapped);\n }\n if (tree.OnSubmit != NoOpEventHandler) {\n (Event) => void wrapped = SubmitListener(tree.OnSubmit);\n tree.AttachedOnSubmit = wrapped;\n el.addEventListener(\"submit\", wrapped);\n }\n\n tree.RealNode = el;\n return el;\n}\n\n// Diffs `updated` against `old` (the previous render's tree for this exact\n// position, or null if there is none — first mount) and returns the real\n// DOM node now representing `updated`, reusing `old`'s real node in place\n// whenever the tag matches. `parent` is only used to attach/replace at the\n// top of whatever subtree Patch() is called on — child-level attach/replace\n// happens inside PatchChildren.\n//\n// Deliberately all positive-branch `if (x != null) { ... } else { ... }`,\n// never an early-return guard clause — KopScript's nullable narrowing is\n// scope-based, not reachability-based, so `if (x == null) { return; }\n// use(x);` would NOT narrow `x` afterward (see KopScript's own LLM.md \"Common\n// mistakes\"). Every nullable member-access path (`oldTree.RealNode`,\n// never narrows directly either) is read into a local first for the same\n// reason.\nElement Patch(Element parent, VElement? old, VElement updated) {\n Mountable? newMounted = updated.Mounted;\n Mountable? oldMounted = null;\n if (old != null) {\n VElement oldTreeForMount = old;\n oldMounted = oldTreeForMount.Mounted;\n }\n\n // A live child slot (VElement.Mounted — see velement.ks) is handled\n // entirely separately from the ordinary Tag-based logic below: it's not\n // Kopular's own DOM element to create/reuse at all, and its \"same node\n // or replace\" decision is instance identity (== on the Mountable itself,\n // real reference equality — interfaces erase to the underlying object),\n // not Tag equality.\n if (newMounted != null) {\n Mountable nm = newMounted;\n if (oldMounted != null) {\n Mountable om = oldMounted;\n if (om == nm) {\n // Same instance still in this slot: patch it in place, don't touch\n // the DOM position at all (its own real node, reused or not, is\n // already exactly where it needs to be).\n Element reused = nm.PatchAsChild();\n updated.RealNode = reused;\n return reused;\n }\n }\n // First time this slot has anything mounted, or a DIFFERENT instance\n // took over the slot: release whatever was here, then mount fresh.\n UnmountPrevious(parent, old, oldMounted);\n Element created = nm.MountAsChild(parent);\n parent.appendChild(created);\n updated.RealNode = created;\n return created;\n }\n\n if (oldMounted != null) {\n // Slot reverted from a live component back to plain content: release\n // the old one, then fall through to an ordinary fresh materialize.\n UnmountPrevious(parent, old, oldMounted);\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element realNode = maybeOldNode;\n if (oldTree.Tag != updated.Tag) {\n Element created = Materialize(updated, parent);\n parent.replaceChild(created, realNode);\n return created;\n } else if (updated.IsText()) {\n updated.RealNode = realNode;\n if (updated.TextContent != oldTree.TextContent) {\n realNode.textContent = updated.TextContent;\n }\n return realNode;\n } else {\n updated.RealNode = realNode;\n\n if (updated.RawHtml.Length > 0 || oldTree.RawHtml.Length > 0) {\n if (updated.RawHtml != oldTree.RawHtml) {\n realNode.innerHTML = updated.RawHtml;\n }\n } else if (updated.Children.Length > 0) {\n // Children own this node's content now. If the old tree was a\n // plain text leaf (no tracked children of its own), any raw text\n // Materialize/a previous Patch left directly in the real node is\n // untracked by PatchChildren (which only knows about VElement-\n // backed children) — clear it first, or the new children would\n // just be appended alongside stray leftover text.\n if (oldTree.Children.Length == 0) {\n realNode.textContent = \"\";\n }\n PatchChildren(realNode, oldTree.Children, updated.Children);\n } else {\n // updated is a plain text leaf (or empty). Any of oldTree's own\n // real children need tearing down/removing properly first\n // (nested Mounted components included, via PatchChildren's own\n // Pass 4) — textContent's native browser side effect would\n // otherwise silently detach them without running their own\n // OnUnmount(), and a later PatchChildren call against those now-\n // orphaned nodes would throw trying to remove an already-detached\n // child.\n if (oldTree.Children.Length > 0) {\n PatchChildren(realNode, oldTree.Children, []);\n }\n if (updated.TextContent != oldTree.TextContent) {\n realNode.textContent = updated.TextContent;\n }\n }\n\n if (updated.ClassName != oldTree.ClassName) {\n realNode.className = updated.ClassName;\n }\n if (updated.Id != oldTree.Id) {\n realNode.id = updated.Id;\n }\n // Always assigned, never conditionally on updated.Value != oldTree.Value\n // — unlike TextContent/ClassName/Id, an <input>/<select>'s live value\n // can diverge from the last-recorded VElement.Value purely through\n // user interaction (typing, picking an option) with no Update() ever\n // running in between (a real, deliberate pattern — see KopularDemo's\n // dogs_page.ks, which never Update()s on input/change). The recorded\n // oldTree.Value only reflects the tree as of the last actual render,\n // so comparing against it can't tell \"genuinely unchanged\" apart from\n // \"changed live in the DOM since then, framework never told\" — the\n // same reason a real \"controlled input\" (React's own term for this)\n // always writes value on every render rather than diffing it.\n realNode.value = updated.Value;\n\n // Same-length is the overwhelmingly common case (the same Render()\n // code path calls SetAttr the same number of times, in the same\n // order, on every call) — compare aligned by index and only touch\n // the real DOM for an entry that actually changed, rather than\n // reapplying every extra attribute on every patch regardless. A\n // length mismatch (the rarer case: a SetAttr call was added,\n // removed, or made conditional between renders) falls back to\n // reapplying everything, since index-aligned comparison isn't\n // meaningful once the two arrays don't correspond entry-for-entry.\n if (updated.ExtraNames.Length == oldTree.ExtraNames.Length) {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n if (updated.ExtraNames[i] != oldTree.ExtraNames[i] || updated.ExtraValues[i] != oldTree.ExtraValues[i]) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n } else {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n foreach (string oldName in oldTree.ExtraNames) {\n if (!updated.ExtraNames.Includes(oldName)) {\n realNode.removeAttribute(oldName);\n }\n }\n\n if (updated.Disabled != oldTree.Disabled) {\n realNode.disabled = updated.Disabled;\n }\n // Like Value above, a checkbox's live checked state changes on click\n // with no render in between, so a checked one is re-asserted every\n // patch rather than diffed against the previous tree.\n if (updated.Checked || oldTree.Checked) {\n realNode.checked = updated.Checked;\n }\n\n // Swap a listener only when the handler reference actually changed.\n // A node that never sets a real OnClick/OnInput/OnBlur/OnChange\n // keeps VElement's own shared NoOpEventHandler reference on both\n // sides (see velement.ks) — comparing by `!=` costs nothing and\n // skips two real DOM API calls per event per node for the (common)\n // case of \"this node has no handler of this kind either time,\"\n // which matters a lot on a list with hundreds/thousands of rows\n // most of which set at most one or two of the four. A node WITH a\n // real handler still gets a fresh closure every render (it\n // captures per-render values, like a loop's own item), so it still\n // swaps every time — correctly, since the old closure really is\n // stale.\n //\n // The actually-attached listener is always a Batching.Run wrapper\n // (see Materialize above), never `oldTree.OnClick`/`updated.OnClick`\n // themselves — removeEventListener only works when passed the exact\n // reference addEventListener received, so removal always goes\n // through `oldTree.AttachedOnClick` (the wrapper Materialize/a\n // previous Patch actually registered), and a real swap builds a\n // fresh wrapper the same way Materialize does. When nothing swaps,\n // `updated.AttachedOnClick` still needs to carry `oldTree`'s\n // forward — `updated` is a brand-new VElement whose own\n // AttachedOnClick starts back at the shared no-op (velement.ks's\n // constructor), and it becomes the retained \"old tree\" the very\n // next Patch() call diffs against.\n if (updated.OnClick != oldTree.OnClick) {\n realNode.removeEventListener(\"click\", oldTree.AttachedOnClick);\n (Event) => void wrappedClick = (Event e) => Batching.Run(() => updated.OnClick(e));\n updated.AttachedOnClick = wrappedClick;\n realNode.addEventListener(\"click\", wrappedClick);\n } else {\n updated.AttachedOnClick = oldTree.AttachedOnClick;\n }\n if (updated.OnInput != oldTree.OnInput) {\n realNode.removeEventListener(\"input\", oldTree.AttachedOnInput);\n (Event) => void wrappedInput = (Event e) => Batching.Run(() => updated.OnInput(e));\n updated.AttachedOnInput = wrappedInput;\n realNode.addEventListener(\"input\", wrappedInput);\n } else {\n updated.AttachedOnInput = oldTree.AttachedOnInput;\n }\n if (updated.OnBlur != oldTree.OnBlur) {\n realNode.removeEventListener(\"blur\", oldTree.AttachedOnBlur);\n (Event) => void wrappedBlur = (Event e) => Batching.Run(() => updated.OnBlur(e));\n updated.AttachedOnBlur = wrappedBlur;\n realNode.addEventListener(\"blur\", wrappedBlur);\n } else {\n updated.AttachedOnBlur = oldTree.AttachedOnBlur;\n }\n if (updated.OnChange != oldTree.OnChange) {\n realNode.removeEventListener(\"change\", oldTree.AttachedOnChange);\n (Event) => void wrappedChange = (Event e) => Batching.Run(() => updated.OnChange(e));\n updated.AttachedOnChange = wrappedChange;\n realNode.addEventListener(\"change\", wrappedChange);\n } else {\n updated.AttachedOnChange = oldTree.AttachedOnChange;\n }\n if (updated.OnSubmit != oldTree.OnSubmit) {\n realNode.removeEventListener(\"submit\", oldTree.AttachedOnSubmit);\n (Event) => void wrappedSubmit = SubmitListener(updated.OnSubmit);\n updated.AttachedOnSubmit = wrappedSubmit;\n realNode.addEventListener(\"submit\", wrappedSubmit);\n } else {\n updated.AttachedOnSubmit = oldTree.AttachedOnSubmit;\n }\n\n return realNode;\n }\n } else {\n // Shouldn't happen in practice (every previously-rendered tree has a\n // real node by the time a second render diffs against it) — treated\n // as \"nothing to reuse\" rather than a crash, same defensive spirit\n // as Component's own IsMounted guard.\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n } else {\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n}\n\n// Releases whatever was previously mounted in a slot (calling its\n// Teardown, which — for a Component — cascades into anything IT mounted\n// too, however many levels deep, before its own app-facing OnUnmount runs)\n// and removes its real node from the DOM, before a different Mountable (or\n// plain content) takes over that slot. A no-op when there was nothing\n// mounted here before — the common \"first render of this slot\" case.\nvoid UnmountPrevious(Element parent, VElement? old, Mountable? oldMounted) {\n // Teardown() only applies when there really was a Mounted instance here\n // before (nothing to tear down for plain content) — but the real node\n // itself needs removing whenever `old` had one, Mounted or not: a plain\n // VElement's slot turning into a Mounted one is exactly as much a\n // wholesale replacement as the reverse direction (handled by falling\n // through to Materialize below in Patch), and both need the OLD node\n // gone before the NEW one is appended, not left behind as a stray\n // sibling.\n if (oldMounted != null) {\n Mountable m = oldMounted;\n m.Teardown();\n }\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Keyed reconciliation: each VElement's own Id is its key when non-empty —\n// a real, existing DOM convention, needing no new API or syntax. A new\n// child whose Id matches an old child's Id is patched against that old\n// child (reusing its real node) regardless of position; a new child with\n// no Id, or an Id not present among the old children, falls back to\n// pairing positionally against whatever old children are still unconsumed,\n// in order. DOCUMENTED, REAL LIMITATION: without stable Ids, a reordered\n// list still produces the correct final output, but a given item's real\n// DOM node (and anything stateful attached to it, like focus) isn't\n// guaranteed to follow its data across the reorder — give list items a\n// stable Id for that guarantee.\nvoid PatchChildren(Element parent, VElement[] oldChildren, VElement[] newChildren) {\n // Map, not a Push loop — Push is deliberately non-mutating (a real\n // spread-copy every call, see KopScript's own README), so building an\n // n-length array by Push-ing once per element in a loop is an\n // accidental O(n^2) on every single PatchChildren call, however small\n // the actual diff. Map is a real, single O(n) pass straight to\n // Array.prototype.map.\n bool[] oldConsumed = oldChildren.Map((VElement c) => false);\n number[] matchedOldIndex = newChildren.Map((VElement c) => -1);\n\n // Pass 1: keyed matches, by Id.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (newChildren[i].Id.Length == 0) { continue; }\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (!oldConsumed[j] && oldChildren[j].Id == newChildren[i].Id) {\n matchedOldIndex[i] = j;\n oldConsumed[j] = true;\n break;\n }\n }\n }\n\n // Pass 2: positional fallback for everything Pass 1 didn't match —\n // pair each remaining new child against the next still-unconsumed old\n // child, in order.\n number nextOld = 0;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] >= 0) { continue; }\n while (nextOld < oldChildren.Length && oldConsumed[nextOld]) {\n nextOld = nextOld + 1;\n }\n if (nextOld < oldChildren.Length) {\n matchedOldIndex[i] = nextOld;\n oldConsumed[nextOld] = true;\n nextOld = nextOld + 1;\n }\n }\n\n // Whether anything is actually moving at all — same length, and every\n // new position matched the *same* old position. The extremely common\n // case for a list that's only had some of its rows' own content change\n // (e.g. \"update every 10th row\"), where reconciliation still has real\n // work to do (see Pass 1/2 above and Patch() itself) but nothing needs\n // to physically move in the DOM at all.\n bool needsReorder = oldChildren.Length != newChildren.Length;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] != i) {\n needsReorder = true;\n break;\n }\n }\n\n // Pass 3: patch/create each new child in order, then — only if the list\n // actually needs reordering — move it into its correct final position.\n // appendChild on a node already attached elsewhere in the DOM MOVES it\n // (real DOM semantics), so processing new children in their final\n // desired order and always appending naturally builds up the correct\n // sequence, no separate insertBefore/reference-node bookkeeping needed.\n // Safe because VElement.Children is always the COMPLETE list of a\n // node's children — nothing else ever shares `parent`. Skipping the\n // move entirely when `needsReorder` is false avoids a real DOM API call\n // per child for the common no-reorder case — Patch() itself already\n // updates or replaces a reused/changed node exactly in place either way.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n VElement? matchedOld = null;\n if (matchedOldIndex[i] >= 0) {\n matchedOld = oldChildren[matchedOldIndex[i]];\n }\n Element childNode = Patch(parent, matchedOld, newChildren[i]);\n if (needsReorder) {\n parent.appendChild(childNode);\n }\n }\n\n // Pass 4: remove whatever old children never got reused — releasing\n // anything Mounted first (see velement.ks) so a child genuinely dropped\n // from a list (not replaced in the same slot by a different one, which\n // Patch() itself already handles via UnmountPrevious — removed outright)\n // still gets torn down. This is the one place THAT case is reachable\n // from, since Patch() only ever sees one slot at a time, never a slot\n // disappearing entirely.\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (oldConsumed[j]) { continue; }\n Mountable? maybeOldMounted = oldChildren[j].Mounted;\n if (maybeOldMounted != null) {\n Mountable m = maybeOldMounted;\n m.Teardown();\n }\n Element? maybeOldNode = oldChildren[j].RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Recursively finds and tears down every Mounted node within `tree` (tree\n// itself included) — used by Component's own Teardown (see component.ks)\n// so removing ONE component transitively releases everything IT rendered,\n// however many Mounted levels deep, not only the outermost one. A pure\n// data walk over the retained VElement tree; never touches the real DOM\n// itself (the caller already owns removing whatever real node is actually\n// attached).\nvoid UnmountTree(VElement tree) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n m.Teardown();\n }\n foreach (VElement child in tree.Children) {\n UnmountTree(child);\n }\n}\n"],"names":[],"mappings":";;;AAyCA;EACiB;;EACA;;EAED;IACZ;;;EAGY;IACZ;MACmB;;;;EAUP;IACG;IACf;MACQ;;MAES;MACf;QACE;QACiB;QACjB;UACe;;;;;;AAmBvB;EACiB;;EAED;IACZ;MACwB;MACtB;MACkB;MACA;MACO;;;;AAO/B;EACE;EACkB;EACJ;;;AAehB;EACE;EACA;IACE;IACA;IACc;IACd;;EAGF;IACE;IACc;IACd;;EAGF;EAEA;IACe;;IAEb;MACgB;;;IAGD;;EAGJ;EACP;EACG;EAET;IACiB;;EAEjB;IACc;;EAEd;IACa;;EAYb;IACE;IACqB;IACF;;EAErB;IACE;IACqB;IACF;;EAErB;IACE;IACoB;IACD;;EAErB;IACE;IACsB;IACH;;EAErB;IACE;IACsB;IACH;;EAGP;EACd;;AAiBF;EACE;EACA;EACA;IACE;IACW;;EASb;IACE;IACA;MACE;MACA;QAIE;QACiB;QACjB;;;IAKW;IACf;IACkB;IACD;IACjB;;EAGF;IAGiB;IACf;IACkB;IAClB;;EAGF;IACE;IACA;IACA;MACE;MACA;QACE;QACmB;QACnB;;QAEiB;QACjB;UACuB;;QAEvB;;QAEiB;QAEjB;UACE;YACqB;;;UASrB;YACuB;;UAEV;;UAUb;YACe;;UAEf;YACuB;;;QAIzB;UACqB;;QAErB;UACc;;QAaC;QAWf;UACE;YACE;cACuB;;;;UAIzB;YACuB;;;QAGzB;UACE;YAC0B;;;QAI5B;UACoB;;QAKpB;UACmB;;QA4BnB;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACuB;UACE;;UAEF;;QAEzB;UAC8B;UAC5B;UACyB;UACA;;UAEA;;QAE3B;UAC8B;UAC5B;UACyB;UACA;;UAEA;;QAG3B;;;MAOF;MACkB;MAClB;;;IAGF;IACkB;IAClB;;;AAUJ;EASE;IACE;IACU;;EAEZ;IACE;IACA;IACA;MACE;MACkB;;;;AAgBxB;EAOE;EACA;EAGA;IACE;MAAqC;;IACrC;MACE;QACqB;QACJ;QACf;;;;EAQN;EACA;IACE;MAA+B;;IAC/B;MACU;;IAEV;MACqB;MACE;MACb;;;EAUZ;EACA;IACE;MACe;MACb;;;EAeJ;IACE;IACA;MACa;;IAEb;IACA;MACoB;;;EAWtB;IACE;MAAsB;;IACtB;IACA;MACE;MACU;;IAEZ;IACA;MACE;MACkB;;;;AAYxB;EACE;EACA;IACE;IACU;;EAEZ;IACa"}
|
package/src/vdom.ks
CHANGED
|
@@ -102,6 +102,15 @@ class ScopedStyles {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// A submit listener: preventDefault() first (no full-page navigation, ever),
|
|
106
|
+
// then the handler, batched like every other event (see Materialize).
|
|
107
|
+
(Event) => void SubmitListener((Event) => void handler) {
|
|
108
|
+
return (Event e) => {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
Batching.Run(() => handler(e));
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
// Builds a brand-new, fully real DOM subtree from a VElement tree with no
|
|
106
115
|
// diffing at all — first mount, or whenever Patch() decides a subtree must
|
|
107
116
|
// be replaced outright (no previous node to reuse, or the tag changed).
|
|
@@ -122,6 +131,12 @@ Element Materialize(VElement tree, Element parent) {
|
|
|
122
131
|
return mountedRoot;
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
if (tree.IsText()) {
|
|
135
|
+
Element text = document.createTextNode(tree.TextContent);
|
|
136
|
+
tree.RealNode = text;
|
|
137
|
+
return text;
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
Element el = document.createElement(tree.Tag);
|
|
126
141
|
|
|
127
142
|
if (tree.RawHtml.Length > 0) {
|
|
@@ -177,6 +192,11 @@ Element Materialize(VElement tree, Element parent) {
|
|
|
177
192
|
tree.AttachedOnChange = wrapped;
|
|
178
193
|
el.addEventListener("change", wrapped);
|
|
179
194
|
}
|
|
195
|
+
if (tree.OnSubmit != NoOpEventHandler) {
|
|
196
|
+
(Event) => void wrapped = SubmitListener(tree.OnSubmit);
|
|
197
|
+
tree.AttachedOnSubmit = wrapped;
|
|
198
|
+
el.addEventListener("submit", wrapped);
|
|
199
|
+
}
|
|
180
200
|
|
|
181
201
|
tree.RealNode = el;
|
|
182
202
|
return el;
|
|
@@ -250,6 +270,12 @@ Element Patch(Element parent, VElement? old, VElement updated) {
|
|
|
250
270
|
Element created = Materialize(updated, parent);
|
|
251
271
|
parent.replaceChild(created, realNode);
|
|
252
272
|
return created;
|
|
273
|
+
} else if (updated.IsText()) {
|
|
274
|
+
updated.RealNode = realNode;
|
|
275
|
+
if (updated.TextContent != oldTree.TextContent) {
|
|
276
|
+
realNode.textContent = updated.TextContent;
|
|
277
|
+
}
|
|
278
|
+
return realNode;
|
|
253
279
|
} else {
|
|
254
280
|
updated.RealNode = realNode;
|
|
255
281
|
|
|
@@ -397,6 +423,14 @@ Element Patch(Element parent, VElement? old, VElement updated) {
|
|
|
397
423
|
} else {
|
|
398
424
|
updated.AttachedOnChange = oldTree.AttachedOnChange;
|
|
399
425
|
}
|
|
426
|
+
if (updated.OnSubmit != oldTree.OnSubmit) {
|
|
427
|
+
realNode.removeEventListener("submit", oldTree.AttachedOnSubmit);
|
|
428
|
+
(Event) => void wrappedSubmit = SubmitListener(updated.OnSubmit);
|
|
429
|
+
updated.AttachedOnSubmit = wrappedSubmit;
|
|
430
|
+
realNode.addEventListener("submit", wrappedSubmit);
|
|
431
|
+
} else {
|
|
432
|
+
updated.AttachedOnSubmit = oldTree.AttachedOnSubmit;
|
|
433
|
+
}
|
|
400
434
|
|
|
401
435
|
return realNode;
|
|
402
436
|
}
|
package/src/velement.js
CHANGED
|
@@ -17,10 +17,12 @@ export class VElement {
|
|
|
17
17
|
this.OnInput = NoOpEventHandler;
|
|
18
18
|
this.OnBlur = NoOpEventHandler;
|
|
19
19
|
this.OnChange = NoOpEventHandler;
|
|
20
|
+
this.OnSubmit = NoOpEventHandler;
|
|
20
21
|
this.AttachedOnClick = NoOpEventHandler;
|
|
21
22
|
this.AttachedOnInput = NoOpEventHandler;
|
|
22
23
|
this.AttachedOnBlur = NoOpEventHandler;
|
|
23
24
|
this.AttachedOnChange = NoOpEventHandler;
|
|
25
|
+
this.AttachedOnSubmit = NoOpEventHandler;
|
|
24
26
|
this.ExtraNames = [];
|
|
25
27
|
this.ExtraValues = [];
|
|
26
28
|
this.RealNode = null;
|
|
@@ -37,6 +39,16 @@ export class VElement {
|
|
|
37
39
|
return ve;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
static Text(content) {
|
|
43
|
+
let ve = new VElement("#text");
|
|
44
|
+
ve.TextContent = content;
|
|
45
|
+
return ve;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
IsText() {
|
|
49
|
+
return (this.Tag === "#text");
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
AppendChild(child) {
|
|
41
53
|
this.Children = [...this.Children, child];
|
|
42
54
|
}
|
package/src/velement.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"velement.js","sources":["velement.ks"],"sourcesContent":["using \"./dom\";\n\n// A lightweight, framework-owned description of one DOM element — Render()\n// builds a tree of these instead of real DOM nodes, so Update() can DIFF\n// the new tree against the previous one and patch only what changed,\n// instead of discarding and rebuilding the whole real DOM subtree every\n// time (see src/vdom.ks for the diff/patch engine, src/component.ks for\n// where Render()'s return type changed from Element to VElement).\n//\n// A single shared no-op, not a fresh closure per VElement — every instance\n// that never sets a real handler gets this exact same function reference,\n// so Patch() (vdom.ks) can tell \"no handler either time\" apart from \"a\n// handler changed\" with a plain `!=` reference check instead of always\n// removing/re-adding all four DOM listeners on every patch regardless of\n// whether anything about them actually changed. A fresh `(Event e) => {}`\n// closure per instance would defeat this — two \"empty\" handlers would\n// never compare equal, even when nothing meaningful differs.\nvoid NoOpEventHandler(Event e) {\n}\n\n// A live child Component (or anything else that wants to be embedded as a\n// slot in another's VElement tree) — see VElement.Mounted below. Declared\n// HERE, not in vdom.ks, even though vdom.ks's Materialize/Patch are the\n// only real callers: VElement's own Mounted field needs to reference this\n// type, and vdom.ks already `using`s this file for VElement itself —\n// declaring it in vdom.ks would make velement.ks need to `using \"./vdom\"`\n// right back, a circular `using` KopScript rejects outright (the same\n// constraint that put the unrelated Flushable interface in vdom.ks\n// instead of component.ks — there, Component was the one doing the\n// referencing; here, VElement is). Component (component.ks) is the one\n// real implementer, via `class Component : Flushable, Mountable`.\ninterface Mountable {\n // Called by the vdom engine only (public purely for interface\n // conformance, same convention as Flushable.FlushUpdate — not the\n // intended way for app code to trigger anything). Builds this\n // component's own tree for the first time and returns its real root\n // node WITHOUT inserting it anywhere; the caller (Materialize/Patch)\n // does that itself, the same as every other VElement content mode.\n Element MountAsChild(Element parent);\n\n // Re-renders this already-mounted child in place; returns the (possibly\n // identical) real root node. Called when a slot's Mounted reference is\n // the SAME instance as last render.\n Element PatchAsChild();\n\n // Called once when this slot's Mounted reference disappears or is\n // replaced by a different instance across a re-render, before its real\n // node is removed — the one hook the vdom engine itself calls; see\n // Component's own OnUnmount for the real, overridable app-facing\n // extension point this delegates to.\n void Teardown();\n}\n\n// Fixed, named fields — not a generic prop bag — because KopScript has no\n// object-literal syntax to build one with. Fixed, named event slots — not\n// an array of handlers — because KopScript has no array-of-function-values\n// type either. Both are real language constraints, not an oversight; see\n// SetAttr below for the escape hatch covering everything not common enough\n// to deserve its own named field.\nclass VElement {\n public string Tag;\n // Mutually exclusive with Children and RawHtml — set at most one of the\n // three. TextContent/Children mirrors the same \"no mixed text/element\n // content\" rule KopScript's own templates already enforce.\n public string TextContent;\n public string ClassName;\n public string Id;\n // The one property patched via direct assignment, never setAttribute —\n // see Element.value's own comment in dom.ks for why (the \"default value\n // attribute\" vs \"current live value property\" DOM footgun — the exact\n // property behind the original typing bug this whole effort traces to).\n public string Value;\n public VElement[] Children;\n // An opaque, undiffed leaf — set instead of TextContent/Children for the\n // existing raw-HTML-then-wire-handlers pattern (header.ks/nav.ks). The\n // patch engine treats two VElements with different RawHtml as a single\n // innerHTML assignment, never recursing inside it — the same \"opaque\n // blob\" treatment `raw string` already gets everywhere else.\n public string RawHtml;\n // Real DOM properties, not attributes: a boolean attribute is on whenever\n // it's present at all, so SetAttr can never turn `disabled` back off.\n public bool Disabled;\n public bool Checked;\n\n // Each defaults to a real no-op, never null — KopScript has no nullable\n // *function* type to fall back on for \"no handler set\" (see Router.Guard\n // for the same default-real-function pattern already established).\n public (Event) => void OnClick;\n public (Event) => void OnInput;\n public (Event) => void OnBlur;\n public (Event) => void OnChange;\n\n // The REAL function reference the patch engine (vdom.ks) actually passed\n // to addEventListener for this exact real DOM node — never OnClick/\n // OnInput/OnBlur/OnChange themselves. Materialize/Patch wrap each handler\n // in Component.RunInBatch (see component.ks) before attaching it, so the\n // listener genuinely registered isn't the same function value as the one\n // an app author wrote; removeEventListener only ever works when passed\n // the exact reference addEventListener received, so the patch engine\n // needs somewhere to remember it for the swap-when-changed path. Plain\n // data, same as every other field here — only vdom.ks ever reads or\n // writes these.\n public (Event) => void AttachedOnClick;\n public (Event) => void AttachedOnInput;\n public (Event) => void AttachedOnBlur;\n public (Event) => void AttachedOnChange;\n\n // A real HTML attribute not common enough for its own named field (href,\n // src, alt, placeholder, ...) — parallel arrays, since KopScript has no\n // Dictionary type. Never Value (see its own field comment above). Public,\n // read directly by the patch engine (src/vdom.ks) rather than through\n // accessor methods — plain data, same style as this codebase's other\n // plain classes (Note, Dog, ...).\n public string[] ExtraNames;\n public string[] ExtraValues;\n\n // Set only once this VElement has been materialized into (or reused as)\n // a real DOM node — null on a freshly-built tree from a not-yet-patched\n // Render() call. The patch engine reads the *previous* render's tree's\n // RealNode to know what to reuse/patch; it never reads the live DOM back\n // to rediscover this (see vdom.ks's own header comment for why).\n public Element? RealNode;\n\n // Set to embed a live, mounted Component (or any other Mountable) as\n // this VElement's entire content — mutually exclusive with\n // Tag/TextContent/Children/RawHtml, and stronger than RawHtml's own\n // \"opaque leaf\" treatment: Tag is unused, since no wrapping element of\n // Kopular's own is created for this slot at all — the child's own\n // rendered root IS this slot's real node (see vdom.ks's Materialize/\n // Patch). null on every ordinary VElement, the only value every\n // VElement had before this existed, so nothing about an existing\n // content mode changes unless a tree opts into this one. Use\n // VElement.Mount(component) below rather than setting this directly.\n public Mountable? Mounted;\n\n constructor(string tag) {\n this.Tag = tag;\n this.TextContent = \"\";\n this.ClassName = \"\";\n this.Id = \"\";\n this.Value = \"\";\n this.Children = [];\n this.RawHtml = \"\";\n this.Disabled = false;\n this.Checked = false;\n this.OnClick = NoOpEventHandler;\n this.OnInput = NoOpEventHandler;\n this.OnBlur = NoOpEventHandler;\n this.OnChange = NoOpEventHandler;\n this.AttachedOnClick = NoOpEventHandler;\n this.AttachedOnInput = NoOpEventHandler;\n this.AttachedOnBlur = NoOpEventHandler;\n this.AttachedOnChange = NoOpEventHandler;\n this.ExtraNames = [];\n this.ExtraValues = [];\n this.RealNode = null;\n this.Mounted = null;\n }\n\n public static VElement Create(string tag) {\n return new VElement(tag);\n }\n\n // Wraps a live child Component (or anything else implementing Mountable)\n // as a VElement slot the diff engine can create/patch/move/destroy\n // declaratively. Set .Id on the result afterward for a list of these to\n // reorder correctly, the same as any other keyed child — PatchChildren\n // (vdom.ks) needs no changes to support this; it already keys by Id\n // regardless of what a VElement's content actually is.\n public static VElement Mount(Mountable component) {\n VElement ve = new VElement(\"\");\n ve.Mounted = component;\n return ve;\n }\n\n public void AppendChild(VElement child) {\n this.Children = this.Children.Push(child);\n }\n\n // Last call for a given name wins if SetAttr is called more than once\n // with the same name on one VElement — the patch engine applies\n // ExtraNames/ExtraValues in order, so a later entry's setAttribute call\n // simply overwrites an earlier one for the same name, no special\n // dedup/replace logic needed here.\n public void SetAttr(string name, string value) {\n this.ExtraNames = this.ExtraNames.Push(name);\n this.ExtraValues = this.ExtraValues.Push(value);\n }\n}\n"],"names":[],"mappings":";;AAiBA;;AA0CA;EA4EE;IACW;IACQ;IACF;IACP;IACG;IACG;IACD;IACC;IACD;IACA;IACA;IACD;IACE;IACO;IACA;IACD;IACE;IACN;IACC;IACH;IACD;;;EAGD;IACZ;;;EASY;IACZ;IACW;IACX;;;EAGK;IACS;;;EAQT;IACW;IACC"}
|
|
1
|
+
{"version":3,"file":"velement.js","sources":["velement.ks"],"sourcesContent":["using \"./dom\";\n\n// A lightweight, framework-owned description of one DOM element — Render()\n// builds a tree of these instead of real DOM nodes, so Update() can DIFF\n// the new tree against the previous one and patch only what changed,\n// instead of discarding and rebuilding the whole real DOM subtree every\n// time (see src/vdom.ks for the diff/patch engine, src/component.ks for\n// where Render()'s return type changed from Element to VElement).\n//\n// A single shared no-op, not a fresh closure per VElement — every instance\n// that never sets a real handler gets this exact same function reference,\n// so Patch() (vdom.ks) can tell \"no handler either time\" apart from \"a\n// handler changed\" with a plain `!=` reference check instead of always\n// removing/re-adding all four DOM listeners on every patch regardless of\n// whether anything about them actually changed. A fresh `(Event e) => {}`\n// closure per instance would defeat this — two \"empty\" handlers would\n// never compare equal, even when nothing meaningful differs.\nvoid NoOpEventHandler(Event e) {\n}\n\n// A live child Component (or anything else that wants to be embedded as a\n// slot in another's VElement tree) — see VElement.Mounted below. Declared\n// HERE, not in vdom.ks, even though vdom.ks's Materialize/Patch are the\n// only real callers: VElement's own Mounted field needs to reference this\n// type, and vdom.ks already `using`s this file for VElement itself —\n// declaring it in vdom.ks would make velement.ks need to `using \"./vdom\"`\n// right back, a circular `using` KopScript rejects outright (the same\n// constraint that put the unrelated Flushable interface in vdom.ks\n// instead of component.ks — there, Component was the one doing the\n// referencing; here, VElement is). Component (component.ks) is the one\n// real implementer, via `class Component : Flushable, Mountable`.\ninterface Mountable {\n // Called by the vdom engine only (public purely for interface\n // conformance, same convention as Flushable.FlushUpdate — not the\n // intended way for app code to trigger anything). Builds this\n // component's own tree for the first time and returns its real root\n // node WITHOUT inserting it anywhere; the caller (Materialize/Patch)\n // does that itself, the same as every other VElement content mode.\n Element MountAsChild(Element parent);\n\n // Re-renders this already-mounted child in place; returns the (possibly\n // identical) real root node. Called when a slot's Mounted reference is\n // the SAME instance as last render.\n Element PatchAsChild();\n\n // Called once when this slot's Mounted reference disappears or is\n // replaced by a different instance across a re-render, before its real\n // node is removed — the one hook the vdom engine itself calls; see\n // Component's own OnUnmount for the real, overridable app-facing\n // extension point this delegates to.\n void Teardown();\n}\n\n// Fixed, named fields — not a generic prop bag — because KopScript has no\n// object-literal syntax to build one with. Fixed, named event slots — not\n// an array of handlers — because KopScript has no array-of-function-values\n// type either. Both are real language constraints, not an oversight; see\n// SetAttr below for the escape hatch covering everything not common enough\n// to deserve its own named field.\nclass VElement {\n public string Tag;\n // Mutually exclusive with Children and RawHtml — set at most one of the\n // three. TextContent/Children mirrors the same \"no mixed text/element\n // content\" rule KopScript's own templates already enforce.\n public string TextContent;\n public string ClassName;\n public string Id;\n // The one property patched via direct assignment, never setAttribute —\n // see Element.value's own comment in dom.ks for why (the \"default value\n // attribute\" vs \"current live value property\" DOM footgun — the exact\n // property behind the original typing bug this whole effort traces to).\n public string Value;\n public VElement[] Children;\n // An opaque, undiffed leaf — set instead of TextContent/Children for the\n // existing raw-HTML-then-wire-handlers pattern (header.ks/nav.ks). The\n // patch engine treats two VElements with different RawHtml as a single\n // innerHTML assignment, never recursing inside it — the same \"opaque\n // blob\" treatment `raw string` already gets everywhere else.\n public string RawHtml;\n // Real DOM properties, not attributes: a boolean attribute is on whenever\n // it's present at all, so SetAttr can never turn `disabled` back off.\n public bool Disabled;\n public bool Checked;\n\n // Each defaults to a real no-op, never null — KopScript has no nullable\n // *function* type to fall back on for \"no handler set\" (see Router.Guard\n // for the same default-real-function pattern already established).\n public (Event) => void OnClick;\n public (Event) => void OnInput;\n public (Event) => void OnBlur;\n public (Event) => void OnChange;\n // Always calls preventDefault() first (see vdom.ks), like Angular's\n // ngSubmit: a single-page app's form never wants the browser's full-page\n // navigation. Enter in a form's text input fires it too.\n public (Event) => void OnSubmit;\n\n // The REAL function reference the patch engine (vdom.ks) actually passed\n // to addEventListener for this exact real DOM node — never OnClick/\n // OnInput/OnBlur/OnChange themselves. Materialize/Patch wrap each handler\n // in Component.RunInBatch (see component.ks) before attaching it, so the\n // listener genuinely registered isn't the same function value as the one\n // an app author wrote; removeEventListener only ever works when passed\n // the exact reference addEventListener received, so the patch engine\n // needs somewhere to remember it for the swap-when-changed path. Plain\n // data, same as every other field here — only vdom.ks ever reads or\n // writes these.\n public (Event) => void AttachedOnClick;\n public (Event) => void AttachedOnInput;\n public (Event) => void AttachedOnBlur;\n public (Event) => void AttachedOnChange;\n public (Event) => void AttachedOnSubmit;\n\n // A real HTML attribute not common enough for its own named field (href,\n // src, alt, placeholder, ...) — parallel arrays, since KopScript has no\n // Dictionary type. Never Value (see its own field comment above). Public,\n // read directly by the patch engine (src/vdom.ks) rather than through\n // accessor methods — plain data, same style as this codebase's other\n // plain classes (Note, Dog, ...).\n public string[] ExtraNames;\n public string[] ExtraValues;\n\n // Set only once this VElement has been materialized into (or reused as)\n // a real DOM node — null on a freshly-built tree from a not-yet-patched\n // Render() call. The patch engine reads the *previous* render's tree's\n // RealNode to know what to reuse/patch; it never reads the live DOM back\n // to rediscover this (see vdom.ks's own header comment for why).\n public Element? RealNode;\n\n // Set to embed a live, mounted Component (or any other Mountable) as\n // this VElement's entire content — mutually exclusive with\n // Tag/TextContent/Children/RawHtml, and stronger than RawHtml's own\n // \"opaque leaf\" treatment: Tag is unused, since no wrapping element of\n // Kopular's own is created for this slot at all — the child's own\n // rendered root IS this slot's real node (see vdom.ks's Materialize/\n // Patch). null on every ordinary VElement, the only value every\n // VElement had before this existed, so nothing about an existing\n // content mode changes unless a tree opts into this one. Use\n // VElement.Mount(component) below rather than setting this directly.\n public Mountable? Mounted;\n\n constructor(string tag) {\n this.Tag = tag;\n this.TextContent = \"\";\n this.ClassName = \"\";\n this.Id = \"\";\n this.Value = \"\";\n this.Children = [];\n this.RawHtml = \"\";\n this.Disabled = false;\n this.Checked = false;\n this.OnClick = NoOpEventHandler;\n this.OnInput = NoOpEventHandler;\n this.OnBlur = NoOpEventHandler;\n this.OnChange = NoOpEventHandler;\n this.OnSubmit = NoOpEventHandler;\n this.AttachedOnClick = NoOpEventHandler;\n this.AttachedOnInput = NoOpEventHandler;\n this.AttachedOnBlur = NoOpEventHandler;\n this.AttachedOnChange = NoOpEventHandler;\n this.AttachedOnSubmit = NoOpEventHandler;\n this.ExtraNames = [];\n this.ExtraValues = [];\n this.RealNode = null;\n this.Mounted = null;\n }\n\n public static VElement Create(string tag) {\n return new VElement(tag);\n }\n\n // Wraps a live child Component (or anything else implementing Mountable)\n // as a VElement slot the diff engine can create/patch/move/destroy\n // declaratively. Set .Id on the result afterward for a list of these to\n // reorder correctly, the same as any other keyed child — PatchChildren\n // (vdom.ks) needs no changes to support this; it already keys by Id\n // regardless of what a VElement's content actually is.\n public static VElement Mount(Mountable component) {\n VElement ve = new VElement(\"\");\n ve.Mounted = component;\n return ve;\n }\n\n // A bare text node, for text sitting next to elements inside one parent\n // (`<label>Email <input></label>`) — a template emits one per text run.\n // Everything but TextContent is ignored on it.\n public static VElement Text(string content) {\n // Not a real HTML tag name (those can't start with '#'), so a text leaf\n // never collides with an element; matches the DOM's own Text.nodeName.\n VElement ve = new VElement(\"#text\");\n ve.TextContent = content;\n return ve;\n }\n\n public bool IsText() {\n return this.Tag == \"#text\";\n }\n\n public void AppendChild(VElement child) {\n this.Children = this.Children.Push(child);\n }\n\n // Last call for a given name wins if SetAttr is called more than once\n // with the same name on one VElement — the patch engine applies\n // ExtraNames/ExtraValues in order, so a later entry's setAttribute call\n // simply overwrites an earlier one for the same name, no special\n // dedup/replace logic needed here.\n public void SetAttr(string name, string value) {\n this.ExtraNames = this.ExtraNames.Push(name);\n this.ExtraValues = this.ExtraValues.Push(value);\n }\n}\n"],"names":[],"mappings":";;AAiBA;;AA0CA;EAiFE;IACW;IACQ;IACF;IACP;IACG;IACG;IACD;IACC;IACD;IACA;IACA;IACD;IACE;IACA;IACO;IACA;IACD;IACE;IACA;IACN;IACC;IACH;IACD;;;EAGD;IACZ;;;EASY;IACZ;IACW;IACX;;;EAMY;IAGZ;IACe;IACf;;;EAGK;IACL;;;EAGK;IACS;;;EAQT;IACW;IACC"}
|
package/src/velement.ks
CHANGED
|
@@ -89,6 +89,10 @@ class VElement {
|
|
|
89
89
|
public (Event) => void OnInput;
|
|
90
90
|
public (Event) => void OnBlur;
|
|
91
91
|
public (Event) => void OnChange;
|
|
92
|
+
// Always calls preventDefault() first (see vdom.ks), like Angular's
|
|
93
|
+
// ngSubmit: a single-page app's form never wants the browser's full-page
|
|
94
|
+
// navigation. Enter in a form's text input fires it too.
|
|
95
|
+
public (Event) => void OnSubmit;
|
|
92
96
|
|
|
93
97
|
// The REAL function reference the patch engine (vdom.ks) actually passed
|
|
94
98
|
// to addEventListener for this exact real DOM node — never OnClick/
|
|
@@ -104,6 +108,7 @@ class VElement {
|
|
|
104
108
|
public (Event) => void AttachedOnInput;
|
|
105
109
|
public (Event) => void AttachedOnBlur;
|
|
106
110
|
public (Event) => void AttachedOnChange;
|
|
111
|
+
public (Event) => void AttachedOnSubmit;
|
|
107
112
|
|
|
108
113
|
// A real HTML attribute not common enough for its own named field (href,
|
|
109
114
|
// src, alt, placeholder, ...) — parallel arrays, since KopScript has no
|
|
@@ -147,10 +152,12 @@ class VElement {
|
|
|
147
152
|
this.OnInput = NoOpEventHandler;
|
|
148
153
|
this.OnBlur = NoOpEventHandler;
|
|
149
154
|
this.OnChange = NoOpEventHandler;
|
|
155
|
+
this.OnSubmit = NoOpEventHandler;
|
|
150
156
|
this.AttachedOnClick = NoOpEventHandler;
|
|
151
157
|
this.AttachedOnInput = NoOpEventHandler;
|
|
152
158
|
this.AttachedOnBlur = NoOpEventHandler;
|
|
153
159
|
this.AttachedOnChange = NoOpEventHandler;
|
|
160
|
+
this.AttachedOnSubmit = NoOpEventHandler;
|
|
154
161
|
this.ExtraNames = [];
|
|
155
162
|
this.ExtraValues = [];
|
|
156
163
|
this.RealNode = null;
|
|
@@ -173,6 +180,21 @@ class VElement {
|
|
|
173
180
|
return ve;
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
// A bare text node, for text sitting next to elements inside one parent
|
|
184
|
+
// (`<label>Email <input></label>`) — a template emits one per text run.
|
|
185
|
+
// Everything but TextContent is ignored on it.
|
|
186
|
+
public static VElement Text(string content) {
|
|
187
|
+
// Not a real HTML tag name (those can't start with '#'), so a text leaf
|
|
188
|
+
// never collides with an element; matches the DOM's own Text.nodeName.
|
|
189
|
+
VElement ve = new VElement("#text");
|
|
190
|
+
ve.TextContent = content;
|
|
191
|
+
return ve;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public bool IsText() {
|
|
195
|
+
return this.Tag == "#text";
|
|
196
|
+
}
|
|
197
|
+
|
|
176
198
|
public void AppendChild(VElement child) {
|
|
177
199
|
this.Children = this.Children.Push(child);
|
|
178
200
|
}
|