kopular 1.2.3 → 1.2.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 +125 -94
- 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
|
|
4
|
-
routing, the diagnostic list — see this package's
|
|
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";`
|
|
9
|
-
`using "./other_file";` per project file it
|
|
10
|
-
file you
|
|
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
|
|
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
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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: `+ - * / %`, `== != < > <= >=`, `&& ||
|
|
33
|
-
`bool` (`if (!done)`). Precedence
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
`.
|
|
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
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
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
|
-
|
|
81
|
-
(through a method, or `this.SomeService.Count`)
|
|
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
|
|
86
|
-
`(click)`/`(input)`/`(blur)`/`(change)` events, `*if="expr"`, `*for="Type v of expr"`
|
|
87
|
-
type required,
|
|
88
|
-
`*for`).
|
|
89
|
-
|
|
90
|
-
**Two-way binding, `[(value)]="Field"`** — `Field`
|
|
91
|
-
path
|
|
92
|
-
call. **A `state<T>` field needs `.Value
|
|
93
|
-
isn't a
|
|
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
|
|
103
|
-
otherwise
|
|
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()
|
|
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
|
|
123
|
-
hatch for anything without a named field). Both styles compile to the same thing and
|
|
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)`
|
|
132
|
-
crash
|
|
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
|
|
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
|
|
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
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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)
|
|
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
|
|
200
|
-
|
|
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
|
|
211
|
-
`JSON.parse`. Note the `;` after an `extern class` body, and never write `async` on an
|
|
212
|
-
signature
|
|
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
|
|
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
|
|
274
|
-
|
|
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
|
|
290
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
-
|
|
309
|
-
-
|
|
310
|
-
|
|
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
|
+
"version": "1.2.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",
|