kopular 1.2.3 → 1.2.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 (2) hide show
  1. package/GUIDE.md +125 -94
  2. package/package.json +1 -1
package/GUIDE.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # Kopular quick guide
2
2
 
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`.
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`.
5
6
 
6
7
  ## Setup
7
8
 
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.
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).
11
12
 
12
13
  ```ks
13
14
  using "kopular";
@@ -17,51 +18,71 @@ Counter app = new Counter();
17
18
  app.Mount(document.body);
18
19
  ```
19
20
 
20
- `npm start` builds and serves; `ks check src/app.ks` type-checks without building.
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.
21
23
 
22
24
  ## The language you need
23
25
 
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`.
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.
31
36
  - Lambdas need explicit parameter types: `(number x) => x * 2`.
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`).
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`.
36
43
  - `try { } catch (string e) { } finally { }` and `throw "message";` all exist — one `catch`
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),
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),
53
67
  `.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
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:
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:
57
74
  ```ks
58
75
  bool isLetter = match c { r"^[a-z]$" => true, _ => false };
59
76
  ```
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.
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.
65
86
 
66
87
  ## Components
67
88
 
@@ -77,20 +98,21 @@ class Counter : Component {
77
98
  <button (click)="Increment()">Count: {{ Count.Value }}</button>
78
99
  ```
79
100
 
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());`
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.
83
104
 
84
105
  **Template bindings**: `{{ expr }}` text, `[prop]="expr"` (real fields for
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`:
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):
94
116
  ```ks
95
117
  public state<string> Qty;
96
118
  constructor() : base() { this.Qty = state("1"); }
@@ -99,8 +121,8 @@ constructor() : base() { this.Qty = state("1"); }
99
121
  <input [(value)]="Qty.Value" />
100
122
  ```
101
123
 
102
- **Exactly one top-level element per template** — no auto-wrapping, a hard compile error
103
- otherwise. Wrap multiple pieces in one container:
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:
104
126
  ```html
105
127
  <div>
106
128
  <input id="text" [(value)]="Draft" />
@@ -108,7 +130,8 @@ otherwise. Wrap multiple pieces in one container:
108
130
  </div>
109
131
  ```
110
132
 
111
- **Hand-written `Render()`**, for logic too dynamic for a template:
133
+ **Hand-written `Render()`** (needed when logic is too dynamic for a template, or a template
134
+ would obscure more than it clarifies):
112
135
 
113
136
  ```ks
114
137
  public override VElement Render() {
@@ -119,17 +142,18 @@ public override VElement Render() {
119
142
  }
120
143
  ```
121
144
  `VElement.Create(tag)`, `.TextContent/.ClassName/.Id/.Value/.RawHtml/.Disabled/.Checked`,
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.
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.
124
148
 
125
- **Conditional value**: no ternary, so to pick between two elements use `If()` (from
149
+ **Conditional value**: there's no ternary, so to pick between two elements use `If()` (from
126
150
  `kopular/directives`, already covered by `using "kopular"`):
127
151
  ```ks
128
152
  root.AppendChild(If(this.On.Value, () => this.Yes(), () => this.No()));
129
153
  ```
130
154
 
131
- **Error boundary**: override `RenderError(string message)` for a fallback instead of an uncaught
132
- crash when `Render()` throws.
155
+ **Error boundary**: override `RenderError(string message)` to show a fallback instead of an
156
+ uncaught crash if `Render()` throws.
133
157
 
134
158
  ## Nested components & content projection
135
159
 
@@ -140,15 +164,15 @@ this.Items.ForEach((Item item) => {
140
164
  list.AppendChild(slot);
141
165
  });
142
166
  ```
143
- A template does the same with `<li *for="Item i of Items" *mount="i"></li>`.
167
+ A template does the same via `<li *for="Item i of Items" *mount="i"></li>`.
144
168
 
145
- Content projection (React's `children`): pass a `() => VElement` into a constructor and call it
146
- from `Render()`.
169
+ Content projection (React's `children`): pass a `() => VElement` into a constructor, call
170
+ it from `Render()` — no separate mechanism needed.
147
171
 
148
172
  ## Services — no DI container
149
173
 
150
- A service is a plain class; "injecting" it is a constructor argument. Wire it once in a
151
- composition root:
174
+ A service is a plain class; "injecting" it is a constructor argument. Wire everything once
175
+ in a composition root:
152
176
 
153
177
  ```ks
154
178
  class AppContainer {
@@ -174,16 +198,19 @@ nav.SetGuard("/login", (string path) => {
174
198
  nav.Navigate("/dogs/1");
175
199
  nav.Mount(document.body);
176
200
  ```
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.
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.
185
212
  - Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
186
- matching file). That's an HTTP limitation, not a Kopular one.
213
+ matching file) a plain HTTP limitation, not a Kopular one.
187
214
 
188
215
  ## Forms
189
216
 
@@ -196,9 +223,8 @@ FormField<string> email = new FormField<string>("", (string v) => {
196
223
  email.Value.Value = "not-an-email";
197
224
  print(email.Error.Value); // "Must be a valid email"
198
225
  ```
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`.
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.
202
228
 
203
229
  ## HTTP
204
230
 
@@ -207,9 +233,9 @@ Response r = await Http.Get("/api/dogs");
207
233
  if (r.ok) { string body = await r.text(); }
208
234
  await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
209
235
  ```
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:
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):
213
239
 
214
240
  ```ks
215
241
  extern class User {
@@ -220,8 +246,8 @@ extern class User {
220
246
  extern User[] ParseUsers(string json) as "JSON.parse";
221
247
  ```
222
248
 
223
- A complete load-with-states method. `async` methods return `task` or `task<T>`, never a bare
224
- value type:
249
+ A complete load-with-states method. `async` methods return `task` or `task<T>`, never a
250
+ bare value type:
225
251
 
226
252
  ```ks
227
253
  public async task Load() {
@@ -270,8 +296,8 @@ class Widget : Component {
270
296
  styles from "./widget.css";
271
297
  }
272
298
  ```
273
- Every selector in `widget.css` is rewritten to match only this class's own elements. The class
274
- must have a constructor.
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.
275
301
 
276
302
  ## Lazy routes (real code-splitting)
277
303
 
@@ -286,8 +312,11 @@ export async function LoadAdminPage() {
286
312
  extern task<Component> LoadAdminPage() from "./admin_page_loader";
287
313
  nav.AddLazyRoute("/admin", LoadAdminPage);
288
314
  ```
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`.
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.
291
320
 
292
321
  ## Testing
293
322
 
@@ -301,10 +330,12 @@ Real jsdom, real compile, real DOM assertions — not a mock.
301
330
 
302
331
  ## Common mistakes
303
332
 
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.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "1.2.3",
3
+ "version": "1.2.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",