kopular 1.1.3 → 1.1.5
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 +71 -5
- package/LLM.md +3 -2
- package/README.md +1 -1
- package/package.json +1 -1
package/GUIDE.md
CHANGED
|
@@ -34,6 +34,23 @@ both scripts). `ks check src/app.ks` type-checks without building, if you just w
|
|
|
34
34
|
for an enum subject, by naming every member). For a `bool`, use `if`/`else` instead —
|
|
35
35
|
there's no equivalent shorthand.
|
|
36
36
|
- Lambdas need explicit parameter types: `(number x) => x * 2`.
|
|
37
|
+
- Operators: `+ - * / %`, `== != < > <= >=`, `&& || !` — `!` is a real unary operator on a
|
|
38
|
+
`bool` (`if (!done)`, `bool flipped = !done;`). Precedence, low → high: `=` → `||` →
|
|
39
|
+
`&&` → `==` `!=` → `<` `>` `<=` `>=` → `+` `-` → `*` `/` `%` → unary `-` `!` →
|
|
40
|
+
`.member`/`(call)`/`[index]`. **No `++`/`--`/`+=`** — write `i = i + 1;`.
|
|
41
|
+
- **number → string**: `+` with a string on either side (`"count: " + n`), or interpolation
|
|
42
|
+
(`$"{n} items left"`). There is no `.ToString()` — calling it is `KS4085`.
|
|
43
|
+
- `try { } catch (string e) { } finally { }` and `throw "message";` all exist — one `catch`
|
|
44
|
+
per `try`, and its parameter type is your choice (it is *not* checked against what was
|
|
45
|
+
actually thrown).
|
|
46
|
+
- **Comments are `//` only** — there is no `/* ... */` block comment, and a `/*` is a parse
|
|
47
|
+
error (`KS2020`), not an ignored region.
|
|
48
|
+
- **Reserved words**: naming a local or parameter one of these is a parse error, not
|
|
49
|
+
shadowing — `raw`, `state`, `task`, `from`, `as`, `get`, `set`, `in`, `base` and `match`
|
|
50
|
+
are the ones easy to pick by accident. Full list: `using extern raw template styles from
|
|
51
|
+
as const class interface enum constructor public private protected static virtual override
|
|
52
|
+
get set return if else while for foreach in break continue match this base new void true
|
|
53
|
+
false null task state async await try catch finally throw`.
|
|
37
54
|
- Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
|
|
38
55
|
narrowing is scoped to that `if` block, not reachability-based (an early
|
|
39
56
|
`if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
|
|
@@ -42,6 +59,9 @@ both scripts). `ks check src/app.ks` type-checks without building, if you just w
|
|
|
42
59
|
`.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (sort comparator), `+` concatenates.
|
|
43
60
|
Both `"double"` and `'single'` quotes work — use whichever the other isn't, e.g. inside
|
|
44
61
|
a template attribute (`*if="Name != ''"`).
|
|
62
|
+
- Arrays are created with a literal and nothing else: `number[] xs = [];`, `[1, 2, 3]`,
|
|
63
|
+
`[new Todo("a")]`. **There is no `new number[3]` form** — that's a parse error, not an
|
|
64
|
+
empty array.
|
|
45
65
|
- Arrays: `xs.Length`, `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
|
|
46
66
|
`.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating**, return a new array),
|
|
47
67
|
`.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
|
|
@@ -126,6 +146,12 @@ public override VElement Render() {
|
|
|
126
146
|
escape hatch for anything without a named field). Both styles compile to the same thing and
|
|
127
147
|
mix freely across a project.
|
|
128
148
|
|
|
149
|
+
**Conditional value**: there's no ternary, so to pick between two elements use `If()` (from
|
|
150
|
+
`kopular/directives`, already covered by `using "kopular"`):
|
|
151
|
+
```ks
|
|
152
|
+
root.AppendChild(If(this.On.Value, () => this.Yes(), () => this.No()));
|
|
153
|
+
```
|
|
154
|
+
|
|
129
155
|
**Error boundary**: override `RenderError(string message)` to show a fallback instead of an
|
|
130
156
|
uncaught crash if `Render()` throws.
|
|
131
157
|
|
|
@@ -166,7 +192,7 @@ Router nav = new Router(new NotFoundPage()); // fallback page, required
|
|
|
166
192
|
nav.AddRoute("/", new HomePage(nav));
|
|
167
193
|
nav.AddRoute("/dogs/:id", new DogDetailPage(nav)); // nav.Param inside that page
|
|
168
194
|
nav.SetGuard("/login", (string path) => {
|
|
169
|
-
if (path == "/admin") { return
|
|
195
|
+
if (path == "/admin") { return auth.LoggedIn.Value; } // .Value — a state<bool> is not a bool
|
|
170
196
|
return true;
|
|
171
197
|
});
|
|
172
198
|
nav.Navigate("/dogs/1");
|
|
@@ -176,8 +202,12 @@ nav.Mount(document.body);
|
|
|
176
202
|
navigating away and back.
|
|
177
203
|
- `nav.Param` — the first `:name` segment, a plain `string` (not `state<T>`; the outlet
|
|
178
204
|
re-renders on every navigation already).
|
|
179
|
-
- `SetGuard(redirectPath, guard)`: one guard for the whole router
|
|
180
|
-
|
|
205
|
+
- `SetGuard(redirectPath, guard)`: one guard for the whole router, called with the target
|
|
206
|
+
path before **every** navigation — in-app, a direct load/refresh, and back/forward alike,
|
|
207
|
+
so a guarded page is covered however it's reached. Returning `false` redirects to
|
|
208
|
+
`redirectPath` (via `pushState`, so the URL changes too). `guard` itself decides which
|
|
209
|
+
paths it cares about, and `redirectPath` is never guard-checked, so pick one the guard
|
|
210
|
+
always allows. Defaults to always-allow.
|
|
181
211
|
- `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
|
|
182
212
|
- Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
|
|
183
213
|
matching file) — a plain HTTP limitation, not a Kopular one.
|
|
@@ -203,8 +233,39 @@ Response r = await Http.Get("/api/dogs");
|
|
|
203
233
|
if (r.ok) { string body = await r.text(); }
|
|
204
234
|
await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
|
|
205
235
|
```
|
|
206
|
-
No typed JSON deserialization built in — describe the shape as `extern class` and
|
|
207
|
-
|
|
236
|
+
No typed JSON deserialization built in — describe the shape as an `extern class` and bind
|
|
237
|
+
`JSON.parse`. Note the `;` after an `extern class` body, and never write `async` on an
|
|
238
|
+
`extern` signature (give it a `task<T>` return type instead):
|
|
239
|
+
|
|
240
|
+
```ks
|
|
241
|
+
extern class User {
|
|
242
|
+
number id { get; }
|
|
243
|
+
string name { get; }
|
|
244
|
+
string email { get; }
|
|
245
|
+
};
|
|
246
|
+
extern User[] ParseUsers(string json) as "JSON.parse";
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
A complete load-with-states method. `async` methods return `task` or `task<T>`, never a
|
|
250
|
+
bare value type:
|
|
251
|
+
|
|
252
|
+
```ks
|
|
253
|
+
public async task Load() {
|
|
254
|
+
this.Status.Value = "loading";
|
|
255
|
+
try {
|
|
256
|
+
Response r = await Http.Get("/api/users");
|
|
257
|
+
if (r.ok) {
|
|
258
|
+
string body = await r.text();
|
|
259
|
+
this.Users.Value = ParseUsers(body);
|
|
260
|
+
this.Status.Value = "ok";
|
|
261
|
+
} else {
|
|
262
|
+
this.Status.Value = "error";
|
|
263
|
+
}
|
|
264
|
+
} catch (string e) {
|
|
265
|
+
this.Status.Value = "error";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
208
269
|
|
|
209
270
|
## Async: `Delay`, `Computed`, `Resource<T>`
|
|
210
271
|
|
|
@@ -217,6 +278,8 @@ Computed2<number, number, number> total = new Computed2<number, number, number>(
|
|
|
217
278
|
total.Value.Subscribe((number v) => this.Update()); // total.Value is itself state<number>
|
|
218
279
|
|
|
219
280
|
Resource<Response> r = new Resource<Response>(Http.Get(url)); // task already in flight
|
|
281
|
+
// Data is state<T?> and Error is state<string?> — both null until the task settles, so
|
|
282
|
+
// whatever these arms call has to accept the nullable type (or null-check first).
|
|
220
283
|
match r.Status.Value {
|
|
221
284
|
AsyncStatus.Loading => BuildSpinner(),
|
|
222
285
|
AsyncStatus.Success => BuildContent(r.Data.Value),
|
|
@@ -228,6 +291,7 @@ match r.Status.Value {
|
|
|
228
291
|
|
|
229
292
|
```ks
|
|
230
293
|
class Widget : Component {
|
|
294
|
+
constructor() : base() { } // required: 'styles from' has to have one to initialize from
|
|
231
295
|
template from "./widget.html";
|
|
232
296
|
styles from "./widget.css";
|
|
233
297
|
}
|
|
@@ -268,6 +332,8 @@ Real jsdom, real compile, real DOM assertions — not a mock.
|
|
|
268
332
|
|
|
269
333
|
- Forgetting `this.` on a member reference inside a lambda — it's a real undefined-identifier
|
|
270
334
|
error, not automatic.
|
|
335
|
+
- A `state<T>` used where a plain `T` is expected needs `.Value` — including a `bool` one
|
|
336
|
+
returned from a router guard or passed to `If()` (`KS4038`).
|
|
271
337
|
- `if (x == null) { return; } use(x);` does NOT narrow `x` — see "Nullable" above.
|
|
272
338
|
- A two-way binding target must be a field path (`Field`, `this.Field`, `Field.Value`), never
|
|
273
339
|
a method call.
|
package/LLM.md
CHANGED
|
@@ -171,7 +171,7 @@ extern class Router {
|
|
|
171
171
|
// is in flight — default: `<div class="router-loading">Loading...</div>`.
|
|
172
172
|
// Only declare this if you actually override it (a class extending
|
|
173
173
|
// Router) — see "Lazy routes" below.
|
|
174
|
-
|
|
174
|
+
virtual VElement BuildLoadingPlaceholder();
|
|
175
175
|
void Navigate(string path);
|
|
176
176
|
void Mount(Element parent);
|
|
177
177
|
// One guard for the whole Router, not per-route — see "Router" below.
|
|
@@ -320,6 +320,7 @@ w.Bump(); // re-render + diff + patch
|
|
|
320
320
|
component, applied once the handler returns:
|
|
321
321
|
```ks
|
|
322
322
|
private void Save() {
|
|
323
|
+
Item newItem = new Item(this.Draft); // whatever this handler just built
|
|
323
324
|
this.Draft = ""; // a plain field Render() also reads
|
|
324
325
|
this.Items.Value = this.Items.Value.Push(newItem); // triggers the ONE render, via Subscribe
|
|
325
326
|
}
|
|
@@ -503,7 +504,7 @@ Real URLs via the History API (`pushState`/`popstate`), not hash routing.
|
|
|
503
504
|
|
|
504
505
|
```ks
|
|
505
506
|
class NotFoundPage : Component {
|
|
506
|
-
public override VElement Render() {
|
|
507
|
+
public override VElement Render() { return VElement.Create("h1"); }
|
|
507
508
|
}
|
|
508
509
|
class HomePage : Component {
|
|
509
510
|
private Router Nav;
|
package/README.md
CHANGED
|
@@ -641,7 +641,7 @@ own `header.ks`/`nav.ks` use it exactly this way):
|
|
|
641
641
|
```ks
|
|
642
642
|
VElement header = VElement.Create("header");
|
|
643
643
|
header.RawHtml = SiteHeaderHtml; // a raw string ... from "./header.html"; constant
|
|
644
|
-
header.OnClick = (Event e) => {
|
|
644
|
+
header.OnClick = (Event e) => { }; // one delegated listener over the whole subtree
|
|
645
645
|
```
|
|
646
646
|
|
|
647
647
|
**It's unescaped, real `innerHTML` — never assign it anything reachable from user input.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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",
|