kopular 1.1.2 → 1.1.4

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 (2) hide show
  1. package/GUIDE.md +72 -7
  2. package/package.json +1 -1
package/GUIDE.md CHANGED
@@ -27,9 +27,28 @@ both scripts). `ks check src/app.ks` type-checks without building, if you just w
27
27
  - No implicit `this` — every member reference is `this.Field`/`this.Method()`, always,
28
28
  including inside a lambda.
29
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).
30
+ `match` for a conditional value: `match x { "a" => "one", _ => "other" }`. **The subject
31
+ must be `string` or an `enum` — never `bool`/`number`/anything else, and every pattern
32
+ must be a string literal (or, for an enum subject, an `Enum.Member` name) — not a number,
33
+ not `true`/`false`.** The `_` arm is required unless every case is covered (only possible
34
+ for an enum subject, by naming every member). For a `bool`, use `if`/`else` instead —
35
+ there's no equivalent shorthand.
32
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
+ - **Reserved words**: naming a local or parameter one of these is a parse error, not
47
+ shadowing — `raw`, `state`, `task`, `from`, `as`, `get`, `set`, `in`, `base` and `match`
48
+ are the ones easy to pick by accident. Full list: `using extern raw template styles from
49
+ as const class interface enum constructor public private protected static virtual override
50
+ get set return if else while for foreach in break continue match this base new void true
51
+ false null task state async await try catch finally throw`.
33
52
  - Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
34
53
  narrowing is scoped to that `if` block, not reachability-based (an early
35
54
  `if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
@@ -122,6 +141,12 @@ public override VElement Render() {
122
141
  escape hatch for anything without a named field). Both styles compile to the same thing and
123
142
  mix freely across a project.
124
143
 
144
+ **Conditional value**: there's no ternary, so to pick between two elements use `If()` (from
145
+ `kopular/directives`, already covered by `using "kopular"`):
146
+ ```ks
147
+ root.AppendChild(If(this.On.Value, () => this.Yes(), () => this.No()));
148
+ ```
149
+
125
150
  **Error boundary**: override `RenderError(string message)` to show a fallback instead of an
126
151
  uncaught crash if `Render()` throws.
127
152
 
@@ -162,7 +187,7 @@ Router nav = new Router(new NotFoundPage()); // fallback page, required
162
187
  nav.AddRoute("/", new HomePage(nav));
163
188
  nav.AddRoute("/dogs/:id", new DogDetailPage(nav)); // nav.Param inside that page
164
189
  nav.SetGuard("/login", (string path) => {
165
- if (path == "/admin") { return authService.LoggedIn; }
190
+ if (path == "/admin") { return auth.LoggedIn.Value; } // .Value — a state<bool> is not a bool
166
191
  return true;
167
192
  });
168
193
  nav.Navigate("/dogs/1");
@@ -172,8 +197,12 @@ nav.Mount(document.body);
172
197
  navigating away and back.
173
198
  - `nav.Param` — the first `:name` segment, a plain `string` (not `state<T>`; the outlet
174
199
  re-renders on every navigation already).
175
- - `SetGuard(redirectPath, guard)`: one guard for the whole router; `guard` returns `false`
176
- to redirect. Defaults to always-allow.
200
+ - `SetGuard(redirectPath, guard)`: one guard for the whole router, called with the target
201
+ path before **every** navigation — in-app, a direct load/refresh, and back/forward alike,
202
+ so a guarded page is covered however it's reached. Returning `false` redirects to
203
+ `redirectPath` (via `pushState`, so the URL changes too). `guard` itself decides which
204
+ paths it cares about, and `redirectPath` is never guard-checked, so pick one the guard
205
+ always allows. Defaults to always-allow.
177
206
  - `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
178
207
  - Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
179
208
  matching file) — a plain HTTP limitation, not a Kopular one.
@@ -199,8 +228,39 @@ Response r = await Http.Get("/api/dogs");
199
228
  if (r.ok) { string body = await r.text(); }
200
229
  await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
201
230
  ```
202
- No typed JSON deserialization built in — describe the shape as `extern class` and parse
203
- with `extern MyShape Parse(string json) as "JSON.parse";`.
231
+ No typed JSON deserialization built in — describe the shape as an `extern class` and bind
232
+ `JSON.parse`. Note the `;` after an `extern class` body, and never write `async` on an
233
+ `extern` signature (give it a `task<T>` return type instead):
234
+
235
+ ```ks
236
+ extern class User {
237
+ number id { get; }
238
+ string name { get; }
239
+ string email { get; }
240
+ };
241
+ extern User[] ParseUsers(string json) as "JSON.parse";
242
+ ```
243
+
244
+ A complete load-with-states method. `async` methods return `task` or `task<T>`, never a
245
+ bare value type:
246
+
247
+ ```ks
248
+ public async task Load() {
249
+ this.Status.Value = "loading";
250
+ try {
251
+ Response r = await Http.Get("/api/users");
252
+ if (r.ok) {
253
+ string body = await r.text();
254
+ this.Users.Value = ParseUsers(body);
255
+ this.Status.Value = "ok";
256
+ } else {
257
+ this.Status.Value = "error";
258
+ }
259
+ } catch (string e) {
260
+ this.Status.Value = "error";
261
+ }
262
+ }
263
+ ```
204
264
 
205
265
  ## Async: `Delay`, `Computed`, `Resource<T>`
206
266
 
@@ -213,6 +273,8 @@ Computed2<number, number, number> total = new Computed2<number, number, number>(
213
273
  total.Value.Subscribe((number v) => this.Update()); // total.Value is itself state<number>
214
274
 
215
275
  Resource<Response> r = new Resource<Response>(Http.Get(url)); // task already in flight
276
+ // Data is state<T?> and Error is state<string?> — both null until the task settles, so
277
+ // whatever these arms call has to accept the nullable type (or null-check first).
216
278
  match r.Status.Value {
217
279
  AsyncStatus.Loading => BuildSpinner(),
218
280
  AsyncStatus.Success => BuildContent(r.Data.Value),
@@ -224,6 +286,7 @@ match r.Status.Value {
224
286
 
225
287
  ```ks
226
288
  class Widget : Component {
289
+ constructor() : base() { } // required: 'styles from' has to have one to initialize from
227
290
  template from "./widget.html";
228
291
  styles from "./widget.css";
229
292
  }
@@ -264,6 +327,8 @@ Real jsdom, real compile, real DOM assertions — not a mock.
264
327
 
265
328
  - Forgetting `this.` on a member reference inside a lambda — it's a real undefined-identifier
266
329
  error, not automatic.
330
+ - A `state<T>` used where a plain `T` is expected needs `.Value` — including a `bool` one
331
+ returned from a router guard or passed to `If()` (`KS4038`).
267
332
  - `if (x == null) { return; } use(x);` does NOT narrow `x` — see "Nullable" above.
268
333
  - A two-way binding target must be a field path (`Field`, `this.Field`, `Field.Value`), never
269
334
  a method call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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",