kopular 0.18.0 → 0.20.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 CHANGED
@@ -564,6 +564,50 @@ via `.Touch()` on `OnBlur`. A **template** has real `[(value)]="Field"` sugar fo
564
564
  value-binding half (see "Templates" above); `Touch()` on blur still needs its own explicit
565
565
  `(blur)="Field.Touch()"` either way — `[(value)]` only ever wires `value`/`input`.
566
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
+
587
+ ## `Resource<T>` (`resource.ks`)
588
+
589
+ ```ks
590
+ enum AsyncStatus { Loading, Success, Failure }
591
+
592
+ Resource<Response> r = new Resource<Response>(Http.Get(url)); // task already in flight
593
+ r.Status.Subscribe((AsyncStatus s) => this.Update());
594
+
595
+ match r.Status.Value {
596
+ AsyncStatus.Loading => ...,
597
+ AsyncStatus.Success => ... r.Data.Value ..., // T?
598
+ AsyncStatus.Failure => ... r.Error.Value ... // string?
599
+ };
600
+ ```
601
+
602
+ The loading/success/failure shape around a `task<T>`, as `state<T>` a `Component` renders
603
+ and `Subscribe`s to — most commonly wrapping an `Http` call. `Status` starts at
604
+ `AsyncStatus.Loading` immediately; the constructor's own handling of the task is
605
+ fire-and-forget (there's no way to construct a `task` value outside an `async` function
606
+ body to `await` it from the constructor itself — see "Async" above), so the caller must
607
+ pass an ALREADY-STARTED task (call the `async` function first — `Http.Get(url)`, not
608
+ something the constructor starts). `Status`/`Data`/`Error` transition together, exactly
609
+ once, inside a `try`/`catch` around the `await` — whichever branch the task actually takes.
610
+
567
611
  ## `kopular/testing` (`testing.js`, hand-written JS, not compiled from `.ks`)
568
612
 
569
613
  ```ts
package/README.md CHANGED
@@ -638,6 +638,77 @@ gets real sugar for exactly this — `[(value)]="Field"` desugars to `[value]="F
638
638
  `(input)="Field = e.target.value"` (see KopScript's own "Templates" docs) — since a
639
639
  markup file has no equivalent direct access to fall back on.
640
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
+
677
+ ## Async data — `Resource<T>`
678
+
679
+ The loading/success/failure shape almost every real app needs around a `task<T>` — most
680
+ commonly wrapping an `Http` call — as reactive `state<T>` a `Component` can render and
681
+ `Subscribe` to, instead of hand-rolling the same three fields and try/catch every time:
682
+
683
+ ```ks
684
+ using "./resource";
685
+
686
+ Resource<Response> dogs = new Resource<Response>(Http.Get("https://dog.ceo/api/breeds/list/all"));
687
+ dogs.Status.Subscribe((AsyncStatus s) => this.Update());
688
+ ```
689
+
690
+ ```ks
691
+ public override VElement Render() {
692
+ return match dogs.Status.Value {
693
+ AsyncStatus.Loading => this.BuildSpinner(),
694
+ AsyncStatus.Success => this.BuildList(dogs.Data.Value),
695
+ AsyncStatus.Failure => this.BuildError(dogs.Error.Value)
696
+ };
697
+ }
698
+ ```
699
+
700
+ `Status` starts at `AsyncStatus.Loading` the moment `Resource` is constructed — the
701
+ operation is already in flight (`Http.Get(...)` above is called before `Resource` ever
702
+ sees it; a `task<T>` value can only be produced by calling an `async` function, and
703
+ `Resource`'s own constructor isn't one, so it always receives an already-started
704
+ operation, never starts one itself). Its internal handling of that task is genuinely
705
+ fire-and-forget from the constructor's point of view (there's no way to construct a
706
+ `task` value outside an async function body to await it there instead — see KopScript's
707
+ own "Async" docs) — `Status`/`Data`/`Error` transition together, exactly once, whichever
708
+ branch the `task` actually takes. `match` over `AsyncStatus` gets the same real
709
+ exhaustiveness checking any other enum `match` does — leaving out a case is a compile
710
+ error, not a runtime blank screen.
711
+
641
712
  ## Starting a new project: `kp new`
642
713
 
643
714
  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.18.0",
3
+ "version": "0.20.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,8 @@
33
33
  "./directives": "./src/directives.js",
34
34
  "./http": "./src/http.js",
35
35
  "./forms": "./src/forms.js",
36
+ "./computed": "./src/computed.js",
37
+ "./resource": "./src/resource.js",
36
38
  "./testing": "./src/testing.js"
37
39
  },
38
40
  "files": [
@@ -42,7 +44,7 @@
42
44
  "LLM.md"
43
45
  ],
44
46
  "scripts": {
45
- "build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks && ks build src/forms.ks",
47
+ "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 && ks build src/resource.ks",
46
48
  "prepublishOnly": "npm run build",
47
49
  "pretest": "npm run build",
48
50
  "test": "vitest run",
@@ -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"}
@@ -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
+ }
@@ -0,0 +1,38 @@
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 const AsyncStatus = Object.freeze({ Loading: 0, Success: 1, Failure: 2 });
18
+ export class Resource {
19
+ constructor(operation) {
20
+ this.Status = new __KopState(AsyncStatus.Loading);
21
+ this.Data = new __KopState(null);
22
+ this.Error = new __KopState(null);
23
+ this.Run(operation);
24
+ }
25
+
26
+ async Run(operation) {
27
+ try {
28
+ let result = await operation;
29
+ this.Data.Value = result;
30
+ this.Status.Value = AsyncStatus.Success;
31
+ } catch (message) {
32
+ this.Error.Value = message;
33
+ this.Status.Value = AsyncStatus.Failure;
34
+ }
35
+ }
36
+ }
37
+
38
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sources":["resource.ks"],"sourcesContent":["// Resource<T>: the \"loading / success / failure\" shape around a task<T>,\n// as reactive state<T> a Component can render and Subscribe to — React's\n// (commonly hand-rolled, or via a data-fetching library) loading-state\n// pattern / Angular's resource() equivalent.\nenum AsyncStatus { Loading, Success, Failure }\n\nclass Resource<T> {\n public state<AsyncStatus> Status;\n public state<T?> Data;\n public state<string?> Error;\n\n constructor(task<T> operation) {\n this.Status = state(AsyncStatus.Loading);\n this.Data = state(null);\n this.Error = state(null);\n this.Run(operation);\n }\n\n private async task Run(task<T> operation) {\n try {\n T result = await operation;\n this.Data.Value = result;\n this.Status.Value = AsyncStatus.Success;\n } catch (string message) {\n this.Error.Value = message;\n this.Status.Value = AsyncStatus.Failure;\n }\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAIA;AAEA;EAKE;IACc;IACF;IACC;IACH;;;EAGI;IACZ;MACE;MACgB;MACE;;MAED;MACC"}
@@ -0,0 +1,29 @@
1
+ // Resource<T>: the "loading / success / failure" shape around a task<T>,
2
+ // as reactive state<T> a Component can render and Subscribe to — React's
3
+ // (commonly hand-rolled, or via a data-fetching library) loading-state
4
+ // pattern / Angular's resource() equivalent.
5
+ enum AsyncStatus { Loading, Success, Failure }
6
+
7
+ class Resource<T> {
8
+ public state<AsyncStatus> Status;
9
+ public state<T?> Data;
10
+ public state<string?> Error;
11
+
12
+ constructor(task<T> operation) {
13
+ this.Status = state(AsyncStatus.Loading);
14
+ this.Data = state(null);
15
+ this.Error = state(null);
16
+ this.Run(operation);
17
+ }
18
+
19
+ private async task Run(task<T> operation) {
20
+ try {
21
+ T result = await operation;
22
+ this.Data.Value = result;
23
+ this.Status.Value = AsyncStatus.Success;
24
+ } catch (string message) {
25
+ this.Error.Value = message;
26
+ this.Status.Value = AsyncStatus.Failure;
27
+ }
28
+ }
29
+ }