kopular 1.1.3 → 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 +66 -5
  2. package/package.json +1 -1
package/GUIDE.md CHANGED
@@ -34,6 +34,21 @@ 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
+ - **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`.
37
52
  - Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
38
53
  narrowing is scoped to that `if` block, not reachability-based (an early
39
54
  `if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
@@ -126,6 +141,12 @@ public override VElement Render() {
126
141
  escape hatch for anything without a named field). Both styles compile to the same thing and
127
142
  mix freely across a project.
128
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
+
129
150
  **Error boundary**: override `RenderError(string message)` to show a fallback instead of an
130
151
  uncaught crash if `Render()` throws.
131
152
 
@@ -166,7 +187,7 @@ Router nav = new Router(new NotFoundPage()); // fallback page, required
166
187
  nav.AddRoute("/", new HomePage(nav));
167
188
  nav.AddRoute("/dogs/:id", new DogDetailPage(nav)); // nav.Param inside that page
168
189
  nav.SetGuard("/login", (string path) => {
169
- if (path == "/admin") { return authService.LoggedIn; }
190
+ if (path == "/admin") { return auth.LoggedIn.Value; } // .Value — a state<bool> is not a bool
170
191
  return true;
171
192
  });
172
193
  nav.Navigate("/dogs/1");
@@ -176,8 +197,12 @@ nav.Mount(document.body);
176
197
  navigating away and back.
177
198
  - `nav.Param` — the first `:name` segment, a plain `string` (not `state<T>`; the outlet
178
199
  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.
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.
181
206
  - `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
182
207
  - Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
183
208
  matching file) — a plain HTTP limitation, not a Kopular one.
@@ -203,8 +228,39 @@ Response r = await Http.Get("/api/dogs");
203
228
  if (r.ok) { string body = await r.text(); }
204
229
  await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
205
230
  ```
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";`.
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
+ ```
208
264
 
209
265
  ## Async: `Delay`, `Computed`, `Resource<T>`
210
266
 
@@ -217,6 +273,8 @@ Computed2<number, number, number> total = new Computed2<number, number, number>(
217
273
  total.Value.Subscribe((number v) => this.Update()); // total.Value is itself state<number>
218
274
 
219
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).
220
278
  match r.Status.Value {
221
279
  AsyncStatus.Loading => BuildSpinner(),
222
280
  AsyncStatus.Success => BuildContent(r.Data.Value),
@@ -228,6 +286,7 @@ match r.Status.Value {
228
286
 
229
287
  ```ks
230
288
  class Widget : Component {
289
+ constructor() : base() { } // required: 'styles from' has to have one to initialize from
231
290
  template from "./widget.html";
232
291
  styles from "./widget.css";
233
292
  }
@@ -268,6 +327,8 @@ Real jsdom, real compile, real DOM assertions — not a mock.
268
327
 
269
328
  - Forgetting `this.` on a member reference inside a lambda — it's a real undefined-identifier
270
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`).
271
332
  - `if (x == null) { return; } use(x);` does NOT narrow `x` — see "Nullable" above.
272
333
  - A two-way binding target must be a field path (`Field`, `this.Field`, `Field.Value`), never
273
334
  a method call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "1.1.3",
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",