kopular 0.17.2 → 0.19.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 +37 -0
- package/README.md +95 -0
- package/package.json +3 -2
- package/src/computed.js +37 -0
- package/src/computed.js.map +1 -0
- package/src/computed.ks +49 -0
package/LLM.md
CHANGED
|
@@ -284,6 +284,23 @@ w.Bump(); // re-render + diff + patch
|
|
|
284
284
|
components, a modal that comes and goes.
|
|
285
285
|
- Hand-written `Render()` only — there's no template (`.html`) syntax yet for embedding a
|
|
286
286
|
child component the way `*if`/`*for` embed structural logic.
|
|
287
|
+
- **Content projection (React's `children`/Angular's `<ng-content>`) needs no separate
|
|
288
|
+
mechanism** — pass a `() => VElement` into a child's constructor and call it from inside
|
|
289
|
+
its own `Render()`; it's invoked fresh every render and closes over the parent's `this`,
|
|
290
|
+
so projected content reflects the parent's current state on every patch:
|
|
291
|
+
```ks
|
|
292
|
+
class Card : Component {
|
|
293
|
+
private () => VElement ContentBuilder;
|
|
294
|
+
constructor(() => VElement contentBuilder) : base() { this.ContentBuilder = contentBuilder; }
|
|
295
|
+
public override VElement Render() {
|
|
296
|
+
VElement div = VElement.Create("div");
|
|
297
|
+
div.AppendChild(this.ContentBuilder());
|
|
298
|
+
return div;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
More than one slot: one named `() => VElement` callback per slot (same fixed-named-slot
|
|
303
|
+
convention as `VElement`'s own `OnClick`/etc.), not a generic bag.
|
|
287
304
|
- **`Update()` before `Mount()` is a safe no-op**, not an error. This matters for two
|
|
288
305
|
sibling `Component`s (e.g. two `Router` pages) that share one injected service's
|
|
289
306
|
`state<T>` and both `Subscribe()` it — every route's page is constructed eagerly (see
|
|
@@ -547,6 +564,26 @@ via `.Touch()` on `OnBlur`. A **template** has real `[(value)]="Field"` sugar fo
|
|
|
547
564
|
value-binding half (see "Templates" above); `Touch()` on blur still needs its own explicit
|
|
548
565
|
`(blur)="Field.Touch()"` either way — `[(value)]` only ever wires `value`/`input`.
|
|
549
566
|
|
|
567
|
+
## `Computed1<A, R>` / `Computed2<A, B, R>` (`computed.ks`)
|
|
568
|
+
|
|
569
|
+
```ks
|
|
570
|
+
state<number> price = state(10);
|
|
571
|
+
state<number> qty = state(2);
|
|
572
|
+
Computed2<number, number, number> total = new Computed2<number, number, number>(
|
|
573
|
+
price, qty, (number p, number q) => p * q);
|
|
574
|
+
|
|
575
|
+
total.Value.Value // 20 — recomputed whenever price OR qty changes
|
|
576
|
+
total.Value.Subscribe((number v) => { ... }); // returns an unsubscribe handle, same as any state<T>
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
`useMemo`/Angular signals' `computed()` equivalent — built entirely on `state<T>`'s own
|
|
580
|
+
`.Value`/`.Subscribe`, no new reactive primitive. `Value` is `state<R>`, not a bare `R` —
|
|
581
|
+
KopScript's property grammar has no custom-getter syntax, so a computed value can't expose
|
|
582
|
+
a self-recomputing property; `state<R>` already gives the right two members instead.
|
|
583
|
+
Dependencies are explicit constructor arguments, not automatically tracked. Fixed arity (1
|
|
584
|
+
source, 2 sources) — same array-of-function-values reason `CombineValidators2/3` are fixed;
|
|
585
|
+
add a `Computed3<A, B, C, R>` the same way if ever needed.
|
|
586
|
+
|
|
550
587
|
## `kopular/testing` (`testing.js`, hand-written JS, not compiled from `.ks`)
|
|
551
588
|
|
|
552
589
|
```ts
|
package/README.md
CHANGED
|
@@ -468,6 +468,65 @@ removal to get right, so there's nothing this mechanism would add there. Reach f
|
|
|
468
468
|
`VElement.Mount` for anything with real add/remove/reorder: a list of components, a modal
|
|
469
469
|
that comes and goes, anything genuinely dynamic.
|
|
470
470
|
|
|
471
|
+
## Content projection — a parent giving a child what to render
|
|
472
|
+
|
|
473
|
+
React's `children` prop and Angular's `<ng-content>` both let a parent hand a child
|
|
474
|
+
arbitrary markup to render at a spot the child itself decides. Kopular needs no separate
|
|
475
|
+
mechanism for this at all: pass a `() => VElement` into the child's constructor, the same
|
|
476
|
+
as any other constructor argument, and have the child call it from inside its own
|
|
477
|
+
`Render()`:
|
|
478
|
+
|
|
479
|
+
```ks
|
|
480
|
+
class Card : Component {
|
|
481
|
+
private () => VElement ContentBuilder;
|
|
482
|
+
constructor(() => VElement contentBuilder) : base() {
|
|
483
|
+
this.ContentBuilder = contentBuilder;
|
|
484
|
+
}
|
|
485
|
+
public override VElement Render() {
|
|
486
|
+
VElement div = VElement.Create("div");
|
|
487
|
+
div.ClassName = "card";
|
|
488
|
+
div.AppendChild(this.ContentBuilder());
|
|
489
|
+
return div;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Usage — the parent decides what's inside the card, Card decides the chrome around it:
|
|
494
|
+
Card card = new Card(() => this.BuildCardBody());
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Because the callback is invoked fresh on every one of `Card`'s own renders (not a value
|
|
498
|
+
captured once and frozen), and it's a closure over the *parent's* own `this`, the projected
|
|
499
|
+
content reflects the parent's current state every time — bump a field the parent's own
|
|
500
|
+
`BuildCardBody()` reads, call the parent's `Update()`, and `Card`'s own next patch shows the
|
|
501
|
+
new value, patched in place like any other content. No new API: this is just
|
|
502
|
+
`VElement.Mount` plus an ordinary function-typed constructor argument, the same "no hidden
|
|
503
|
+
magic, explicit constructor args" pattern dependency injection above already uses.
|
|
504
|
+
|
|
505
|
+
More than one projection point works the same way — one named callback per slot, the same
|
|
506
|
+
fixed-named-slot convention `VElement`'s own `OnClick`/`OnInput`/`OnBlur`/`OnChange` already
|
|
507
|
+
use rather than a generic, unordered bag:
|
|
508
|
+
|
|
509
|
+
```ks
|
|
510
|
+
class Panel : Component {
|
|
511
|
+
private () => VElement HeaderBuilder;
|
|
512
|
+
private () => VElement BodyBuilder;
|
|
513
|
+
constructor(() => VElement headerBuilder, () => VElement bodyBuilder) : base() {
|
|
514
|
+
this.HeaderBuilder = headerBuilder;
|
|
515
|
+
this.BodyBuilder = bodyBuilder;
|
|
516
|
+
}
|
|
517
|
+
public override VElement Render() {
|
|
518
|
+
VElement div = VElement.Create("div");
|
|
519
|
+
VElement header = VElement.Create("header");
|
|
520
|
+
header.AppendChild(this.HeaderBuilder());
|
|
521
|
+
div.AppendChild(header);
|
|
522
|
+
VElement body = VElement.Create("section");
|
|
523
|
+
body.AppendChild(this.BodyBuilder());
|
|
524
|
+
div.AppendChild(body);
|
|
525
|
+
return div;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
471
530
|
## HTTP
|
|
472
531
|
|
|
473
532
|
```ks
|
|
@@ -579,6 +638,42 @@ gets real sugar for exactly this — `[(value)]="Field"` desugars to `[value]="F
|
|
|
579
638
|
`(input)="Field = e.target.value"` (see KopScript's own "Templates" docs) — since a
|
|
580
639
|
markup file has no equivalent direct access to fall back on.
|
|
581
640
|
|
|
641
|
+
## Computed values
|
|
642
|
+
|
|
643
|
+
React's `useMemo`/Angular signals' `computed()` equivalent — a read-only value derived
|
|
644
|
+
from one or more `state<T>` sources, recomputed and re-notified whenever a source changes:
|
|
645
|
+
|
|
646
|
+
```ks
|
|
647
|
+
using "./computed";
|
|
648
|
+
|
|
649
|
+
state<number> price = state(10);
|
|
650
|
+
state<number> qty = state(2);
|
|
651
|
+
Computed2<number, number, number> total = new Computed2<number, number, number>(
|
|
652
|
+
price, qty, (number p, number q) => p * q);
|
|
653
|
+
|
|
654
|
+
print(total.Value.Value); // 20
|
|
655
|
+
total.Value.Subscribe((number v) => print("total: " + v));
|
|
656
|
+
price.Value = 15; // prints "total: 30"
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
`Value` is `state<R>`, not a bare `R` — KopScript's property grammar has no custom-getter
|
|
660
|
+
syntax (only auto-implemented `{ get; }`/`{ get; set; }`), so a computed value can't expose
|
|
661
|
+
a property that recomputes itself on read. `state<R>` already **is** the right shape for
|
|
662
|
+
"read the current value, or subscribe to it changing" — `total.Value.Value`/
|
|
663
|
+
`total.Value.Subscribe(...)` are the exact same two members any other `state<T>` has, not a
|
|
664
|
+
parallel API to learn.
|
|
665
|
+
|
|
666
|
+
Dependencies are explicit, not automatically tracked the way Angular signals infer their
|
|
667
|
+
own by watching which signals get read during evaluation — that needs a live "currently
|
|
668
|
+
evaluating" context threaded through every read, a different kind of feature from anything
|
|
669
|
+
else here. Naming each source directly is one honest step short of automatic, consistent
|
|
670
|
+
with explicit constructor arguments everywhere else in Kopular (dependency injection,
|
|
671
|
+
routing). `Computed1<A, R>` covers one source; `Computed2<A, B, R>` covers two — fixed
|
|
672
|
+
arity for the same reason `CombineValidators2`/`CombineValidators3` (above) are: KopScript
|
|
673
|
+
has no array-of-function-values type, and no way to express "N sources of N different
|
|
674
|
+
types" without one type parameter per source. Add a `Computed3<A, B, C, R>` the same way if
|
|
675
|
+
a real derivation ever needs three.
|
|
676
|
+
|
|
582
677
|
## Starting a new project: `kp new`
|
|
583
678
|
|
|
584
679
|
Everything in the next section — the `extern` bindings, plus a `vendor/kopular/` copy of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"./directives": "./src/directives.js",
|
|
34
34
|
"./http": "./src/http.js",
|
|
35
35
|
"./forms": "./src/forms.js",
|
|
36
|
+
"./computed": "./src/computed.js",
|
|
36
37
|
"./testing": "./src/testing.js"
|
|
37
38
|
},
|
|
38
39
|
"files": [
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"LLM.md"
|
|
43
44
|
],
|
|
44
45
|
"scripts": {
|
|
45
|
-
"build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks && ks build src/forms.ks",
|
|
46
|
+
"build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks && ks build src/forms.ks && ks build src/computed.ks",
|
|
46
47
|
"prepublishOnly": "npm run build",
|
|
47
48
|
"pretest": "npm run build",
|
|
48
49
|
"test": "vitest run",
|
package/src/computed.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class __KopState {
|
|
2
|
+
constructor(value) {
|
|
3
|
+
this._value = value;
|
|
4
|
+
this._listeners = [];
|
|
5
|
+
}
|
|
6
|
+
get Value() { return this._value; }
|
|
7
|
+
set Value(v) {
|
|
8
|
+
this._value = v;
|
|
9
|
+
for (const listener of this._listeners) listener(v);
|
|
10
|
+
}
|
|
11
|
+
Subscribe(listener) {
|
|
12
|
+
this._listeners.push(listener);
|
|
13
|
+
return () => { this._listeners = this._listeners.filter((l) => l !== listener); };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Computed1 {
|
|
18
|
+
constructor(source, compute) {
|
|
19
|
+
this.Value = new __KopState(compute(source.Value));
|
|
20
|
+
source.Subscribe((v) => {
|
|
21
|
+
this.Value.Value = compute(v);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class Computed2 {
|
|
26
|
+
constructor(a, b, compute) {
|
|
27
|
+
this.Value = new __KopState(compute(a.Value, b.Value));
|
|
28
|
+
a.Subscribe((v) => {
|
|
29
|
+
this.Value.Value = compute(v, b.Value);
|
|
30
|
+
});
|
|
31
|
+
b.Subscribe((v) => {
|
|
32
|
+
this.Value.Value = compute(a.Value, v);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=computed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computed.js","sources":["computed.ks"],"sourcesContent":["// Computed1<A, R>/Computed2<A, B, R>: a read-only reactive value derived\n// from one or more state<T> sources, recomputed and re-notified whenever\n// any source changes — React's useMemo/Angular signals' computed()\n// equivalent. No new reactive primitive: built entirely on state<T>'s\n// existing .Value/.Subscribe (including the unsubscribe handle Subscribe\n// now returns — see kopscript's own LLM.md), the same way FormField<T>\n// (forms.ks) is built on state<T> rather than introducing a second\n// reactive system.\n//\n// Deliberately EXPLICIT dependencies, not automatic tracking (the way\n// Angular signals' computed() infers its own deps by watching which\n// signals get read during evaluation) — that needs a live \"currently\n// evaluating\" context the compiler/runtime would have to thread through\n// every read, a different kind of feature than anything else here.\n// Naming each source explicitly is one honest step short of automatic,\n// consistent with this project's own \"explicit constructor args, no\n// hidden magic\" position elsewhere (dependency injection, routing).\n//\n// `Value` is `state<R>`, not a bare `R` — KopScript's declared-property\n// grammar has no way to write a custom getter body (only auto-implemented\n// `{ get; }`/`{ get; set; }`, backed by ordinary assignment), so a\n// Computed can't expose a property that recomputes itself on read. state<R>\n// already IS the \"read the current value, or subscribe to it changing\"\n// shape this needs — Value.Value / Value.Subscribe(...), the exact same\n// two members any other state<T> exposes, not a parallel API to learn.\n//\n// Fixed arity (1 and 2 sources) for the same reason CombineValidators2/3\n// (forms.ks) are fixed-arity, not a general array-taking form: KopScript\n// has no array-of-function-values type, and no way to express \"N sources\n// of N different types\" without one type parameter per source. Add\n// Computed3<A, B, C, R> the same way if a real derivation ever needs three.\nclass Computed1<A, R> {\n public state<R> Value { get; }\n\n constructor(state<A> source, (A) => R compute) {\n this.Value = state(compute(source.Value));\n source.Subscribe((A v) => { this.Value.Value = compute(v); });\n }\n}\n\nclass Computed2<A, B, R> {\n public state<R> Value { get; }\n\n constructor(state<A> a, state<B> b, (A, B) => R compute) {\n this.Value = state(compute(a.Value, b.Value));\n a.Subscribe((A v) => { this.Value.Value = compute(v, b.Value); });\n b.Subscribe((B v) => { this.Value.Value = compute(a.Value, v); });\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AA+BA;EAGE;IACa;IACK;EAA6B;;;;AAIjD;EAGE;IACa;IACA;EAA6B;;IAC7B;EAA6B"}
|
package/src/computed.ks
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Computed1<A, R>/Computed2<A, B, R>: a read-only reactive value derived
|
|
2
|
+
// from one or more state<T> sources, recomputed and re-notified whenever
|
|
3
|
+
// any source changes — React's useMemo/Angular signals' computed()
|
|
4
|
+
// equivalent. No new reactive primitive: built entirely on state<T>'s
|
|
5
|
+
// existing .Value/.Subscribe (including the unsubscribe handle Subscribe
|
|
6
|
+
// now returns — see kopscript's own LLM.md), the same way FormField<T>
|
|
7
|
+
// (forms.ks) is built on state<T> rather than introducing a second
|
|
8
|
+
// reactive system.
|
|
9
|
+
//
|
|
10
|
+
// Deliberately EXPLICIT dependencies, not automatic tracking (the way
|
|
11
|
+
// Angular signals' computed() infers its own deps by watching which
|
|
12
|
+
// signals get read during evaluation) — that needs a live "currently
|
|
13
|
+
// evaluating" context the compiler/runtime would have to thread through
|
|
14
|
+
// every read, a different kind of feature than anything else here.
|
|
15
|
+
// Naming each source explicitly is one honest step short of automatic,
|
|
16
|
+
// consistent with this project's own "explicit constructor args, no
|
|
17
|
+
// hidden magic" position elsewhere (dependency injection, routing).
|
|
18
|
+
//
|
|
19
|
+
// `Value` is `state<R>`, not a bare `R` — KopScript's declared-property
|
|
20
|
+
// grammar has no way to write a custom getter body (only auto-implemented
|
|
21
|
+
// `{ get; }`/`{ get; set; }`, backed by ordinary assignment), so a
|
|
22
|
+
// Computed can't expose a property that recomputes itself on read. state<R>
|
|
23
|
+
// already IS the "read the current value, or subscribe to it changing"
|
|
24
|
+
// shape this needs — Value.Value / Value.Subscribe(...), the exact same
|
|
25
|
+
// two members any other state<T> exposes, not a parallel API to learn.
|
|
26
|
+
//
|
|
27
|
+
// Fixed arity (1 and 2 sources) for the same reason CombineValidators2/3
|
|
28
|
+
// (forms.ks) are fixed-arity, not a general array-taking form: KopScript
|
|
29
|
+
// has no array-of-function-values type, and no way to express "N sources
|
|
30
|
+
// of N different types" without one type parameter per source. Add
|
|
31
|
+
// Computed3<A, B, C, R> the same way if a real derivation ever needs three.
|
|
32
|
+
class Computed1<A, R> {
|
|
33
|
+
public state<R> Value { get; }
|
|
34
|
+
|
|
35
|
+
constructor(state<A> source, (A) => R compute) {
|
|
36
|
+
this.Value = state(compute(source.Value));
|
|
37
|
+
source.Subscribe((A v) => { this.Value.Value = compute(v); });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Computed2<A, B, R> {
|
|
42
|
+
public state<R> Value { get; }
|
|
43
|
+
|
|
44
|
+
constructor(state<A> a, state<B> b, (A, B) => R compute) {
|
|
45
|
+
this.Value = state(compute(a.Value, b.Value));
|
|
46
|
+
a.Subscribe((A v) => { this.Value.Value = compute(v, b.Value); });
|
|
47
|
+
b.Subscribe((B v) => { this.Value.Value = compute(a.Value, v); });
|
|
48
|
+
}
|
|
49
|
+
}
|