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.
- package/GUIDE.md +72 -7
- 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 {
|
|
31
|
-
|
|
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
|
|
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
|
|
176
|
-
|
|
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
|
|
203
|
-
|
|
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.
|
|
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",
|