kopular 0.2.0 → 0.3.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 +198 -0
- package/README.md +147 -3
- package/assets/logo.png +0 -0
- package/package.json +8 -5
- package/src/directives.js +8 -0
- package/src/directives.ks +30 -0
package/LLM.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Kopular — LLM reference
|
|
2
|
+
|
|
3
|
+
Complete reference for generating correct Kopular code. This is a spec, not a tutorial —
|
|
4
|
+
see `README.md` for narrative/rationale. Kopular is 4 files total; this covers all of
|
|
5
|
+
them. For the host language, see KopScript's own `LLM.md` in the `Kop` repo (or its
|
|
6
|
+
published `LLM.md` on the `kopscript` npm package) — that reference is a prerequisite,
|
|
7
|
+
not repeated here.
|
|
8
|
+
|
|
9
|
+
Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
|
|
10
|
+
`kopular/router` (Router), `kopular/dom` (ambient DOM bindings), `kopular/directives` (If).
|
|
11
|
+
|
|
12
|
+
## Consuming Kopular from your own KopScript project
|
|
13
|
+
|
|
14
|
+
`using` only resolves same-project relative paths — reaching into an npm package (Kopular
|
|
15
|
+
included) always goes through `extern`, re-describing exactly the members you use:
|
|
16
|
+
|
|
17
|
+
```ks
|
|
18
|
+
extern class Component {
|
|
19
|
+
constructor();
|
|
20
|
+
virtual Element Render(); // `virtual` here is what lets your subclass `override` it
|
|
21
|
+
void Mount(Element parent);
|
|
22
|
+
void Update();
|
|
23
|
+
} from "kopular/component";
|
|
24
|
+
|
|
25
|
+
extern class Router {
|
|
26
|
+
constructor(Component notFoundPage);
|
|
27
|
+
void AddRoute(string path, Component page);
|
|
28
|
+
void Navigate(string path);
|
|
29
|
+
void Mount(Element parent);
|
|
30
|
+
} from "kopular/router";
|
|
31
|
+
|
|
32
|
+
extern Element If(bool condition, () => Element whenTrue, () => Element whenFalse) from "kopular/directives";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You also need your own ambient DOM `extern` block (`document`, `Element`, `Event`, ...) —
|
|
36
|
+
Kopular's own copy in `dom.ks` isn't reachable across the package boundary; redeclare the
|
|
37
|
+
handful of members you actually use. See KopularDemo's `src/kopular_bindings.ks` for a
|
|
38
|
+
complete, real example of both.
|
|
39
|
+
|
|
40
|
+
## `Component` (`component.ks`)
|
|
41
|
+
|
|
42
|
+
```ks
|
|
43
|
+
class MyWidget : Component {
|
|
44
|
+
private number count;
|
|
45
|
+
|
|
46
|
+
constructor() : base() {
|
|
47
|
+
this.count = 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public override Element Render() {
|
|
51
|
+
Element el = document.createElement("div");
|
|
52
|
+
el.textContent = "Count: " + this.count;
|
|
53
|
+
return el;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public void Bump() {
|
|
57
|
+
this.count = this.count + 1;
|
|
58
|
+
this.Update(); // re-runs Render(), swaps the old root for the new one
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
MyWidget w = new MyWidget();
|
|
63
|
+
w.Mount(document.body); // first Render() + append
|
|
64
|
+
w.Bump(); // re-render
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `Render()`: `virtual`, override it to build a fresh DOM subtree from current state.
|
|
68
|
+
Called by both `Mount()` and `Update()`. **No template language, no diffing** — every
|
|
69
|
+
call rebuilds the whole subtree from scratch.
|
|
70
|
+
- `Mount(parent)`: calls `Render()` once, appends the result to `parent`, remembers both
|
|
71
|
+
for `Update()` to use later.
|
|
72
|
+
- `Update()` (protected — called from within the component, not externally): calls
|
|
73
|
+
`Render()` again and `replaceChild`s the old root with the new one.
|
|
74
|
+
- **Known limitation**: if a *parent* component's `Update()` runs while it has mounted
|
|
75
|
+
children, those children are NOT automatically re-mounted into the new parent tree —
|
|
76
|
+
`Component` only handles a single component's own re-render cycle, not tree
|
|
77
|
+
reconciliation. Compose independent components into stable slots (see `Router`'s own
|
|
78
|
+
pattern of keeping page instances alive) to avoid this rather than nesting components
|
|
79
|
+
that both re-render.
|
|
80
|
+
|
|
81
|
+
## `Router` (`router.ks`)
|
|
82
|
+
|
|
83
|
+
Real URLs via the History API (`pushState`/`popstate`), not hash routing.
|
|
84
|
+
|
|
85
|
+
```ks
|
|
86
|
+
class NotFoundPage : Component {
|
|
87
|
+
public override Element Render() { /* ... */ }
|
|
88
|
+
}
|
|
89
|
+
class HomePage : Component {
|
|
90
|
+
private Router Nav;
|
|
91
|
+
constructor(Router nav) : base() { this.Nav = nav; }
|
|
92
|
+
public override Element Render() {
|
|
93
|
+
Element btn = document.createElement("button");
|
|
94
|
+
btn.addEventListener("click", (Event e) => { this.Nav.Navigate("/about"); });
|
|
95
|
+
return btn;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Router nav = new Router(new NotFoundPage()); // NotFoundPage REQUIRED up front — no null route
|
|
100
|
+
nav.AddRoute("/", new HomePage(nav)); // pages built once, kept alive for Router's lifetime
|
|
101
|
+
nav.AddRoute("/about", new AboutPage(nav));
|
|
102
|
+
nav.Mount(document.body);
|
|
103
|
+
nav.Navigate("/about"); // pushState + immediate re-render
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Constructor takes the fallback page (`Component`), required — there's no nullable
|
|
107
|
+
"no match" representation.
|
|
108
|
+
- `AddRoute(path, page)` takes an **already-constructed `Component` instance**, not a
|
|
109
|
+
factory — every registered page is built once and stays alive for the Router's whole
|
|
110
|
+
lifetime, so a page's own `state<T>` fields survive navigating away and back.
|
|
111
|
+
- `Navigate(path)` calls `history.pushState` then re-renders immediately. A browser
|
|
112
|
+
back/forward triggers re-render via a `popstate` listener registered in the
|
|
113
|
+
constructor — `Navigate()` itself doesn't rely on that event.
|
|
114
|
+
- Needs a server that falls back to the app shell for any unrecognized path (a plain
|
|
115
|
+
static server has nothing to serve at `/about` on direct load/refresh).
|
|
116
|
+
|
|
117
|
+
## `If` (`directives.ks`) — the `*ngIf` equivalent
|
|
118
|
+
|
|
119
|
+
```ks
|
|
120
|
+
Element If(bool condition, () => Element whenTrue, () => Element whenFalse)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```ks
|
|
124
|
+
root.appendChild(If(this.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLogin()));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Both branches always required (no null "nothing" value); only the branch actually taken
|
|
128
|
+
is called — the other lambda never runs. `*ngFor` and `*ngSwitch` need no Kopular helper
|
|
129
|
+
at all:
|
|
130
|
+
|
|
131
|
+
```ks
|
|
132
|
+
// *ngFor — plain array method
|
|
133
|
+
this.Items.ForEach((Item item) => { list.appendChild(this.BuildItemRow(item)); });
|
|
134
|
+
|
|
135
|
+
// *ngSwitch — plain KopScript `match` expression (exhaustiveness-checked, unlike *ngSwitch)
|
|
136
|
+
root.appendChild(match this.Status {
|
|
137
|
+
"loading" => this.BuildSpinner(),
|
|
138
|
+
"error" => this.BuildError(),
|
|
139
|
+
_ => this.BuildContent()
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
No keyed/reuse-existing-DOM-nodes diffing (the performance angle of `*ngFor trackBy`) —
|
|
144
|
+
that needs comparing old/new data by a caller key, generic over item type, and KopScript
|
|
145
|
+
has no generics. Not planned as a workaround; would need real language-level generics
|
|
146
|
+
first.
|
|
147
|
+
|
|
148
|
+
## Dependency injection — no container, no decorators
|
|
149
|
+
|
|
150
|
+
There is no injector, no `@Injectable`, no provider tokens. "Injecting" a service is
|
|
151
|
+
passing it as a constructor argument — the compiler enforces it (a missing/mistyped
|
|
152
|
+
dependency is a build error). For an app with more than a couple of services, use one
|
|
153
|
+
plain "composition root" class (not a `Component`, never touches the DOM) that builds
|
|
154
|
+
the whole service/page graph exactly once and hands the finished pieces to whatever
|
|
155
|
+
needs them:
|
|
156
|
+
|
|
157
|
+
```ks
|
|
158
|
+
class AppContainer {
|
|
159
|
+
public Router Nav;
|
|
160
|
+
constructor() {
|
|
161
|
+
CounterService counter = new CounterService(); // shared singleton: pass the same
|
|
162
|
+
this.Nav = new Router(new NotFoundPage()); // instance wherever it should be
|
|
163
|
+
this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class RoutedApp : Component {
|
|
168
|
+
private Router Nav;
|
|
169
|
+
constructor(AppContainer services) : base() { this.Nav = services.Nav; }
|
|
170
|
+
public override Element Render() {
|
|
171
|
+
Element el = document.createElement("div");
|
|
172
|
+
this.Nav.Mount(el);
|
|
173
|
+
return el;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
RoutedApp app = new RoutedApp(new AppContainer());
|
|
178
|
+
app.Mount(document.body);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
A service needing its own state/logic is a plain class — no base class, no
|
|
182
|
+
decorators, nothing framework-specific:
|
|
183
|
+
|
|
184
|
+
```ks
|
|
185
|
+
class CounterService {
|
|
186
|
+
public state<number> Count;
|
|
187
|
+
constructor() { this.Count = state(0); }
|
|
188
|
+
public void Increment() { this.Count.Value = this.Count.Value + 1; }
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Does not exist
|
|
193
|
+
|
|
194
|
+
DI container/injector · decorators (`@Injectable`, `@Component`, ...) · a template
|
|
195
|
+
language/DSL — everything is imperative `Render()` code against plain DOM bindings ·
|
|
196
|
+
vdom diffing / reconciliation beyond a single component's own re-render · pipes ·
|
|
197
|
+
animations · forms/validation module · HTTP client · a CLI/scaffolding tool (`ng
|
|
198
|
+
generate`-equivalent) · SSR.
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<img src="https://cdn.jsdelivr.net/npm/kopular@latest/assets/logo.png" width="96" height="96" alt="Kopular logo">
|
|
2
|
+
|
|
1
3
|
# Kopular
|
|
2
4
|
|
|
3
5
|
Kopular is a small component framework for [KopScript](https://dev.azure.com/koppinator/Koppindependence/_git/Kop),
|
|
@@ -5,6 +7,9 @@ built to give Angular's separation of concerns — components own UI, services o
|
|
|
5
7
|
a router owns navigation — without Angular's steepest learning-curve pieces: no RxJS, no
|
|
6
8
|
dependency-injection container, no template DSL.
|
|
7
9
|
|
|
10
|
+
Generating Kopular code with an AI coding assistant? Point it at **[`LLM.md`](./LLM.md)**
|
|
11
|
+
— a dense, complete reference designed to be loaded straight into an LLM's context.
|
|
12
|
+
|
|
8
13
|
## Highlights
|
|
9
14
|
|
|
10
15
|
- **`Component`**: a base class with `virtual Render()` (builds a fresh DOM subtree from
|
|
@@ -24,16 +29,155 @@ dependency-injection container, no template DSL.
|
|
|
24
29
|
DSL. Needs a server that falls back to the app shell for unrecognized paths — see
|
|
25
30
|
[KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)'s
|
|
26
31
|
`scripts/serve.mjs`.
|
|
32
|
+
- **Structural directives, no template DSL**: `*ngIf`/`*ngFor`/`*ngSwitch`'s job — build
|
|
33
|
+
a subtree conditionally, repeat one per item, pick one of several cases — done as plain
|
|
34
|
+
function calls (`If(...)`) and existing KopScript expressions (`array.ForEach(...)`,
|
|
35
|
+
`match`), not special template syntax. See "Structural directives" below.
|
|
27
36
|
|
|
28
37
|
## What's here
|
|
29
38
|
|
|
30
39
|
- `src/dom.ks` — ambient DOM bindings (`document`, `Element`, `Event`, `window`,
|
|
31
|
-
`location`) that `component.ks`/`router.ks` are built on.
|
|
40
|
+
`location`) that `component.ks`/`router.ks`/`directives.ks` are built on.
|
|
32
41
|
- `src/component.ks` — the `Component` base class.
|
|
33
42
|
- `src/router.ks` — the `Router`.
|
|
43
|
+
- `src/directives.ks` — `If()`, the structural-directive equivalents' one genuinely new
|
|
44
|
+
piece (see below).
|
|
45
|
+
|
|
46
|
+
That's the whole framework — four files. Everything else (a real app built on top of it)
|
|
47
|
+
lives in a separate consumer repo, [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo).
|
|
48
|
+
|
|
49
|
+
## Dependency injection: the composition root pattern
|
|
50
|
+
|
|
51
|
+
Kopular has no injector because KopScript has nothing for one to hook into — no
|
|
52
|
+
decorators, no reflection, no generics for a type-safe `Resolve<T>()`. Instead, the
|
|
53
|
+
whole app's service/page graph gets built exactly once, by hand, in one place: a plain
|
|
54
|
+
class with no `Component` base and no framework code in it at all, sometimes called an
|
|
55
|
+
**app container** or (in the wider DI literature) a **composition root**. Everything
|
|
56
|
+
else just takes what it needs as constructor arguments and never constructs its own
|
|
57
|
+
dependencies.
|
|
58
|
+
|
|
59
|
+
```ks
|
|
60
|
+
// app_container.ks — the one place that decides what's shared and builds
|
|
61
|
+
// the graph, in dependency order.
|
|
62
|
+
class AppContainer {
|
|
63
|
+
public Router Nav;
|
|
64
|
+
|
|
65
|
+
constructor() {
|
|
66
|
+
// Built once, passed to every page that needs it below — that's the
|
|
67
|
+
// whole mechanism for a shared singleton. A page that constructed its
|
|
68
|
+
// own `new CounterService()` instead would get an independent one; the
|
|
69
|
+
// difference is which variable gets passed in, not a config flag.
|
|
70
|
+
CounterService counter = new CounterService();
|
|
71
|
+
|
|
72
|
+
this.Nav = new Router(new NotFoundPage());
|
|
73
|
+
this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
|
|
74
|
+
this.Nav.AddRoute("/about", new AboutPage(this.Nav));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// routed_app.ks — the root Component. Takes the already-built graph; never
|
|
79
|
+
// builds one of its own.
|
|
80
|
+
class RoutedApp : Component {
|
|
81
|
+
private Router Nav;
|
|
82
|
+
|
|
83
|
+
constructor(AppContainer services) : base() {
|
|
84
|
+
this.Nav = services.Nav;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public override Element Render() {
|
|
88
|
+
Element container = document.createElement("div");
|
|
89
|
+
this.Nav.Mount(container);
|
|
90
|
+
return container;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
RoutedApp app = new RoutedApp(new AppContainer());
|
|
95
|
+
app.Mount(document.body);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This is sometimes called "Pure DI" — the same benefits a container gives you (nothing
|
|
99
|
+
hardcodes its own dependencies, everything is swappable in a test) with none of a
|
|
100
|
+
container's cost:
|
|
101
|
+
|
|
102
|
+
- **Compile-time checked.** A missing or mistyped dependency is `expected N arguments,
|
|
103
|
+
got M` from the KopScript compiler, not a `NullInjectorError` your users hit at
|
|
104
|
+
runtime after a container fails to resolve something.
|
|
105
|
+
- **Fully legible.** The entire dependency graph is ordinary, readable code in one file
|
|
106
|
+
— grep for `new` in the composition root and you've read the whole wiring diagram.
|
|
107
|
+
Nothing is constructed by a framework inspecting metadata behind the scenes.
|
|
108
|
+
- **No new concepts.** If you already know how to call a constructor, you already know
|
|
109
|
+
Kopular's DI story — there's no separate injector API, provider syntax, or
|
|
110
|
+
injection-token vocabulary to learn.
|
|
111
|
+
|
|
112
|
+
See [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)'s
|
|
113
|
+
`src/app_container.ks` and `src/routed_app.ks` for the real, working version this
|
|
114
|
+
example is drawn from.
|
|
115
|
+
|
|
116
|
+
## Structural directives
|
|
117
|
+
|
|
118
|
+
Angular's `*ngIf`/`*ngFor`/`*ngSwitch` are template syntax that expands, at compile time,
|
|
119
|
+
into imperative view-container calls. Kopular has no template compiler to expand
|
|
120
|
+
anything into (`Render()` is already imperative — see `component.ks`), so there's no
|
|
121
|
+
special syntax here either: each one maps onto a plain expression, and two of the three
|
|
122
|
+
need nothing new at all.
|
|
123
|
+
|
|
124
|
+
| Angular | Kopular | New code? |
|
|
125
|
+
| ----------------- | ------------------------------------------- | :-------: |
|
|
126
|
+
| `*ngFor` | `array.ForEach((item) => ...)` | none — already a KopScript array method |
|
|
127
|
+
| `*ngSwitch` | `match value { ... }` | none — already a KopScript expression, and exhaustiveness-checked (`*ngSwitch` isn't) |
|
|
128
|
+
| `*ngIf` / `*ngIf-else` | `If(condition, () => ..., () => ...)` | `directives.ks` |
|
|
129
|
+
|
|
130
|
+
`*ngIf` is the one case that needs something new: `if` is a *statement* in KopScript, so
|
|
131
|
+
without a helper you'd need a throwaway mutable local just to get a conditional value out
|
|
132
|
+
of it. `If()` is that helper — nothing more than:
|
|
133
|
+
|
|
134
|
+
```ks
|
|
135
|
+
Element If(bool condition, () => Element whenTrue, () => Element whenFalse) {
|
|
136
|
+
if (condition) {
|
|
137
|
+
return whenTrue();
|
|
138
|
+
}
|
|
139
|
+
return whenFalse();
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Both branches are required (same reasoning `Router` uses for requiring a `NotFoundPage`
|
|
144
|
+
up front — see `router.ks`): v1 has no nullable types, so "render nothing" has no value
|
|
145
|
+
to hand back. Only the branch actually taken runs — the other lambda is never called, so
|
|
146
|
+
an explicit empty branch (`() => document.createElement("span")`) costs nothing when
|
|
147
|
+
there's genuinely nothing to show.
|
|
148
|
+
|
|
149
|
+
All three read the same way, right inside `Render()` — no template file, no directive
|
|
150
|
+
registration, nothing to import beyond the function itself:
|
|
151
|
+
|
|
152
|
+
```ks
|
|
153
|
+
public override Element Render() {
|
|
154
|
+
Element root = document.createElement("div");
|
|
155
|
+
|
|
156
|
+
// *ngIf
|
|
157
|
+
root.appendChild(If(this.User.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLoginButton()));
|
|
158
|
+
|
|
159
|
+
// *ngFor
|
|
160
|
+
Element list = document.createElement("ul");
|
|
161
|
+
this.Items.ForEach((Item item) => { list.appendChild(this.BuildItemRow(item)); });
|
|
162
|
+
root.appendChild(list);
|
|
163
|
+
|
|
164
|
+
// *ngSwitch
|
|
165
|
+
root.appendChild(match this.Status {
|
|
166
|
+
"loading" => this.BuildSpinner(),
|
|
167
|
+
"error" => this.BuildError(),
|
|
168
|
+
_ => this.BuildContent()
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return root;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
34
174
|
|
|
35
|
-
|
|
36
|
-
|
|
175
|
+
Why not a general reuse-existing-DOM-nodes diffing layer, the way `*ngFor trackBy`
|
|
176
|
+
avoids rebuilding unchanged rows? That needs comparing old and new *data* items by a
|
|
177
|
+
caller-supplied key, generic over the item type — and KopScript has no generics (no
|
|
178
|
+
`class Foo<T>`, no `T Resolve<T>()`). A one-off keyed-diff helper could be hand-written
|
|
179
|
+
per list, but that's real vdom-diffing work — already called out as out of scope in
|
|
180
|
+
"Status" below, and not something these three lines take on.
|
|
37
181
|
|
|
38
182
|
## Using Kopular from another KopScript project
|
|
39
183
|
|
package/assets/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, and routing, with no template DSL and no DI container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,19 +20,22 @@
|
|
|
20
20
|
".": "./src/component.js",
|
|
21
21
|
"./component": "./src/component.js",
|
|
22
22
|
"./router": "./src/router.js",
|
|
23
|
-
"./dom": "./src/dom.js"
|
|
23
|
+
"./dom": "./src/dom.js",
|
|
24
|
+
"./directives": "./src/directives.js"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
|
-
"src"
|
|
27
|
+
"src",
|
|
28
|
+
"assets",
|
|
29
|
+
"LLM.md"
|
|
27
30
|
],
|
|
28
31
|
"scripts": {
|
|
29
|
-
"build": "ks build src/router.ks",
|
|
32
|
+
"build": "ks build src/router.ks && ks build src/directives.ks",
|
|
30
33
|
"prepublishOnly": "npm run build",
|
|
31
34
|
"test": "vitest run",
|
|
32
35
|
"test:watch": "vitest"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
|
-
"kopscript": "^0.
|
|
38
|
+
"kopscript": "^0.2.0",
|
|
36
39
|
"@types/jsdom": "^30.0.0",
|
|
37
40
|
"@types/node": "^20.14.0",
|
|
38
41
|
"jsdom": "^25.0.1",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
using "./dom";
|
|
2
|
+
|
|
3
|
+
// Kopular's structural-directive equivalents — see the README's
|
|
4
|
+
// "Structural directives" section for the full *ngIf/*ngFor/*ngSwitch
|
|
5
|
+
// mapping. There's no template language here (Kopular doesn't have one, by
|
|
6
|
+
// design — see component.ks), so these are just plain functions: call them
|
|
7
|
+
// like any other expression from inside Render(), the same way you'd call
|
|
8
|
+
// document.createElement.
|
|
9
|
+
//
|
|
10
|
+
// *ngFor and *ngSwitch need nothing new — `array.ForEach(...)` and
|
|
11
|
+
// KopScript's own `match` expression already cover them (and `match` is
|
|
12
|
+
// exhaustiveness-checked, which *ngSwitch isn't). `If` below is the one
|
|
13
|
+
// piece the language doesn't already give you as an expression: `if` is a
|
|
14
|
+
// statement in KopScript, so without this you'd need a throwaway mutable
|
|
15
|
+
// local to get a conditional value.
|
|
16
|
+
|
|
17
|
+
// The *ngIf equivalent — conditionally build one of two subtrees, as an
|
|
18
|
+
// expression. Both branches are required: v1 has no nullable types, so
|
|
19
|
+
// "render nothing" has no value to return — the same reasoning Router uses
|
|
20
|
+
// for requiring a NotFoundPage up front (see router.ks) rather than letting
|
|
21
|
+
// "no match" be null. Only the branch actually taken runs; the other
|
|
22
|
+
// lambda is never called, so an explicit empty branch (e.g.
|
|
23
|
+
// `() => document.createElement("span")`) costs nothing when there's
|
|
24
|
+
// genuinely nothing to show.
|
|
25
|
+
Element If(bool condition, () => Element whenTrue, () => Element whenFalse) {
|
|
26
|
+
if (condition) {
|
|
27
|
+
return whenTrue();
|
|
28
|
+
}
|
|
29
|
+
return whenFalse();
|
|
30
|
+
}
|