kopular 1.2.1 → 1.2.3

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 (3) hide show
  1. package/GUIDE.md +94 -125
  2. package/LLM.md +4 -2
  3. package/package.json +1 -1
package/GUIDE.md CHANGED
@@ -1,14 +1,13 @@
1
1
  # Kopular quick guide
2
2
 
3
- Everything you need for typical app work, in one page. For anything not covered here
4
- generics, nullable types, advanced routing, the full diagnostic list — see this package's
5
- own `LLM.md` and `node_modules/kopscript/LLM.md`.
3
+ Everything for typical app work. For what isn't here generics, nullable depth, advanced
4
+ routing, the diagnostic list — see this package's `LLM.md` and `node_modules/kopscript/LLM.md`.
6
5
 
7
6
  ## Setup
8
7
 
9
- Every file starts with `using "kopular";` for the DOM and every Kopular type, plus
10
- `using "./other_file";` per project file it needs (`using` isn't transitive list every
11
- file you reference, not just direct dependencies' dependencies).
8
+ Every file starts with `using "kopular";` (the DOM and every Kopular type), plus
9
+ `using "./other_file";` per project file it references. `using` isn't transitive: list every
10
+ file you use, not just your dependencies' dependencies.
12
11
 
13
12
  ```ks
14
13
  using "kopular";
@@ -18,71 +17,51 @@ Counter app = new Counter();
18
17
  app.Mount(document.body);
19
18
  ```
20
19
 
21
- Run `npm run build && npm run serve`, or `npm start` (a scaffolded project already has
22
- both scripts). `ks check src/app.ks` type-checks without building, if you just want errors.
20
+ `npm start` builds and serves; `ks check src/app.ks` type-checks without building.
23
21
 
24
22
  ## The language you need
25
23
 
26
- - `Type name = value;` for every local — no `let`/`var`, no inference on declarations.
27
- - No implicit `this`every member reference is `this.Field`/`this.Method()`, always,
28
- including inside a lambda.
29
- - `if`/`else`/`while`/`for`/`foreach (Type x in xs)` all statements, no ternary. Use
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.
24
+ - `Type name = value;` for every local — no `let`/`var`, no inference.
25
+ - **No implicit `this`**always `this.Field`/`this.Method()`, lambdas included.
26
+ - `if`/`else`/`while`/`for`/`foreach (Type x in xs)` are statements, and there is no ternary.
27
+ For a conditional *value* use `match`: `match x { "a" => "one", _ => "other" }`. **The
28
+ subject must be `string` or an `enum`** never `bool`/`number` and every pattern must be
29
+ a string literal or an `Enum.Member` name. `_` is required unless an enum subject names
30
+ every member. For a `bool`, use `if`/`else`.
36
31
  - 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`.
32
+ - Operators: `+ - * / %`, `== != < > <= >=`, `&& || !`. `!` is a real unary operator on a
33
+ `bool` (`if (!done)`). Precedence is conventional: `||` below `&&` below comparisons below
34
+ `+ -` below `* / %` below unary. **No `++`/`--`/`+=`** write `i = i + 1;`.
35
+ - **number string**: `"count: " + n`, or `$"{n} items left"`. No `.ToString()` (`KS4085`).
43
36
  - `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`.
54
- - Nullable: `string? name` a nullable field forces `if (x != null) { ... }` before use;
55
- narrowing is scoped to that `if` block, not reachability-based (an early
56
- `if (x == null) { return; }` does NOT narrow `x` afterward wrap the rest in
57
- `if (x != null) { ... }` instead).
58
- - Strings: `s.Length`, `.Contains()/.StartsWith()/.EndsWith()`, `.Trim()/.ToUpper()/.ToLower()`,
59
- `.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (sort comparator), `+` concatenates.
60
- Both `"double"` and `'single'` quotes work — use whichever the other isn't, e.g. inside
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.
65
- - Arrays: `xs.Length`, `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
66
- `.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating**, return a new array),
37
+ per `try`, its parameter type your choice and unchecked against what was thrown.
38
+ - **Comments are `//` only** — `/* ... */` is a parse error (`KS2020`), not an ignored region.
39
+ - **Reserved words** as a local/parameter name are a parse error, not shadowing. Easy to pick
40
+ by accident: `raw`, `state`, `task`, `from`, `as`, `get`, `set`, `in`, `base`, `match`.
41
+ - Nullable `string? name` forces `if (x != null) { ... }` before use. Narrowing is scoped to
42
+ that block, not reachability-based: an early `if (x == null) { return; }` does **not**
43
+ narrow afterwards wrap the rest in `if (x != null) { ... }`.
44
+ - `x == null`/`x != null` also match `undefined` a missed `.Find()`, an absent optional field.
45
+ - Strings: `.Length`, `.Contains()/.StartsWith()/.EndsWith()`, `.Trim()/.ToUpper()/.ToLower()`,
46
+ `.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (a sort comparator), `+` concatenates.
47
+ `"double"` and `'single'` quotes both work use whichever the surrounding context isn't,
48
+ e.g. `*if="Name != ''"` inside a template attribute.
49
+ - **Arrays are created by literal only**: `number[] xs = [];`, `[1, 2, 3]`, `[new Todo("a")]`.
50
+ There is no `new number[3]` form that's a parse error, not an empty array.
51
+ - Arrays: `.Length`, `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
52
+ `.Sort(cmp)/.Reverse()/.Push(x)` (**all three non-mutating** — each returns a new array),
67
53
  `.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
68
- - `x == null` / `x != null` also match `undefined` a missed `Array.Find`, an absent
69
- optional field, etc. all read as `null`.
70
- - **No `.Match()`/`.Test()`/`.Exec()` on `string` there is no direct regex-execute method
71
- at all.** A regex literal (`r"^[a-z]+$"`) only means something as a `match` *pattern*.
72
- The idiomatic way to classify or extract by character class is `.Split("")` (splits into
73
- a `string[]` of single characters) plus `match` per character:
54
+ - **No `.Match()`/`.Test()`/`.Exec()` on `string`** there is no regex-execute at all. A regex
55
+ literal (`r"^[a-z]+$"`) only means something as a `match` *pattern*. To classify by character
56
+ class, `.Split("")` into single characters and `match` each one:
74
57
  ```ks
75
58
  bool isLetter = match c { r"^[a-z]$" => true, _ => false };
76
59
  ```
77
- Build up runs (words, tokens, ...) by iterating those characters and appending to an
78
- accumulator string, flushing it whenever a non-matching character (or the end) is
79
- reached this covers tokenizing/extracting by character class without ever needing a
80
- hand-written JS shim. Reach for a real shim (below) only for something a `match` pattern
81
- genuinely can't express, like capturing a submatch.
82
- - No object-literal syntax anywhere (`{ key: value }` doesn't exist as a value). A JS API
83
- that needs one (rare — `fetch`'s options, `addEventListener`'s options object) needs a
84
- small hand-written `.js` shim; Kopular's own `Http`/`FormField` cover the common cases so
85
- you'll rarely hit this.
60
+ Build words/tokens by appending matching characters to an accumulator and flushing it on a
61
+ non-match or at the end. Reach for a hand-written `.js` shim only for what a pattern genuinely
62
+ can't express, like capturing a submatch.
63
+ - **No object-literal syntax** `{ key: value }` isn't a value anywhere. A JS API needing one
64
+ wants a small `.js` shim; `Http`/`FormField` already cover the common cases.
86
65
 
87
66
  ## Components
88
67
 
@@ -98,21 +77,20 @@ class Counter : Component {
98
77
  <button (click)="Increment()">Count: {{ Count.Value }}</button>
99
78
  ```
100
79
 
101
- `state<T>` referenced directly in a template auto-subscribes no manual `Subscribe`
102
- needed. State reached *indirectly* (through a method, or `this.SomeService.Count`) does
103
- need one: `this.SomeService.Count.Subscribe((v) => this.Update());` in the constructor.
80
+ A `state<T>` referenced directly in a template auto-subscribes. State reached *indirectly*
81
+ (through a method, or `this.SomeService.Count`) needs one in the constructor:
82
+ `this.SomeService.Count.Subscribe((v) => this.Update());`
104
83
 
105
84
  **Template bindings**: `{{ expr }}` text, `[prop]="expr"` (real fields for
106
- `id`/`className`/`value`/`disabled`/`checked`; anything else is a plain attribute),
107
- `(click)`/`(input)`/`(blur)`/`(change)` events, `*if="expr"`, `*for="Type v of expr"`
108
- (element type required, no inference), `*mount="expr"` (embeds a live child component,
109
- composes with `*for`).
110
-
111
- **Two-way binding, `[(value)]="Field"`** — `Field` can be a bare name, `this.Field`, or a
112
- member path; it's assigned back directly (`Field = e.target.value`), so it must resolve
113
- to something assignable, never a method call. **A `state<T>` field needs `.Value` on the
114
- end** — `[(value)]="Qty.Value"`, not `[(value)]="Qty"` (a bare `state<T>` isn't itself a
115
- `string`, so binding it directly is a type error):
85
+ `id`/`className`/`value`/`disabled`/`checked`; anything else becomes a plain attribute),
86
+ `(click)`/`(input)`/`(blur)`/`(change)` events, `*if="expr"`, `*for="Type v of expr"` (element
87
+ type required, never inferred), `*mount="expr"` (embeds a live child component, composes with
88
+ `*for`).
89
+
90
+ **Two-way binding, `[(value)]="Field"`** — `Field` may be a bare name, `this.Field`, or a member
91
+ path, and is assigned back directly, so it must resolve to something assignable, never a method
92
+ call. **A `state<T>` field needs `.Value`** — `[(value)]="Qty.Value"`, since a bare `state<T>`
93
+ isn't a `string`:
116
94
  ```ks
117
95
  public state<string> Qty;
118
96
  constructor() : base() { this.Qty = state("1"); }
@@ -121,8 +99,8 @@ constructor() : base() { this.Qty = state("1"); }
121
99
  <input [(value)]="Qty.Value" />
122
100
  ```
123
101
 
124
- **Exactly one top-level element per template — no auto-wrapping, a hard compile error
125
- otherwise.** Wrap multiple top-level pieces in one real container element:
102
+ **Exactly one top-level element per template** — no auto-wrapping, a hard compile error
103
+ otherwise. Wrap multiple pieces in one container:
126
104
  ```html
127
105
  <div>
128
106
  <input id="text" [(value)]="Draft" />
@@ -130,8 +108,7 @@ otherwise.** Wrap multiple top-level pieces in one real container element:
130
108
  </div>
131
109
  ```
132
110
 
133
- **Hand-written `Render()`** (needed when logic is too dynamic for a template, or a template
134
- would obscure more than it clarifies):
111
+ **Hand-written `Render()`**, for logic too dynamic for a template:
135
112
 
136
113
  ```ks
137
114
  public override VElement Render() {
@@ -142,18 +119,17 @@ public override VElement Render() {
142
119
  }
143
120
  ```
144
121
  `VElement.Create(tag)`, `.TextContent/.ClassName/.Id/.Value/.RawHtml/.Disabled/.Checked`,
145
- `.OnClick/.OnInput/.OnBlur/.OnChange`, `.AppendChild(child)`, `.SetAttr(name, value)` (the
146
- escape hatch for anything without a named field). Both styles compile to the same thing and
147
- mix freely across a project.
122
+ `.OnClick/.OnInput/.OnBlur/.OnChange`, `.AppendChild(child)`, `.SetAttr(name, value)` (the escape
123
+ hatch for anything without a named field). Both styles compile to the same thing and mix freely.
148
124
 
149
- **Conditional value**: there's no ternary, so to pick between two elements use `If()` (from
125
+ **Conditional value**: no ternary, so to pick between two elements use `If()` (from
150
126
  `kopular/directives`, already covered by `using "kopular"`):
151
127
  ```ks
152
128
  root.AppendChild(If(this.On.Value, () => this.Yes(), () => this.No()));
153
129
  ```
154
130
 
155
- **Error boundary**: override `RenderError(string message)` to show a fallback instead of an
156
- uncaught crash if `Render()` throws.
131
+ **Error boundary**: override `RenderError(string message)` for a fallback instead of an uncaught
132
+ crash when `Render()` throws.
157
133
 
158
134
  ## Nested components & content projection
159
135
 
@@ -164,15 +140,15 @@ this.Items.ForEach((Item item) => {
164
140
  list.AppendChild(slot);
165
141
  });
166
142
  ```
167
- A template does the same via `<li *for="Item i of Items" *mount="i"></li>`.
143
+ A template does the same with `<li *for="Item i of Items" *mount="i"></li>`.
168
144
 
169
- Content projection (React's `children`): pass a `() => VElement` into a constructor, call
170
- it from `Render()` — no separate mechanism needed.
145
+ Content projection (React's `children`): pass a `() => VElement` into a constructor and call it
146
+ from `Render()`.
171
147
 
172
148
  ## Services — no DI container
173
149
 
174
- A service is a plain class; "injecting" it is a constructor argument. Wire everything once
175
- in a composition root:
150
+ A service is a plain class; "injecting" it is a constructor argument. Wire it once in a
151
+ composition root:
176
152
 
177
153
  ```ks
178
154
  class AppContainer {
@@ -198,19 +174,16 @@ nav.SetGuard("/login", (string path) => {
198
174
  nav.Navigate("/dogs/1");
199
175
  nav.Mount(document.body);
200
176
  ```
201
- - Routes hold already-built `Component`s, not factories — built once, state survives
202
- navigating away and back.
203
- - `nav.Param` the first `:name` segment, a plain `string` (not `state<T>`; the outlet
204
- re-renders on every navigation already).
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.
211
- - `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
177
+ - Routes hold already-built `Component`s, not factories — state survives navigating away and back.
178
+ - `nav.Param` the first `:name` segment, a plain `string` (not `state<T>`; the outlet re-renders
179
+ on every navigation anyway).
180
+ - `SetGuard(redirectPath, guard)`: one guard for the whole router, called with the target path
181
+ before **every** navigation in-app, direct load/refresh, and back/forward alike. Returning
182
+ `false` redirects to `redirectPath` via `pushState`. The guard decides which paths it cares
183
+ about; `redirectPath` is never itself guard-checked, so pick one the guard always allows.
184
+ - `AddLazyRoute(path, loader)` for code-splitting see below.
212
185
  - Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
213
- matching file) a plain HTTP limitation, not a Kopular one.
186
+ matching file). That's an HTTP limitation, not a Kopular one.
214
187
 
215
188
  ## Forms
216
189
 
@@ -223,8 +196,9 @@ FormField<string> email = new FormField<string>("", (string v) => {
223
196
  email.Value.Value = "not-an-email";
224
197
  print(email.Error.Value); // "Must be a valid email"
225
198
  ```
226
- `Validators`: `Required/MinLength/MaxLength/Email/Min/Max`, each returns an error message
227
- or `null`. No array-of-validators param — chain checks in one lambda, as above.
199
+ `Validators`: `Required/MinLength/MaxLength/Email/Min/Max`, each returning a message or `null`.
200
+ There's no array-of-validators parameter — chain checks in one lambda, as above, or use
201
+ `CombineValidators2`/`CombineValidators3`.
228
202
 
229
203
  ## HTTP
230
204
 
@@ -233,9 +207,9 @@ Response r = await Http.Get("/api/dogs");
233
207
  if (r.ok) { string body = await r.text(); }
234
208
  await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
235
209
  ```
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):
210
+ No typed JSON deserialization built in: describe the shape as an `extern class` and bind
211
+ `JSON.parse`. Note the `;` after an `extern class` body, and never write `async` on an `extern`
212
+ signature give it a `task<T>` return type instead:
239
213
 
240
214
  ```ks
241
215
  extern class User {
@@ -246,8 +220,8 @@ extern class User {
246
220
  extern User[] ParseUsers(string json) as "JSON.parse";
247
221
  ```
248
222
 
249
- A complete load-with-states method. `async` methods return `task` or `task<T>`, never a
250
- bare value type:
223
+ A complete load-with-states method. `async` methods return `task` or `task<T>`, never a bare
224
+ value type:
251
225
 
252
226
  ```ks
253
227
  public async task Load() {
@@ -296,8 +270,8 @@ class Widget : Component {
296
270
  styles from "./widget.css";
297
271
  }
298
272
  ```
299
- Every selector in `widget.css` is rewritten to only match this class's own elements
300
- never a sibling's or child's. Needs a constructor to exist on the class.
273
+ Every selector in `widget.css` is rewritten to match only this class's own elements. The class
274
+ must have a constructor.
301
275
 
302
276
  ## Lazy routes (real code-splitting)
303
277
 
@@ -312,11 +286,8 @@ export async function LoadAdminPage() {
312
286
  extern task<Component> LoadAdminPage() from "./admin_page_loader";
313
287
  nav.AddLazyRoute("/admin", LoadAdminPage);
314
288
  ```
315
- The lazy page (`admin_page.ks`) must be built as its own entry too it's deliberately not
316
- `using`'d from your app's entry (that's what keeps it out of the eager bundle), so add a
317
- second build line: `ks build src/app.ks && ks build src/admin_page.ks`. Testing it via
318
- `kopular/testing`'s `runKopularApp` needs no extra setup — it compiles any file in your
319
- `srcDir` the entry doesn't reach.
289
+ The lazy page is deliberately not `using`'d from your entry — that's what keeps it out of the
290
+ eager bundle so build it as its own entry too: `ks build src/app.ks && ks build src/admin_page.ks`.
320
291
 
321
292
  ## Testing
322
293
 
@@ -330,12 +301,10 @@ Real jsdom, real compile, real DOM assertions — not a mock.
330
301
 
331
302
  ## Common mistakes
332
303
 
333
- - Forgetting `this.` on a member reference inside a lambda — it's a real undefined-identifier
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`).
337
- - `if (x == null) { return; } use(x);` does NOT narrow `x` — see "Nullable" above.
338
- - A two-way binding target must be a field path (`Field`, `this.Field`, `Field.Value`), never
339
- a method call.
340
- - `*mount` accepts only `id`/`[id]` on its element — any other attr/binding is a compile
341
- error, since the mounted child's own `Render()` owns all of its content.
304
+ - Forgetting `this.` on a member inside a lambda — a real undefined-identifier error.
305
+ - A `state<T>` where a plain `T` is expected needs `.Value` — including a `bool` returned from a
306
+ router guard or passed to `If()` (`KS4038`).
307
+ - `if (x == null) { return; } use(x);` does **not** narrow `x`.
308
+ - A two-way binding target must be a field path, never a method call.
309
+ - `*mount` accepts only `id`/`[id]` on its element anything else is a compile error, since the
310
+ mounted child's own `Render()` owns its content.
package/LLM.md CHANGED
@@ -442,8 +442,10 @@ class Counter : Component {
442
442
  to the exact same `VElement.Create`/`.AppendChild`/`.TextContent`/named-event-field
443
443
  calls a hand-written `Render()` would use. `(event)` only accepts `click`/`input`/
444
444
  `blur`/`change` — `VElement`'s own fixed set — anything else is a compile error
445
- (`KS5016`). `[prop]`/static `attr="..."` assign directly for `id`/`className`/`value`;
446
- anything else goes through `SetAttr` instead.
445
+ (`KS5016`). `[prop]`/static `attr="..."` assign directly for `id`/`className`/`value`
446
+ **and the two real bool fields, `disabled`/`checked`** (the compiler's own
447
+ `BOOL_FIELD_NAMES` map — a boolean attribute is present-or-absent, so only a real
448
+ property can turn one back off); anything else goes through `SetAttr` instead.
447
449
  - **`*mount="expr"`** embeds a live child Component declaratively — desugars to
448
450
  `VElement.Mount(expr)`, the same mechanism a hand-written `Render()` uses (see "Nested
449
451
  component composition" below). Unlike `*if`/`*for`, it composes with either — `*for="Row
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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",