kopular 0.13.0 → 0.15.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/LLM.md +144 -74
- package/README.md +98 -68
- package/package.json +2 -1
- package/src/component.js +14 -6
- package/src/component.js.map +1 -1
- package/src/component.ks +37 -15
- package/src/directives.js +1 -0
- package/src/directives.js.map +1 -1
- package/src/directives.ks +5 -4
- package/src/dom.js.map +1 -1
- package/src/dom.ks +18 -1
- package/src/forms.js.map +1 -1
- package/src/forms.ks +1 -1
- package/src/router.js +9 -4
- package/src/router.js.map +1 -1
- package/src/router.ks +23 -10
- package/src/vdom.js +163 -0
- package/src/vdom.js.map +1 -0
- package/src/vdom.ks +277 -0
- package/src/velement.js +37 -0
- package/src/velement.js.map +1 -0
- package/src/velement.ks +106 -0
package/LLM.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# Kopular — LLM reference
|
|
2
2
|
|
|
3
3
|
Complete reference for generating correct Kopular code. This is a spec, not a tutorial —
|
|
4
|
-
see `README.md` for narrative/rationale. Kopular is
|
|
5
|
-
them. For the host language, see KopScript's own `LLM.md` in the `
|
|
4
|
+
see `README.md` for narrative/rationale. Kopular is 10 files total; this covers all of
|
|
5
|
+
them. For the host language, see KopScript's own `LLM.md` in the `KopScript` repo (or its
|
|
6
6
|
published `LLM.md` on the `kopscript` npm package) — that reference is a prerequisite,
|
|
7
7
|
not repeated here.
|
|
8
8
|
|
|
9
9
|
Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
|
|
10
|
+
`kopular/velement` (VElement — what `Render()` returns; see "Component" below),
|
|
10
11
|
`kopular/router` (Router), `kopular/dom` (ambient DOM bindings), `kopular/directives`
|
|
11
12
|
(If), `kopular/http` (Http), `kopular/forms` (FormField, Validators), `kopular/testing`
|
|
12
|
-
(runKopularApp, runKopularFixture — see below).
|
|
13
|
+
(runKopularApp, runKopularFixture — see below). `kopular/vdom` (the diff/patch engine
|
|
14
|
+
behind `Update()`) is internal — nothing outside `component.ks` needs to import it
|
|
15
|
+
directly. Also ships a bin, `kp` — `npx kp new
|
|
13
16
|
<dir>` scaffolds a new project (the `extern` bindings below, plus the vendor/serve
|
|
14
17
|
scripts needed to run in a browser) rather than requiring it be reconstructed by hand;
|
|
15
18
|
prefer it over hand-writing the section below for a new project. Templates (`template
|
|
@@ -22,9 +25,25 @@ there is no `kopular/template` entry point to import.
|
|
|
22
25
|
included) always goes through `extern`, re-describing exactly the members you use:
|
|
23
26
|
|
|
24
27
|
```ks
|
|
28
|
+
extern class VElement {
|
|
29
|
+
static VElement Create(string tag);
|
|
30
|
+
string TextContent { get; set; }
|
|
31
|
+
string ClassName { get; set; }
|
|
32
|
+
string Id { get; set; }
|
|
33
|
+
string Value { get; set; }
|
|
34
|
+
string RawHtml { get; set; }
|
|
35
|
+
(Event) => void OnClick { get; set; }
|
|
36
|
+
(Event) => void OnInput { get; set; }
|
|
37
|
+
(Event) => void OnBlur { get; set; }
|
|
38
|
+
(Event) => void OnChange { get; set; }
|
|
39
|
+
void AppendChild(VElement child);
|
|
40
|
+
void SetAttr(string name, string value);
|
|
41
|
+
} from "kopular/velement";
|
|
42
|
+
|
|
25
43
|
extern class Component {
|
|
26
44
|
constructor();
|
|
27
|
-
virtual
|
|
45
|
+
virtual VElement Render(); // `virtual` here is what lets your subclass `override` it
|
|
46
|
+
virtual void AfterRender(Element root); // see "Component" below
|
|
28
47
|
void Mount(Element parent);
|
|
29
48
|
void Update();
|
|
30
49
|
} from "kopular/component";
|
|
@@ -36,7 +55,7 @@ extern class Router {
|
|
|
36
55
|
void Mount(Element parent);
|
|
37
56
|
} from "kopular/router";
|
|
38
57
|
|
|
39
|
-
extern
|
|
58
|
+
extern VElement If(bool condition, () => VElement whenTrue, () => VElement whenFalse) from "kopular/directives";
|
|
40
59
|
|
|
41
60
|
extern class Response {
|
|
42
61
|
bool ok { get; }
|
|
@@ -86,7 +105,7 @@ Kopular's own copy in `dom.ks` isn't reachable across the package boundary; rede
|
|
|
86
105
|
handful of members you actually use. See KopularDemo's `src/kopular_bindings.ks` for a
|
|
87
106
|
complete, real example of both.
|
|
88
107
|
|
|
89
|
-
## `Component` (`component.ks`)
|
|
108
|
+
## `Component` (`component.ks`, `velement.ks`, `vdom.ks`) — real vdom diffing
|
|
90
109
|
|
|
91
110
|
```ks
|
|
92
111
|
class MyWidget : Component {
|
|
@@ -96,46 +115,68 @@ class MyWidget : Component {
|
|
|
96
115
|
this.count = 0;
|
|
97
116
|
}
|
|
98
117
|
|
|
99
|
-
public override
|
|
100
|
-
|
|
101
|
-
el.
|
|
118
|
+
public override VElement Render() {
|
|
119
|
+
VElement el = VElement.Create("div");
|
|
120
|
+
el.TextContent = "Count: " + this.count;
|
|
102
121
|
return el;
|
|
103
122
|
}
|
|
104
123
|
|
|
105
124
|
public void Bump() {
|
|
106
125
|
this.count = this.count + 1;
|
|
107
|
-
this.Update(); // re-runs Render(),
|
|
126
|
+
this.Update(); // re-runs Render(), diffs it against the previous tree, patches real DOM
|
|
108
127
|
}
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
MyWidget w = new MyWidget();
|
|
112
|
-
w.Mount(document.body); // first Render() + append
|
|
113
|
-
w.Bump(); // re-render
|
|
131
|
+
w.Mount(document.body); // first Render() + materialize + append
|
|
132
|
+
w.Bump(); // re-render + diff + patch
|
|
114
133
|
```
|
|
115
134
|
|
|
116
|
-
- `Render()`: `virtual`, override it to
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
- `
|
|
122
|
-
|
|
135
|
+
- `Render()`: `virtual`, override it to describe the current state as a `VElement` tree —
|
|
136
|
+
a lightweight description of a DOM element (`kopular/velement`), not a real one. Called
|
|
137
|
+
by both `Mount()` and `Update()`. Provide it as a real markup file via KopScript's
|
|
138
|
+
`template from "./x.html";` (see "Templates" below) instead of hand-writing it — both
|
|
139
|
+
produce the exact same method; Kopular needed no framework code changes to support this.
|
|
140
|
+
- `VElement`: `Tag`, `TextContent`/`ClassName`/`Id`/`Value` (direct fields — `Value` is the
|
|
141
|
+
one that must be a live DOM *property*, not an attribute: `SetAttr("value", x)` sets the
|
|
142
|
+
default value, not the current one), `RawHtml` (an opaque, undiffed leaf — set instead of
|
|
143
|
+
`TextContent`/children, for a raw-HTML-then-wire-handlers pattern), the four fixed named
|
|
144
|
+
event fields `OnClick`/`OnInput`/`OnBlur`/`OnChange` (each a real no-op by default, never
|
|
145
|
+
null — no nullable function type to fall back on), `AppendChild(child)`, and
|
|
146
|
+
`SetAttr(name, value)` (the escape hatch for any other real HTML attribute — `href`,
|
|
147
|
+
`src`, `alt`, `placeholder`, ...; never `Value`, see above). `Id` doubles as a stable key
|
|
148
|
+
for list-child reconciliation — see `PatchChildren` below.
|
|
149
|
+
- `Mount(parent)`: calls `Render()` once, materializes the returned tree into real DOM
|
|
150
|
+
(`Materialize` in `vdom.ks`), appends it to `parent`, then calls `AfterRender(root)`.
|
|
123
151
|
- `Update()` (protected — called from within the component, not externally): calls
|
|
124
|
-
`Render()` again and `
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
`Render()` again and `Patch`es the new tree against the previous one (`vdom.ks`), reusing
|
|
153
|
+
a real DOM node wherever a node's tag stays the same instead of rebuilding it — sibling
|
|
154
|
+
nodes untouched by the change keep their exact identity (`===`), not just their content.
|
|
155
|
+
A list child without a stable `Id` still ends up correct after a reorder, but isn't
|
|
156
|
+
guaranteed to keep its own real node (see `PatchChildren`'s keyed-vs-positional matching
|
|
157
|
+
in `vdom.ks`). Then calls `AfterRender(root)`.
|
|
158
|
+
- `AfterRender(root)`: `virtual`, a no-op by default, called at the end of both `Mount()`
|
|
159
|
+
and `Update()` with the real, now-materialized/patched root `Element`. For a component
|
|
160
|
+
that needs to do further imperative work against its own real DOM — most commonly,
|
|
161
|
+
mounting a *nested* `Component` into a slot `Render()`'s tree just describes as data,
|
|
162
|
+
since a live mounted child isn't something `VElement` can express (see `Router`'s own
|
|
163
|
+
use of this to mount its matched page into the outlet, and "Dependency injection" below
|
|
164
|
+
for `RoutedApp` doing the same with `Router` itself).
|
|
165
|
+
- **Known limitation**: reconciliation is per-`Component`, not across nested ones — if a
|
|
166
|
+
*parent* component's own `Render()` output changes shape around a slot where a nested
|
|
167
|
+
Component was `Mount()`ed via `AfterRender`, that nested Component isn't automatically
|
|
168
|
+
re-`Mount()`ed or torn down as part of the parent's diff. Compose independent components
|
|
169
|
+
into a stable, unchanging slot (see `Router`'s own outlet, which never changes tag or
|
|
170
|
+
position) to avoid this rather than nesting components whose *surrounding* structure
|
|
171
|
+
also changes.
|
|
131
172
|
- **`Update()` before `Mount()` is a safe no-op**, not an error. This matters for two
|
|
132
173
|
sibling `Component`s (e.g. two `Router` pages) that share one injected service's
|
|
133
174
|
`state<T>` and both `Subscribe()` it — every route's page is constructed eagerly (see
|
|
134
175
|
`Router.AddRoute`), so at any given time most of them are constructed but never
|
|
135
176
|
`Mount()`ed. Changing that shared state fires `Subscribe` on all of them, including the
|
|
136
|
-
ones that aren't the currently-routed page — `Update()` just skips the render/
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
ones that aren't the currently-routed page — `Update()` just skips the render/patch for
|
|
178
|
+
those, since `Mount()` will run a fresh `Render()` anyway whenever one of them actually
|
|
179
|
+
becomes routed.
|
|
139
180
|
- `RenderError(string message)`: `virtual`, called by `Mount()`/`Update()` (via a private
|
|
140
181
|
`SafeRender()` wrapper) when `Render()` throws, instead of letting the exception
|
|
141
182
|
propagate uncaught and crash whatever triggered the render (a click handler, a `Router`
|
|
@@ -143,9 +184,9 @@ w.Bump(); // re-render
|
|
|
143
184
|
opt-in error recovery**; a `Component` that never overrides `RenderError` behaves
|
|
144
185
|
exactly as before this existed. Override it to show a fallback UI instead:
|
|
145
186
|
```ks
|
|
146
|
-
protected override
|
|
147
|
-
|
|
148
|
-
el.
|
|
187
|
+
protected override VElement RenderError(string message) {
|
|
188
|
+
VElement el = VElement.Create("div");
|
|
189
|
+
el.TextContent = "Something went wrong: " + message;
|
|
149
190
|
return el;
|
|
150
191
|
}
|
|
151
192
|
```
|
|
@@ -170,16 +211,22 @@ class Counter : Component {
|
|
|
170
211
|
|
|
171
212
|
- `{{ expr }}` interpolation, `(event)="stmt"`, `[prop]="expr"`, `*if="expr"`,
|
|
172
213
|
`*for="Type varName of expr"` — all real KopScript, checked at compile time, desugared
|
|
173
|
-
to the exact same `
|
|
174
|
-
|
|
214
|
+
to the exact same `VElement.Create`/`.AppendChild`/`.TextContent`/named-event-field
|
|
215
|
+
calls a hand-written `Render()` would use. `(event)` only accepts `click`/`input`/
|
|
216
|
+
`blur`/`change` — `VElement`'s own fixed set — anything else is a compile error
|
|
217
|
+
(`KS5016`). `[prop]`/static `attr="..."` assign directly for `id`/`className`/`value`;
|
|
218
|
+
anything else goes through `SetAttr` instead.
|
|
175
219
|
- A `state<T>` field declared directly on the class and referenced directly in the
|
|
176
220
|
template (`Count` above) gets `Subscribe((v) => this.Update())` wired automatically —
|
|
177
221
|
no manual `Subscribe` in the constructor for that field. State reached indirectly
|
|
178
222
|
(through a method, or `this.SomeService.Count`) still needs a manual `Subscribe`, same
|
|
179
223
|
as a hand-written `Render()` always has.
|
|
224
|
+
- **Two-way binding**: `[(value)]="Field"` desugars to `[value]="Field"` +
|
|
225
|
+
`(input)="Field = e.target.value"` — `value` only, and `Field` must be a bare name or
|
|
226
|
+
`this.Field` (see KopScript's own LLM.md `KS5017`/`KS5018` for the two rejected cases).
|
|
180
227
|
- One top-level element per template (hard error otherwise); no mixing text and element
|
|
181
|
-
children under one element (no text-node
|
|
182
|
-
|
|
228
|
+
children under one element (`VElement` has no text-node sibling concept, only
|
|
229
|
+
`.TextContent`); no pipes, at most one structural directive per element.
|
|
183
230
|
- This is entirely a KopScript compiler feature (parsed/desugared before type-checking
|
|
184
231
|
runs) — Kopular's own framework code (`component.ks`, `dom.ks`) is unmodified and
|
|
185
232
|
unaware templates exist; a template-generated `Render()` is indistinguishable from a
|
|
@@ -191,14 +238,14 @@ Real URLs via the History API (`pushState`/`popstate`), not hash routing.
|
|
|
191
238
|
|
|
192
239
|
```ks
|
|
193
240
|
class NotFoundPage : Component {
|
|
194
|
-
public override
|
|
241
|
+
public override VElement Render() { /* ... */ }
|
|
195
242
|
}
|
|
196
243
|
class HomePage : Component {
|
|
197
244
|
private Router Nav;
|
|
198
245
|
constructor(Router nav) : base() { this.Nav = nav; }
|
|
199
|
-
public override
|
|
200
|
-
|
|
201
|
-
btn.
|
|
246
|
+
public override VElement Render() {
|
|
247
|
+
VElement btn = VElement.Create("button");
|
|
248
|
+
btn.OnClick = (Event e) => { this.Nav.Navigate("/about"); };
|
|
202
249
|
return btn;
|
|
203
250
|
}
|
|
204
251
|
}
|
|
@@ -238,14 +285,14 @@ nav.Navigate("/about"); // pushState + immediate re-ren
|
|
|
238
285
|
`"/search?sort=name"` -> `Query("sort") == "name"`. Values are **not percent-decoded** —
|
|
239
286
|
no `decodeURIComponent` binding exists yet, a deliberate v1 cut; `%20`/`+` arrive
|
|
240
287
|
exactly as written in the URL, not converted to a space.
|
|
241
|
-
- **Why `Param` is a plain field, not `state<T>`**: `
|
|
242
|
-
|
|
288
|
+
- **Why `Param` is a plain field, not `state<T>`**: `Router`'s own `AfterRender` already
|
|
289
|
+
re-`Mount()`s the matched page into its outlet on every `Navigate()`/`popstate`, which
|
|
243
290
|
re-runs that page's own `Render()` (reading the fresh `Param`) with no extra step.
|
|
244
291
|
Making it `state<T>` and having a page `Subscribe()` to it — the pattern every *other*
|
|
245
292
|
piece of state in this framework uses — actually crashes: `Match()` sets it *before*
|
|
246
|
-
`
|
|
247
|
-
that matches a page nothing has `Mount()`ed yet fires that page's subscribed
|
|
248
|
-
while its inherited `Update()` still has no `ParentElement` to
|
|
293
|
+
`AfterRender` finishes mounting the matched page into the outlet, so the very first
|
|
294
|
+
route that matches a page nothing has `Mount()`ed yet fires that page's subscribed
|
|
295
|
+
listener while its inherited `Update()` still has no `ParentElement` to patch into.
|
|
249
296
|
- **Navigation guards**: `SetGuard(redirectPath, (string) => bool guard)` — `guard` is
|
|
250
297
|
called with the target path before every navigation (including a direct load/refresh);
|
|
251
298
|
returning `false` redirects to `redirectPath` (via `pushState`, so the URL updates too —
|
|
@@ -282,11 +329,11 @@ a **hand-written** `Render()`, where `if` being a statement (not an expression)
|
|
|
282
329
|
conditional value needs a helper to get one out of it:
|
|
283
330
|
|
|
284
331
|
```ks
|
|
285
|
-
|
|
332
|
+
VElement If(bool condition, () => VElement whenTrue, () => VElement whenFalse)
|
|
286
333
|
```
|
|
287
334
|
|
|
288
335
|
```ks
|
|
289
|
-
root.
|
|
336
|
+
root.AppendChild(If(this.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLogin()));
|
|
290
337
|
```
|
|
291
338
|
|
|
292
339
|
Both branches always required (no null "nothing" value); only the branch actually taken
|
|
@@ -295,20 +342,22 @@ at all in hand-written `Render()` either:
|
|
|
295
342
|
|
|
296
343
|
```ks
|
|
297
344
|
// *ngFor — plain array method
|
|
298
|
-
this.Items.ForEach((Item item) => { list.
|
|
345
|
+
this.Items.ForEach((Item item) => { list.AppendChild(this.BuildItemRow(item)); });
|
|
299
346
|
|
|
300
347
|
// *ngSwitch — plain KopScript `match` expression (exhaustiveness-checked, unlike *ngSwitch)
|
|
301
|
-
root.
|
|
348
|
+
root.AppendChild(match this.Status {
|
|
302
349
|
"loading" => this.BuildSpinner(),
|
|
303
350
|
"error" => this.BuildError(),
|
|
304
351
|
_ => this.BuildContent()
|
|
305
352
|
});
|
|
306
353
|
```
|
|
307
354
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
355
|
+
**Keyed/reuse-existing-DOM-nodes diffing (the `*ngFor trackBy` angle) exists** — give each
|
|
356
|
+
item's `VElement` a stable `.Id` (e.g. the item's own id) and `Update()`'s diff engine
|
|
357
|
+
(`PatchChildren` in `vdom.ks`) matches children by `Id` across a re-render, reusing a
|
|
358
|
+
matched child's real DOM node rather than rebuilding it. Without a stable `Id`, a
|
|
359
|
+
reordered list still renders correctly, but a given item's real node isn't guaranteed to
|
|
360
|
+
follow its data.
|
|
312
361
|
|
|
313
362
|
## `Http` (`http.ks`) — thin wrapper over `fetch`
|
|
314
363
|
|
|
@@ -367,7 +416,7 @@ email.Valid(); // bool — Error.Value == null
|
|
|
367
416
|
after the first as a nested function type, not an array element type, and errors expecting
|
|
368
417
|
`=>`). Combine checks as an if-chain in one lambda (see the `email` example above), or via
|
|
369
418
|
the fixed-arity `CombineValidators2<T>`/`CombineValidators3<T>` free functions (generic,
|
|
370
|
-
inference-only — see `
|
|
419
|
+
inference-only — see `KopScript`'s own "Generics" docs for why they're *free* functions, not
|
|
371
420
|
`Validators` static methods):
|
|
372
421
|
```ks
|
|
373
422
|
CombineValidators2((string v) => Validators.Required(v), (string v) => Validators.Email(v))
|
|
@@ -376,13 +425,12 @@ CombineValidators2((string v) => Validators.Required(v), (string v) => Validator
|
|
|
376
425
|
Fixed-arity (2, 3 — add more the same way if a form ever needs to chain further), not a
|
|
377
426
|
general `Validators.All(...)`, for the same array-of-function-values reason above.
|
|
378
427
|
|
|
379
|
-
**No DOM binding
|
|
380
|
-
|
|
381
|
-
`
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`[(ngModel)]`-equivalent.
|
|
428
|
+
**No DOM binding in a hand-written `Render()`** — wiring `.Value` to a real `<input>` is a
|
|
429
|
+
plain `VElement.OnInput` assignment reading `e.target.value` (the same as any other event
|
|
430
|
+
handler; `VElement.Value` itself is one-way, host-to-DOM only), and reading it back out
|
|
431
|
+
via `.Touch()` on `OnBlur`. A **template** has real `[(value)]="Field"` sugar for the
|
|
432
|
+
value-binding half (see "Templates" above); `Touch()` on blur still needs its own explicit
|
|
433
|
+
`(blur)="Field.Touch()"` either way — `[(value)]` only ever wires `value`/`input`.
|
|
386
434
|
|
|
387
435
|
## `kopular/testing` (`testing.js`, hand-written JS, not compiled from `.ks`)
|
|
388
436
|
|
|
@@ -435,10 +483,14 @@ class AppContainer {
|
|
|
435
483
|
class RoutedApp : Component {
|
|
436
484
|
private Router Nav;
|
|
437
485
|
constructor(AppContainer services) : base() { this.Nav = services.Nav; }
|
|
438
|
-
public override
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
486
|
+
public override VElement Render() {
|
|
487
|
+
return VElement.Create("div");
|
|
488
|
+
}
|
|
489
|
+
// Mounting Router — a live, nested Component — isn't something a VElement
|
|
490
|
+
// tree can express as data; AfterRender gets the real, now-materialized
|
|
491
|
+
// root Element instead (see "Component" above).
|
|
492
|
+
protected override void AfterRender(Element root) {
|
|
493
|
+
this.Nav.Mount(root);
|
|
442
494
|
}
|
|
443
495
|
}
|
|
444
496
|
|
|
@@ -462,8 +514,8 @@ class CounterService {
|
|
|
462
514
|
- **`Router.Param` is a plain `string`, not `state<T>` — don't `Subscribe()` to it.** A
|
|
463
515
|
`state<T>`-based design for it was tried and genuinely crashes: `Navigate()` sets `Param`
|
|
464
516
|
*before* the newly-matched page finishes mounting, so a page `Subscribe`-ing to it fires
|
|
465
|
-
while its own `Update()` still has no `ParentElement` to
|
|
466
|
-
|
|
517
|
+
while its own `Update()` still has no `ParentElement` to patch into. `Render()` already
|
|
518
|
+
re-reads the fresh `Param` on every navigation with no extra step — just read
|
|
467
519
|
`this.Nav.Param` directly inside `Render()`.
|
|
468
520
|
- **Deploying a `Router`-based app needs SPA/history-fallback configured on the actual host**
|
|
469
521
|
— this bit the real `KopularDemo` production site (worked when navigated to via a link,
|
|
@@ -480,19 +532,37 @@ class CounterService {
|
|
|
480
532
|
independent mechanisms, not the same thing wired two ways** — a template never needs
|
|
481
533
|
`directives.ks`'s `If()` imported or called; `*if`/`*for` compile to real `if`/`for`
|
|
482
534
|
directly. Don't mix a template with a hand-written-`Render()` helper call.
|
|
483
|
-
-
|
|
484
|
-
|
|
485
|
-
`
|
|
486
|
-
either
|
|
535
|
+
- **`[(value)]` two-way binding is template-only, and `value`-only.** A hand-written
|
|
536
|
+
`Render()` still always needs an explicit `OnInput` assignment reading
|
|
537
|
+
`e.target.value` — there's no equivalent shorthand there, since it already has direct
|
|
538
|
+
field access. `[(id)]`/`[(className)]` don't exist either, even in a template — the
|
|
539
|
+
sugar only wires `value`/`input`, the one pairing with a real "user just changed this"
|
|
540
|
+
event.
|
|
541
|
+
- **A `VElement` event binding only accepts `click`/`input`/`blur`/`change`** — both in a
|
|
542
|
+
template's `(event)="..."` and a hand-written `OnClick`/`OnInput`/`OnBlur`/`OnChange`
|
|
543
|
+
assignment. There's no generic `addEventListener` on `VElement` (event handlers are part
|
|
544
|
+
of the tree's own data, not wired against a live DOM node until `Materialize`/`Patch`
|
|
545
|
+
runs) — an event Kopular doesn't have a named field for isn't reachable from `Render()`
|
|
546
|
+
at all yet.
|
|
547
|
+
- **Reconciliation doesn't cross a `Mount()`ed-via-`AfterRender` boundary.** Real vdom
|
|
548
|
+
diffing patches one `Component`'s own subtree; a *nested* Component mounted into a slot
|
|
549
|
+
via `AfterRender` (see `Router`'s outlet, or `RoutedApp` mounting `Router` itself) isn't
|
|
550
|
+
automatically re-`Mount()`ed if the *parent's* surrounding tree shape changes — see
|
|
551
|
+
"Component"'s own "Known limitation" above.
|
|
487
552
|
|
|
488
553
|
## Does not exist
|
|
489
554
|
|
|
490
555
|
DI container/injector · decorators (`@Injectable`, `@Component`, ...) · a runtime
|
|
491
556
|
template engine or interpreted expression language — templates compile to the same
|
|
492
557
|
imperative `Render()` code as the hand-written form, checked at compile time, not
|
|
493
|
-
interpreted at runtime (see "Templates" above) · two-way binding
|
|
494
|
-
|
|
495
|
-
|
|
558
|
+
interpreted at runtime (see "Templates" above) · two-way binding in a hand-written
|
|
559
|
+
`Render()`, or on anything but `value` even in a template (`[(value)]="Field"` exists —
|
|
560
|
+
see "Templates" above — but it's `value`-only, and templates-only) ·
|
|
561
|
+
reconciliation across a nested-Component boundary (see "Common mistakes" above — real
|
|
562
|
+
vdom diffing exists *within* one Component's own subtree, via `Update()`/`vdom.ks`) ·
|
|
563
|
+
a generic/arbitrary `VElement` event binding — only `click`/`input`/`blur`/`change` ·
|
|
564
|
+
pipes · animations · typed/generic HTTP responses (`Http` returns raw text — see above) ·
|
|
565
|
+
SSR.
|
|
496
566
|
|
|
497
567
|
(`FormField<T>`/`Validators` and `kp new` — see "FormField<T> / Validators" and "Starting
|
|
498
568
|
a new project" above — are real, shipped features; they used to be listed here as gaps
|