kopular 1.0.0 → 1.1.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 +238 -0
- package/LLM.md +97 -30
- package/README.md +41 -47
- package/bin/kp.mjs +44 -149
- package/package.json +5 -2
- package/src/dom.js.map +1 -1
- package/src/dom.ks +3 -0
- package/src/forms.js +4 -4
- package/src/kopular.ks +179 -0
- package/src/router.js +3 -3
- package/src/testing.js +45 -7
- package/src/timers.js +5 -0
- package/src/vdom.js +30 -13
- package/src/vdom.js.map +1 -1
- package/src/vdom.ks +21 -0
- package/src/velement.js +2 -0
- package/src/velement.js.map +1 -1
- package/src/velement.ks +6 -0
package/GUIDE.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Kopular quick guide
|
|
2
|
+
|
|
3
|
+
Everything you need for typical app work, in one page. For anything not covered here —
|
|
4
|
+
generics, nullable types, advanced routing, the full diagnostic list — see this package's
|
|
5
|
+
own `LLM.md` and `node_modules/kopscript/LLM.md`.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Every file starts with `using "kopular";` for the DOM and every Kopular type, plus
|
|
10
|
+
`using "./other_file";` per project file it needs (`using` isn't transitive — list every
|
|
11
|
+
file you reference, not just direct dependencies' dependencies).
|
|
12
|
+
|
|
13
|
+
```ks
|
|
14
|
+
using "kopular";
|
|
15
|
+
using "./counter";
|
|
16
|
+
|
|
17
|
+
Counter app = new Counter();
|
|
18
|
+
app.Mount(document.body);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run `npm run build && npm run serve`, or `npm start` (a scaffolded project already has
|
|
22
|
+
both scripts). `ks check src/app.ks` type-checks without building, if you just want errors.
|
|
23
|
+
|
|
24
|
+
## The language you need
|
|
25
|
+
|
|
26
|
+
- `Type name = value;` for every local — no `let`/`var`, no inference on declarations.
|
|
27
|
+
- No implicit `this` — every member reference is `this.Field`/`this.Method()`, always,
|
|
28
|
+
including inside a lambda.
|
|
29
|
+
- `if`/`else`/`while`/`for`/`foreach (Type x in xs)` — all statements, no ternary. Use
|
|
30
|
+
`match` for a conditional value: `match x { 1 => "one", _ => "other" }` (the `_` arm is
|
|
31
|
+
required unless every case is covered, e.g. an enum).
|
|
32
|
+
- Lambdas need explicit parameter types: `(number x) => x * 2`.
|
|
33
|
+
- Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
|
|
34
|
+
narrowing is scoped to that `if` block, not reachability-based (an early
|
|
35
|
+
`if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
|
|
36
|
+
`if (x != null) { ... }` instead).
|
|
37
|
+
- Strings: `s.Length`, `.Contains()/.StartsWith()/.EndsWith()`, `.Trim()/.ToUpper()/.ToLower()`,
|
|
38
|
+
`.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (sort comparator), `+` concatenates.
|
|
39
|
+
Both `"double"` and `'single'` quotes work — use whichever the other isn't, e.g. inside
|
|
40
|
+
a template attribute (`*if="Name != ''"`).
|
|
41
|
+
- Arrays: `xs.Length`, `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
|
|
42
|
+
`.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating**, return a new array),
|
|
43
|
+
`.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
|
|
44
|
+
- `x == null` / `x != null` also match `undefined` — a missed `Array.Find`, an absent
|
|
45
|
+
optional field, etc. all read as `null`.
|
|
46
|
+
- No object-literal syntax anywhere (`{ key: value }` doesn't exist as a value). A JS API
|
|
47
|
+
that needs one (rare — `fetch`'s options, `addEventListener`'s options object) needs a
|
|
48
|
+
small hand-written `.js` shim; Kopular's own `Http`/`FormField` cover the common cases so
|
|
49
|
+
you'll rarely hit this.
|
|
50
|
+
|
|
51
|
+
## Components
|
|
52
|
+
|
|
53
|
+
```ks
|
|
54
|
+
class Counter : Component {
|
|
55
|
+
public state<number> Count;
|
|
56
|
+
constructor() : base() { this.Count = state(0); }
|
|
57
|
+
public void Increment() { this.Count.Value = this.Count.Value + 1; }
|
|
58
|
+
template from "./counter.html";
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
```html
|
|
62
|
+
<button (click)="Increment()">Count: {{ Count.Value }}</button>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`state<T>` referenced directly in a template auto-subscribes — no manual `Subscribe`
|
|
66
|
+
needed. State reached *indirectly* (through a method, or `this.SomeService.Count`) does
|
|
67
|
+
need one: `this.SomeService.Count.Subscribe((v) => this.Update());` in the constructor.
|
|
68
|
+
|
|
69
|
+
**Template bindings**: `{{ expr }}` text, `[prop]="expr"` (real fields for
|
|
70
|
+
`id`/`className`/`value`/`disabled`/`checked`; anything else is a plain attribute),
|
|
71
|
+
`(click)`/`(input)`/`(blur)`/`(change)` events, `[(value)]="Field"` two-way binding
|
|
72
|
+
(`Field` can be a bare name, `this.Field`, or a path like `Qty.Value` — not a method call),
|
|
73
|
+
`*if="expr"`, `*for="Type v of expr"` (element type required, no inference), `*mount="expr"`
|
|
74
|
+
(embeds a live child component, composes with `*for`).
|
|
75
|
+
|
|
76
|
+
**Hand-written `Render()`** (needed when logic is too dynamic for a template, or a template
|
|
77
|
+
would obscure more than it clarifies):
|
|
78
|
+
|
|
79
|
+
```ks
|
|
80
|
+
public override VElement Render() {
|
|
81
|
+
VElement button = VElement.Create("button");
|
|
82
|
+
button.TextContent = "Count: " + this.Count.Value;
|
|
83
|
+
button.OnClick = (Event e) => { this.Increment(); };
|
|
84
|
+
return button;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
`VElement.Create(tag)`, `.TextContent/.ClassName/.Id/.Value/.RawHtml/.Disabled/.Checked`,
|
|
88
|
+
`.OnClick/.OnInput/.OnBlur/.OnChange`, `.AppendChild(child)`, `.SetAttr(name, value)` (the
|
|
89
|
+
escape hatch for anything without a named field). Both styles compile to the same thing and
|
|
90
|
+
mix freely across a project.
|
|
91
|
+
|
|
92
|
+
**Error boundary**: override `RenderError(string message)` to show a fallback instead of an
|
|
93
|
+
uncaught crash if `Render()` throws.
|
|
94
|
+
|
|
95
|
+
## Nested components & content projection
|
|
96
|
+
|
|
97
|
+
```ks
|
|
98
|
+
this.Items.ForEach((Item item) => {
|
|
99
|
+
VElement slot = VElement.Mount(item); // item : Component, e.g. its own `public string Id;`
|
|
100
|
+
slot.Id = item.Id; // stable key for reordering
|
|
101
|
+
list.AppendChild(slot);
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
A template does the same via `<li *for="Item i of Items" *mount="i"></li>`.
|
|
105
|
+
|
|
106
|
+
Content projection (React's `children`): pass a `() => VElement` into a constructor, call
|
|
107
|
+
it from `Render()` — no separate mechanism needed.
|
|
108
|
+
|
|
109
|
+
## Services — no DI container
|
|
110
|
+
|
|
111
|
+
A service is a plain class; "injecting" it is a constructor argument. Wire everything once
|
|
112
|
+
in a composition root:
|
|
113
|
+
|
|
114
|
+
```ks
|
|
115
|
+
class AppContainer {
|
|
116
|
+
public Router Nav;
|
|
117
|
+
constructor() {
|
|
118
|
+
CounterService counter = new CounterService();
|
|
119
|
+
this.Nav = new Router(new NotFoundPage());
|
|
120
|
+
this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Router
|
|
126
|
+
|
|
127
|
+
```ks
|
|
128
|
+
Router nav = new Router(new NotFoundPage()); // fallback page, required
|
|
129
|
+
nav.AddRoute("/", new HomePage(nav));
|
|
130
|
+
nav.AddRoute("/dogs/:id", new DogDetailPage(nav)); // nav.Param inside that page
|
|
131
|
+
nav.SetGuard("/login", (string path) => {
|
|
132
|
+
if (path == "/admin") { return authService.LoggedIn; }
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
nav.Navigate("/dogs/1");
|
|
136
|
+
nav.Mount(document.body);
|
|
137
|
+
```
|
|
138
|
+
- Routes hold already-built `Component`s, not factories — built once, state survives
|
|
139
|
+
navigating away and back.
|
|
140
|
+
- `nav.Param` — the first `:name` segment, a plain `string` (not `state<T>`; the outlet
|
|
141
|
+
re-renders on every navigation already).
|
|
142
|
+
- `SetGuard(redirectPath, guard)`: one guard for the whole router; `guard` returns `false`
|
|
143
|
+
to redirect. Defaults to always-allow.
|
|
144
|
+
- `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
|
|
145
|
+
- Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
|
|
146
|
+
matching file) — a plain HTTP limitation, not a Kopular one.
|
|
147
|
+
|
|
148
|
+
## Forms
|
|
149
|
+
|
|
150
|
+
```ks
|
|
151
|
+
FormField<string> email = new FormField<string>("", (string v) => {
|
|
152
|
+
string? required = Validators.Required(v);
|
|
153
|
+
if (required != null) { return required; }
|
|
154
|
+
return Validators.Email(v);
|
|
155
|
+
});
|
|
156
|
+
email.Value.Value = "not-an-email";
|
|
157
|
+
print(email.Error.Value); // "Must be a valid email"
|
|
158
|
+
```
|
|
159
|
+
`Validators`: `Required/MinLength/MaxLength/Email/Min/Max`, each returns an error message
|
|
160
|
+
or `null`. No array-of-validators param — chain checks in one lambda, as above.
|
|
161
|
+
|
|
162
|
+
## HTTP
|
|
163
|
+
|
|
164
|
+
```ks
|
|
165
|
+
Response r = await Http.Get("/api/dogs");
|
|
166
|
+
if (r.ok) { string body = await r.text(); }
|
|
167
|
+
await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
|
|
168
|
+
```
|
|
169
|
+
No typed JSON deserialization built in — describe the shape as `extern class` and parse
|
|
170
|
+
with `extern MyShape Parse(string json) as "JSON.parse";`.
|
|
171
|
+
|
|
172
|
+
## Async: `Delay`, `Computed`, `Resource<T>`
|
|
173
|
+
|
|
174
|
+
```ks
|
|
175
|
+
await Delay(500); // real setTimeout-backed delay
|
|
176
|
+
|
|
177
|
+
Computed2<number, number, number> total = new Computed2<number, number, number>(
|
|
178
|
+
qty, price, (number q, number p) => q * p
|
|
179
|
+
);
|
|
180
|
+
total.Value.Subscribe((number v) => this.Update()); // total.Value is itself state<number>
|
|
181
|
+
|
|
182
|
+
Resource<Response> r = new Resource<Response>(Http.Get(url)); // task already in flight
|
|
183
|
+
match r.Status.Value {
|
|
184
|
+
AsyncStatus.Loading => BuildSpinner(),
|
|
185
|
+
AsyncStatus.Success => BuildContent(r.Data.Value),
|
|
186
|
+
AsyncStatus.Failure => BuildError(r.Error.Value)
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Scoped styles
|
|
191
|
+
|
|
192
|
+
```ks
|
|
193
|
+
class Widget : Component {
|
|
194
|
+
template from "./widget.html";
|
|
195
|
+
styles from "./widget.css";
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
Every selector in `widget.css` is rewritten to only match this class's own elements —
|
|
199
|
+
never a sibling's or child's. Needs a constructor to exist on the class.
|
|
200
|
+
|
|
201
|
+
## Lazy routes (real code-splitting)
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
// admin_page_loader.js — hand-written, not compiled from .ks
|
|
205
|
+
export async function LoadAdminPage() {
|
|
206
|
+
const { AdminPage } = await import("./admin_page.js");
|
|
207
|
+
return new AdminPage();
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
```ks
|
|
211
|
+
extern task<Component> LoadAdminPage() from "./admin_page_loader";
|
|
212
|
+
nav.AddLazyRoute("/admin", LoadAdminPage);
|
|
213
|
+
```
|
|
214
|
+
The lazy page (`admin_page.ks`) must be built as its own entry too — it's deliberately not
|
|
215
|
+
`using`'d from your app's entry (that's what keeps it out of the eager bundle), so add a
|
|
216
|
+
second build line: `ks build src/app.ks && ks build src/admin_page.ks`. Testing it via
|
|
217
|
+
`kopular/testing`'s `runKopularApp` needs no extra setup — it compiles any file in your
|
|
218
|
+
`srcDir` the entry doesn't reach.
|
|
219
|
+
|
|
220
|
+
## Testing
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import { runKopularApp } from "kopular/testing";
|
|
224
|
+
const { window, cleanup } = await runKopularApp(srcDir, "app.ks", { includeKopularPackage: true });
|
|
225
|
+
// assert against window.document, then:
|
|
226
|
+
cleanup();
|
|
227
|
+
```
|
|
228
|
+
Real jsdom, real compile, real DOM assertions — not a mock.
|
|
229
|
+
|
|
230
|
+
## Common mistakes
|
|
231
|
+
|
|
232
|
+
- Forgetting `this.` on a member reference inside a lambda — it's a real undefined-identifier
|
|
233
|
+
error, not automatic.
|
|
234
|
+
- `if (x == null) { return; } use(x);` does NOT narrow `x` — see "Nullable" above.
|
|
235
|
+
- A two-way binding target must be a field path (`Field`, `this.Field`, `Field.Value`), never
|
|
236
|
+
a method call.
|
|
237
|
+
- `*mount` accepts only `id`/`[id]` on its element — any other attr/binding is a compile
|
|
238
|
+
error, since the mounted child's own `Render()` owns all of its content.
|
package/LLM.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
see `README.md` for narrative/rationale, or this package's own **`GUIDE.md`** for a short,
|
|
5
|
+
task-oriented page covering everything a typical app needs (start there for most tasks;
|
|
6
|
+
come back here for anything it doesn't cover). Kopular is 10 files total; this covers all
|
|
7
|
+
of them. For the host language, see KopScript's own `LLM.md` in the `KopScript` repo (or
|
|
8
|
+
its published `LLM.md` on the `kopscript` npm package, `kopscript@1.1.0`+) — that
|
|
9
|
+
reference is a prerequisite, not repeated here.
|
|
8
10
|
|
|
9
11
|
Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
|
|
10
12
|
`kopular/velement` (VElement — what `Render()` returns; see "Component" below),
|
|
@@ -13,30 +15,50 @@ Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Compo
|
|
|
13
15
|
(Computed1, Computed2), `kopular/resource` (Resource, AsyncStatus), `kopular/vdom`
|
|
14
16
|
(`ScopedStyles` — the runtime half of `styles from`; everything else in this file, the
|
|
15
17
|
diff/patch engine behind `Update()`, is internal, nothing else here needs importing
|
|
16
|
-
directly), `kopular/testing` (runKopularApp,
|
|
17
|
-
bin, `kp` — `npx kp new
|
|
18
|
-
|
|
19
|
-
scripts needed to run in a browser)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
there is no `kopular/template` entry point to import.
|
|
18
|
+
directly), `kopular/timers` (`Delay`), `kopular/testing` (runKopularApp,
|
|
19
|
+
runKopularFixture — see below). Also ships a bin, `kp` — `npx kp new <dir>` scaffolds a
|
|
20
|
+
new project (an `index.html` with every module already wired, a `Counter` component, and
|
|
21
|
+
the vendor/serve scripts needed to run in a browser). Templates (`template from
|
|
22
|
+
"./x.html";`, see below) are a KopScript language feature, not a Kopular export — there is
|
|
23
|
+
no `kopular/template` entry point to import.
|
|
23
24
|
|
|
24
25
|
## Consuming Kopular from your own KopScript project
|
|
25
26
|
|
|
26
|
-
`using`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
`using "kopular";` (`kopscript@1.1.0`+) resolves this package's own declarations —
|
|
28
|
+
everything in the "Entry points" list above — through `node_modules`, the same way `npm
|
|
29
|
+
install` already makes them available to plain JS. One line, every Kopular type in scope,
|
|
30
|
+
each consuming file's own compiled output importing only the bindings it actually
|
|
31
|
+
references (never the whole surface). This is the normal way to consume Kopular now; `kp
|
|
32
|
+
new` generates it by default.
|
|
33
|
+
|
|
34
|
+
```ks
|
|
35
|
+
using "kopular";
|
|
36
|
+
using "./counter";
|
|
37
|
+
|
|
38
|
+
Counter app = new Counter();
|
|
39
|
+
app.Mount(document.body);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`document`/`Element`/`Event`/etc. come from the same `using "kopular";` — no separate ambient
|
|
43
|
+
block needed. The rest of this file describes every one of those bindings' real shapes, for
|
|
44
|
+
when you need something `GUIDE.md` doesn't cover, or need to know exactly what a type looks
|
|
45
|
+
like.
|
|
46
|
+
|
|
47
|
+
### The hand-copied `extern` block (older projects, or trimming what you import)
|
|
48
|
+
|
|
49
|
+
Before `kopscript@1.1.0`, every consuming project hand-declared its own copy of Kopular's
|
|
50
|
+
`extern` bindings (`using` couldn't reach into a package). That still works — a real,
|
|
51
|
+
hand-written `extern` block is exactly as valid as one resolved via `using "kopular";",
|
|
52
|
+
just more to maintain — and is occasionally still the right call if you want a visibly
|
|
53
|
+
trimmed subset. **Don't reach for `Element`/`Document`/`Event` via `extern ... from
|
|
54
|
+
"kopular/dom";`, even though that actually works** (`kopular/dom` genuinely re-exports real
|
|
55
|
+
`globalThis` bindings, same as any other Kopular export) — Kopular's own internal `dom.ks`
|
|
56
|
+
only declares what Kopular's own framework code itself touches, a smaller surface than a
|
|
57
|
+
real app typically needs. `using "kopular";`'s own declarations (`src/kopular.ks` in this
|
|
58
|
+
package) don't have that problem — they're written for consumers, not just for Kopular's
|
|
59
|
+
own internals — so prefer it over a hand-copied ambient block for new code. If you do
|
|
60
|
+
hand-copy one anyway, copy the block below rather than hand-rolling a smaller one from
|
|
61
|
+
scratch and adding members as compile errors demand them — a real, complete first-attempt
|
|
40
62
|
implementation of a Kopular app hit the exact same missing property (`Element.value`)
|
|
41
63
|
twice from two independently-trimmed subsets, because the compile error only ever names
|
|
42
64
|
the one member actually touched, never warns that a *sibling* feature (a template's
|
|
@@ -95,6 +117,10 @@ extern class VElement {
|
|
|
95
117
|
string Id { get; set; }
|
|
96
118
|
string Value { get; set; }
|
|
97
119
|
string RawHtml { get; set; }
|
|
120
|
+
// Real bool properties, not attribute strings — a boolean attribute is on
|
|
121
|
+
// whenever it's present at all, so only a real property can turn it back off.
|
|
122
|
+
bool Disabled { get; set; }
|
|
123
|
+
bool Checked { get; set; }
|
|
98
124
|
(Event) => void OnClick { get; set; }
|
|
99
125
|
(Event) => void OnInput { get; set; }
|
|
100
126
|
(Event) => void OnBlur { get; set; }
|
|
@@ -109,6 +135,14 @@ extern class VElement {
|
|
|
109
135
|
static VElement Mount(Component component);
|
|
110
136
|
} from "kopular/velement";
|
|
111
137
|
|
|
138
|
+
// Only needed if you use `styles from "./x.css";` in a class body (see
|
|
139
|
+
// "Scoped styles" below) — the compiler splices a call to this into the
|
|
140
|
+
// constructor automatically, but (like every other Kopular export) it
|
|
141
|
+
// still needs its own `extern` declaration; it is NOT auto-imported.
|
|
142
|
+
extern class ScopedStyles {
|
|
143
|
+
static void Inject(string id, string css);
|
|
144
|
+
} from "kopular/vdom";
|
|
145
|
+
|
|
112
146
|
extern class Component {
|
|
113
147
|
constructor();
|
|
114
148
|
virtual VElement Render(); // `virtual` here is what lets your subclass `override` it
|
|
@@ -133,6 +167,11 @@ extern class Router {
|
|
|
133
167
|
// Every registered path (AddRoute + AddLazyRoute), in registration
|
|
134
168
|
// order — see "Router" below.
|
|
135
169
|
string[] AllPaths();
|
|
170
|
+
// Overridable outlet content shown while an AddLazyRoute page's loader
|
|
171
|
+
// is in flight — default: `<div class="router-loading">Loading...</div>`.
|
|
172
|
+
// Only declare this if you actually override it (a class extending
|
|
173
|
+
// Router) — see "Lazy routes" below.
|
|
174
|
+
protected virtual VElement BuildLoadingPlaceholder();
|
|
136
175
|
void Navigate(string path);
|
|
137
176
|
void Mount(Element parent);
|
|
138
177
|
// One guard for the whole Router, not per-route — see "Router" below.
|
|
@@ -317,6 +356,11 @@ w.Bump(); // re-render + diff + patch
|
|
|
317
356
|
return ul;
|
|
318
357
|
}
|
|
319
358
|
```
|
|
359
|
+
`item.Id` above means exactly what it looks like: for a keyed list of mounted children,
|
|
360
|
+
the mounted `Component` itself needs its own public `Id` (or similarly-named) field/
|
|
361
|
+
property to copy onto `slot.Id` — `VElement.Id` lives on the wrapper slot, not on the
|
|
362
|
+
component, so there's nothing to key by without one. Set it however suits the type
|
|
363
|
+
(constructor param, or a plain field assigned right after construction).
|
|
320
364
|
The SAME `Mountable` instance still in a slot across a re-render is patched in place
|
|
321
365
|
(`Update()` inside that child re-renders just its own subtree, siblings untouched); a
|
|
322
366
|
DIFFERENT instance (or the slot disappearing) tears the old one down first — calling its
|
|
@@ -434,7 +478,11 @@ class Widget : Component {
|
|
|
434
478
|
- Independent of `template from` — works with a hand-written `Render()` too. The compiler
|
|
435
479
|
rewrites the referenced `.css` so every selector requires a per-class
|
|
436
480
|
`data-kop-scope="<id>"` attribute, then splices one `ScopedStyles.Inject(id, css);` call
|
|
437
|
-
into the constructor. **
|
|
481
|
+
into the constructor. **This means `ScopedStyles` must be `extern`-declared in your own
|
|
482
|
+
project the same as `Component`/`VElement`/etc. (see the copy-paste block above) — it is
|
|
483
|
+
NOT auto-imported just because you wrote `styles from`.** Forgetting it is a real `KS4048
|
|
484
|
+
Undefined identifier 'ScopedStyles'` at the constructor the compiler spliced the call
|
|
485
|
+
into. **Unlike `template from`, this needed real framework code**:
|
|
438
486
|
`ScopedStyles` (`vdom.ks`, new) — idempotent, static-array-registry dedup shape same as
|
|
439
487
|
`Batching`, injects one real `<style>` per component *type* (not per instance) into
|
|
440
488
|
`document.head` (`dom.ks`'s `head { get; }`, new) the first time any instance is
|
|
@@ -532,10 +580,29 @@ nav.Navigate("/about"); // pushState + immediate re-ren
|
|
|
532
580
|
extern task<Component> LoadDogsPage() from "./dogs_page_loader";
|
|
533
581
|
nav.AddLazyRoute("/dogs", LoadDogsPage);
|
|
534
582
|
```
|
|
535
|
-
The outlet shows a plain loading placeholder (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
the same
|
|
583
|
+
The outlet shows a plain loading placeholder (`<div class="router-loading">Loading...</div>`
|
|
584
|
+
by default; override `protected virtual VElement BuildLoadingPlaceholder()` on a class
|
|
585
|
+
extending `Router` to customize) while the fetch is in flight; the loaded page is cached
|
|
586
|
+
after the first fetch, same as an eager page — navigating away and back reuses it, no
|
|
587
|
+
re-fetch. Mixes freely with `AddRoute` in the same `Router`.
|
|
588
|
+
- **Building it for real**: the whole point of a lazy route is that `dogs_page.ks` is
|
|
589
|
+
deliberately *not* `using`'d from your app's own entry file — that's what keeps its
|
|
590
|
+
code out of the eager bundle. That also means your normal build command (`ks build
|
|
591
|
+
src/app.ks`) never compiles it, since `ks build`/`compileGraph` only walk the `using`
|
|
592
|
+
graph reachable from the entry you give them. Build the lazy page as its own separate
|
|
593
|
+
entry too, e.g. `ks build src/app.ks && ks build src/dogs_page.ks` in your build
|
|
594
|
+
script (or one `ks build` invocation per lazy route, if you have several) — each
|
|
595
|
+
already-compiled shared dependency just gets written again with identical output, so
|
|
596
|
+
this is safe to add without restructuring anything else.
|
|
597
|
+
- **Testing it**: `kopular/testing`'s `runKopularApp` (kopular 0.24.0+) compiles any
|
|
598
|
+
other real `.ks` file present in your app's own `srcDir` that the entry doesn't
|
|
599
|
+
reach — a lazy-route target is exactly that — so a test exercising `AddLazyRoute`
|
|
600
|
+
needs no special setup: write `dogs_page.ks`/`dogs_page_loader.js` into the same
|
|
601
|
+
directory as your entry file, same as any other page, and the loader's
|
|
602
|
+
`import("./dogs_page.js")` finds a real, freshly-compiled file. (`runKopularFixture`,
|
|
603
|
+
used only by Kopular's own internal test suite, does not do this — it copies
|
|
604
|
+
Kopular's entire framework source tree alongside a fixture, where the same behavior
|
|
605
|
+
would mean recompiling most of the framework on every test.)
|
|
539
606
|
- **`AllPaths(): string[]`** — every registered path (`AddRoute` + `AddLazyRoute`), in
|
|
540
607
|
registration order. For enumerating real routes (a build-time prerender step, most
|
|
541
608
|
likely) without a second, hand-maintained list.
|
package/README.md
CHANGED
|
@@ -166,8 +166,13 @@ class Widget : Component {
|
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
Unlike `template from`, this **did** need real framework code — `ScopedStyles.Inject`
|
|
169
|
-
(`vdom.ks`) is the runtime half
|
|
170
|
-
|
|
169
|
+
(`vdom.ks`) is the runtime half, and (like every other Kopular export) it needs its own
|
|
170
|
+
`extern` declaration in your project — `extern class ScopedStyles { static void
|
|
171
|
+
Inject(string id, string css); } from "kopular/vdom";` — it is not auto-imported just
|
|
172
|
+
because you wrote `styles from`; omitting it is a real `KS4048 Undefined identifier
|
|
173
|
+
'ScopedStyles'` at the constructor the compiler spliced the call into. It's idempotent,
|
|
174
|
+
injecting one real `<style>` per component *type* into `document.head` the first time any
|
|
175
|
+
instance of that type is constructed
|
|
171
176
|
(dedup is per-type, not per-instance — every instance's constructor calls `Inject` with
|
|
172
177
|
the same compile-time `id`/rewritten-`css`, so only the first actually creates a tag).
|
|
173
178
|
Never removed once injected — a scoped stylesheet is global infrastructure for as long as
|
|
@@ -368,11 +373,23 @@ export async function LoadDogsPage() {
|
|
|
368
373
|
extern task<Component> LoadDogsPage() from "./dogs_page_loader";
|
|
369
374
|
```
|
|
370
375
|
|
|
371
|
-
The outlet shows a plain loading placeholder (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
+
The outlet shows a plain loading placeholder (`<div class="router-loading">Loading...</div>`
|
|
377
|
+
by default; override `protected virtual VElement BuildLoadingPlaceholder()` on a class
|
|
378
|
+
extending `Router` to customize it) while the fetch is in flight, then the real page once
|
|
379
|
+
it resolves — and, like an eager page, it's only ever fetched once: navigating away and
|
|
380
|
+
back reuses the same already-loaded instance, keeping whatever state it built up. A lazy
|
|
381
|
+
and an eager route mix freely in the same `Router`; nothing about `AddRoute`'s own existing
|
|
382
|
+
signature changes.
|
|
383
|
+
|
|
384
|
+
**Building and testing a lazy route**: `dogs_page.ks` is deliberately *not* `using`'d from
|
|
385
|
+
your app's own entry — that's what keeps it out of the eager bundle — which also means your
|
|
386
|
+
normal `ks build src/app.ks` never compiles it, since `ks build` only walks the `using`
|
|
387
|
+
graph reachable from the entry you give it. Build it as its own separate entry too:
|
|
388
|
+
`ks build src/app.ks && ks build src/dogs_page.ks` (already-compiled shared dependencies
|
|
389
|
+
just get written again with identical output, so this is safe to add). For tests,
|
|
390
|
+
`kopular/testing`'s `runKopularApp` (0.24.0+) compiles any other real `.ks` file present in
|
|
391
|
+
your `srcDir` that the entry doesn't reach — a lazy-route target is exactly that — so
|
|
392
|
+
`AddLazyRoute` needs no special test setup at all.
|
|
376
393
|
|
|
377
394
|
**`AllPaths()`** returns every registered path (both `AddRoute` and `AddLazyRoute`), in
|
|
378
395
|
registration order — for a caller that needs to enumerate real routes (a build-time static
|
|
@@ -819,11 +836,11 @@ error, not a runtime blank screen.
|
|
|
819
836
|
|
|
820
837
|
## Starting a new project: `kp new`
|
|
821
838
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
839
|
+
`npx kp new my-app` scaffolds a real, working project: a `Counter` component
|
|
840
|
+
(`src/counter.ks`), an `index.html` with every Kopular module already mapped, and the
|
|
841
|
+
vendor/serve scripts needed to run in a browser (a browser can't resolve a bare specifier
|
|
842
|
+
like `"kopular/component"` the way Node's own module resolution does, so `vendor-kopular.mjs`
|
|
843
|
+
copies Kopular's runtime into `vendor/` and `index.html`'s import map points there).
|
|
827
844
|
|
|
828
845
|
```bash
|
|
829
846
|
npx kp new my-app
|
|
@@ -832,37 +849,19 @@ npm install
|
|
|
832
849
|
npm start # builds, vendors kopular's browser files, and serves at :8080
|
|
833
850
|
```
|
|
834
851
|
|
|
835
|
-
|
|
836
|
-
above), the ambient DOM/Kopular `extern` bindings it needs (`src/kopular_bindings.ks`),
|
|
837
|
-
and a `README.md` that points an AI agent at this package's own `LLM.md` before it starts
|
|
852
|
+
Its `README.md` points an AI agent at this package's own `GUIDE.md` before it starts
|
|
838
853
|
generating code. `kp` ships from this package (not from `kopscript`'s own `ks` CLI) since
|
|
839
854
|
scaffolding a *Kopular* app is a framework concern, not a language one — `ks` stays a
|
|
840
855
|
pure-language tool with no framework knowledge baked in.
|
|
841
856
|
|
|
842
857
|
## Using Kopular from another KopScript project
|
|
843
858
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
(`kp new` above generates exactly this, if you'd rather not hand-write it):
|
|
859
|
+
`using "kopular";` (`kopscript@1.1.0`+) resolves this package's own KopScript declarations
|
|
860
|
+
through `node_modules` — one line, every Kopular type in scope, with each file's own
|
|
861
|
+
compiled output importing only what it actually references:
|
|
848
862
|
|
|
849
863
|
```ks
|
|
850
|
-
|
|
851
|
-
static VElement Create(string tag);
|
|
852
|
-
string TextContent { get; set; }
|
|
853
|
-
} from "kopular/velement";
|
|
854
|
-
|
|
855
|
-
extern class Component {
|
|
856
|
-
constructor();
|
|
857
|
-
virtual VElement Render();
|
|
858
|
-
void Mount(Element parent);
|
|
859
|
-
} from "kopular/component";
|
|
860
|
-
|
|
861
|
-
extern class Router {
|
|
862
|
-
constructor(Component notFoundPage);
|
|
863
|
-
void AddRoute(string path, Component page);
|
|
864
|
-
void Navigate(string path);
|
|
865
|
-
} from "kopular/router";
|
|
864
|
+
using "kopular";
|
|
866
865
|
|
|
867
866
|
class MyWidget : Component {
|
|
868
867
|
public override VElement Render() {
|
|
@@ -873,19 +872,14 @@ class MyWidget : Component {
|
|
|
873
872
|
}
|
|
874
873
|
```
|
|
875
874
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
for a full working example (components, a service, and routing, all consuming Kopular
|
|
879
|
-
this way).
|
|
875
|
+
See [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) for
|
|
876
|
+
a full working example (components, a service, and routing).
|
|
880
877
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
these, `Element`/`Document`/`Event` included. `npx kp new` (above) generates this
|
|
887
|
-
boilerplate for a fresh project either way — reach for `LLM.md`'s block when adding to
|
|
888
|
-
an existing one instead.
|
|
878
|
+
Before `kopscript@1.1.0`, every project hand-declared its own copy of Kopular's `extern`
|
|
879
|
+
bindings instead — `using` couldn't reach into a package yet. That still works (`kp new`
|
|
880
|
+
above can generate it), and is still the right call if you want a visibly trimmed subset —
|
|
881
|
+
see `LLM.md`'s "Consuming Kopular from your own KopScript project" for the complete,
|
|
882
|
+
copy-ready block, `Element`/`Document`/`Event` included.
|
|
889
883
|
|
|
890
884
|
`extern class` can carry its own `<T>` (kopscript >= 0.5.0), so a generic export like
|
|
891
885
|
`FormField<T>` describes the same way a real generic class does — see `LLM.md`'s
|