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.
Files changed (4) hide show
  1. package/GUIDE.md +71 -5
  2. package/LLM.md +3 -2
  3. package/README.md +1 -1
  4. 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 authService.LoggedIn; }
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; `guard` returns `false`
180
- to redirect. Defaults to always-allow.
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 parse
207
- with `extern MyShape Parse(string json) as "JSON.parse";`.
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
- protected virtual VElement BuildLoadingPlaceholder();
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) => { /* one delegated listener over the whole subtree */ };
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",
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",