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.
- package/GUIDE.md +94 -125
- package/LLM.md +4 -2
- package/package.json +1 -1
package/GUIDE.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# Kopular quick guide
|
|
2
2
|
|
|
3
|
-
Everything
|
|
4
|
-
|
|
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";`
|
|
10
|
-
`using "./other_file";` per project file it
|
|
11
|
-
file you
|
|
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
|
-
|
|
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
|
|
27
|
-
- No implicit `this
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
|
|
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: `+ - * / %`, `== != < > <= >=`, `&& ||
|
|
38
|
-
`bool` (`if (!done)
|
|
39
|
-
|
|
40
|
-
|
|
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`,
|
|
45
|
-
|
|
46
|
-
- **
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`
|
|
58
|
-
-
|
|
59
|
-
`.
|
|
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
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
107
|
-
`(click)`/`(input)`/`(blur)`/`(change)` events, `*if="expr"`, `*for="Type v of expr"`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
**Two-way binding, `[(value)]="Field"`** — `Field`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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()
|
|
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
|
-
|
|
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**:
|
|
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)`
|
|
156
|
-
|
|
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
|
|
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
|
|
170
|
-
|
|
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
|
|
175
|
-
|
|
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 —
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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)
|
|
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
|
|
227
|
-
|
|
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
|
|
237
|
-
`JSON.parse`. Note the `;` after an `extern class` body, and never write `async` on an
|
|
238
|
-
|
|
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
|
-
|
|
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
|
|
300
|
-
|
|
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
|
|
316
|
-
|
|
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
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
-
|
|
338
|
-
-
|
|
339
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|