kopular 1.4.2 → 1.4.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 +40 -130
- package/LLM.md +6 -2
- package/package.json +1 -1
package/GUIDE.md
CHANGED
|
@@ -49,65 +49,46 @@ building, for when you only want the errors.
|
|
|
49
49
|
## The language you need
|
|
50
50
|
|
|
51
51
|
- `Type name = value;` for every local — no `let`/`var`, no inference on declarations.
|
|
52
|
-
- No implicit `this
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
must be a string literal (or, for an enum subject, an `Enum.Member` name) — not a number,
|
|
58
|
-
not `true`/`false`.** The `_` arm is required unless every case is covered (only possible
|
|
59
|
-
for an enum subject, by naming every member). For a `bool`, use `if`/`else` instead —
|
|
60
|
-
there's no equivalent shorthand.
|
|
52
|
+
- No implicit `this`: every member is `this.Field`/`this.Method()`, inside lambdas too.
|
|
53
|
+
- `if`/`else`/`while`/`for`/`foreach (Type x in xs)`; no ternary. For a conditional value use
|
|
54
|
+
`match x { "a" => "one", _ => "other" }` — **the subject must be a `string` or an `enum`,
|
|
55
|
+
every pattern a string literal (or `Enum.Member`), never a number or `true`/`false`.** `_`
|
|
56
|
+
is required unless an enum's members are all named. For a `bool`, use `if`/`else`.
|
|
61
57
|
- Lambdas need explicit parameter types: `(number x) => x * 2`.
|
|
62
|
-
- Operators: `+ - * / %`, `== != < > <= >=`, `&& || !`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
as const class interface enum constructor public private protected static virtual override
|
|
77
|
-
get set return if else while for foreach in break continue match this base new void true
|
|
78
|
-
false null task state async await try catch finally throw`.
|
|
79
|
-
- Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
|
|
80
|
-
narrowing is scoped to that `if` block, not reachability-based (an early
|
|
81
|
-
`if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
|
|
82
|
-
`if (x != null) { ... }` instead).
|
|
58
|
+
- Operators: `+ - * / %`, `== != < > <= >=`, `&& || !` (`!` works on a `bool`: `if (!done)`).
|
|
59
|
+
Precedence low → high: `=`, `||`, `&&`, `== !=`, `< > <= >=`, `+ -`, `* / %`, unary `- !`,
|
|
60
|
+
then `.member`/`(call)`/`[index]`. **No `++`/`--`/`+=`** — write `i = i + 1;`.
|
|
61
|
+
- **number → string**: `"count: " + n` or `$"{n} items left"`. There is no `.ToString()`
|
|
62
|
+
(`KS4085`).
|
|
63
|
+
- `try { } catch (string e) { } finally { }` and `throw "message";` — one `catch` per `try`;
|
|
64
|
+
its parameter type isn't checked against what was thrown.
|
|
65
|
+
- **Comments are `//` only** — `/*` is a parse error (`KS2020`).
|
|
66
|
+
- **Reserved words** — easy to pick by accident as a local or parameter name, and a parse
|
|
67
|
+
error if you do: `raw`, `state`, `task`, `from`, `as`, `get`, `set`, `in`, `base`, `match`,
|
|
68
|
+
`template`, `styles`, `const`, `async`, `await`, plus the usual keywords.
|
|
69
|
+
- Nullable: `string? name` must be checked with `if (x != null) { ... }` before use, and
|
|
70
|
+
narrowing only lasts inside that block — an early `if (x == null) { return; }` does NOT
|
|
71
|
+
narrow `x` afterward.
|
|
83
72
|
- Strings: `s.Length`, `.Contains()/.StartsWith()/.EndsWith()`, `.Trim()/.ToUpper()/.ToLower()`,
|
|
84
73
|
`.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (sort comparator), `+` concatenates.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- Arrays
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating**, return a new array),
|
|
74
|
+
`"double"` and `'single'` quotes both work — handy inside a template attribute
|
|
75
|
+
(`*if="Name != ''"`).
|
|
76
|
+
- Arrays come only from literals: `number[] xs = [];`, `[1, 2, 3]`, `[new Todo("a")]` —
|
|
77
|
+
**`new number[3]` is a parse error.** Methods: `xs.Length`,
|
|
78
|
+
`.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
|
|
79
|
+
`.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating** — they return a new array),
|
|
92
80
|
`.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
|
|
93
|
-
- `x == null`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
a `string[]` of single characters) plus `match` per character:
|
|
81
|
+
- `x == null` also matches `undefined` (a missed `Find`, an absent field).
|
|
82
|
+
- **No regex methods on `string`** (`.Match()`/`.Test()`/`.Exec()` don't exist). A regex
|
|
83
|
+
literal works only as a `match` pattern, so to classify or tokenize by character class,
|
|
84
|
+
`.Split("")` into characters and `match` each one, appending to an accumulator and flushing
|
|
85
|
+
it at each non-matching character and at the end:
|
|
99
86
|
```ks
|
|
100
87
|
bool isLetter = match c { r"^[a-z]$" => true, _ => false };
|
|
101
88
|
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
hand-written JS shim. Reach for a real shim (below) only for something a `match` pattern
|
|
106
|
-
genuinely can't express, like capturing a submatch.
|
|
107
|
-
- No object-literal syntax anywhere (`{ key: value }` doesn't exist as a value). A JS API
|
|
108
|
-
that needs one (rare — `fetch`'s options, `addEventListener`'s options object) needs a
|
|
109
|
-
small hand-written `.js` shim; Kopular's own `Http`/`FormField` cover the common cases so
|
|
110
|
-
you'll rarely hit this.
|
|
89
|
+
Only something a pattern can't express (capturing a submatch) needs a `.js` shim.
|
|
90
|
+
- No object literals (`{ key: value }` isn't a value). The rare JS API that needs one
|
|
91
|
+
needs a small `.js` shim; `Http`/`FormField` cover the common cases.
|
|
111
92
|
|
|
112
93
|
## Components
|
|
113
94
|
|
|
@@ -151,44 +132,8 @@ otherwise.** Wrap multiple top-level pieces in one real container element:
|
|
|
151
132
|
</div>
|
|
152
133
|
```
|
|
153
134
|
|
|
154
|
-
**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```ks
|
|
158
|
-
public override VElement Render() {
|
|
159
|
-
VElement button = VElement.Create("button");
|
|
160
|
-
button.TextContent = "Count: " + this.Count.Value;
|
|
161
|
-
button.OnClick = (Event e) => { this.Increment(); };
|
|
162
|
-
return button;
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
`VElement.Create(tag)`, `.TextContent/.ClassName/.Id/.Value/.RawHtml/.Disabled/.Checked`,
|
|
166
|
-
`.OnClick/.OnInput/.OnBlur/.OnChange`, `.AppendChild(child)`, `.SetAttr(name, value)` (the
|
|
167
|
-
escape hatch for anything without a named field). Both styles compile to the same thing and
|
|
168
|
-
mix freely across a project.
|
|
169
|
-
|
|
170
|
-
**Conditional value**: there's no ternary, so to pick between two elements use `If()` (from
|
|
171
|
-
`kopular/directives`, already covered by `using "kopular"`):
|
|
172
|
-
```ks
|
|
173
|
-
root.AppendChild(If(this.On.Value, () => this.Yes(), () => this.No()));
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**Error boundary**: override `RenderError(string message)` to show a fallback instead of an
|
|
177
|
-
uncaught crash if `Render()` throws.
|
|
178
|
-
|
|
179
|
-
## Nested components & content projection
|
|
180
|
-
|
|
181
|
-
```ks
|
|
182
|
-
this.Items.ForEach((Item item) => {
|
|
183
|
-
VElement slot = VElement.Mount(item); // item : Component, e.g. its own `public string Id;`
|
|
184
|
-
slot.Id = item.Id; // stable key for reordering
|
|
185
|
-
list.AppendChild(slot);
|
|
186
|
-
});
|
|
187
|
-
```
|
|
188
|
-
A template does the same via `<li *for="Item i of Items" *mount="i"></li>`.
|
|
189
|
-
|
|
190
|
-
Content projection (React's `children`): pass a `() => VElement` into a constructor, call
|
|
191
|
-
it from `Render()` — no separate mechanism needed.
|
|
135
|
+
**Beyond templates** — a hand-written `Render()` (the `VElement` API), `If()`, and the
|
|
136
|
+
`RenderError` error boundary are in `LLM.md`; templates cover typical app work.
|
|
192
137
|
|
|
193
138
|
## Services — no DI container
|
|
194
139
|
|
|
@@ -233,7 +178,7 @@ nav.Mount(document.body);
|
|
|
233
178
|
`redirectPath` (via `pushState`, so the URL changes too). `guard` itself decides which
|
|
234
179
|
paths it cares about, and `redirectPath` is never guard-checked, so pick one the guard
|
|
235
180
|
always allows. Defaults to always-allow.
|
|
236
|
-
- `AddLazyRoute(path, loader)` for real code-splitting — see
|
|
181
|
+
- `AddLazyRoute(path, loader)` for real code-splitting — see `LLM.md`.
|
|
237
182
|
- Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
|
|
238
183
|
matching file) — a plain HTTP limitation, not a Kopular one.
|
|
239
184
|
|
|
@@ -312,46 +257,11 @@ match r.Status.Value {
|
|
|
312
257
|
};
|
|
313
258
|
```
|
|
314
259
|
|
|
315
|
-
##
|
|
316
|
-
|
|
317
|
-
```ks
|
|
318
|
-
class Widget : Component {
|
|
319
|
-
constructor() : base() { } // required: 'styles from' has to have one to initialize from
|
|
320
|
-
template from "./widget.html";
|
|
321
|
-
styles from "./widget.css";
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
Every selector in `widget.css` is rewritten to only match this class's own elements —
|
|
325
|
-
never a sibling's or child's. Needs a constructor to exist on the class.
|
|
326
|
-
|
|
327
|
-
## Lazy routes (real code-splitting)
|
|
260
|
+
## More in `node_modules/kopular/LLM.md`
|
|
328
261
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const { AdminPage } = await import("./admin_page.js");
|
|
333
|
-
return new AdminPage();
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
```ks
|
|
337
|
-
extern task<Component> LoadAdminPage() from "./admin_page_loader";
|
|
338
|
-
nav.AddLazyRoute("/admin", LoadAdminPage);
|
|
339
|
-
```
|
|
340
|
-
The lazy page (`admin_page.ks`) must be built as its own entry too — it's deliberately not
|
|
341
|
-
`using`'d from your app's entry (that's what keeps it out of the eager bundle), so add a
|
|
342
|
-
second build line: `ks build src/app.ks && ks build src/admin_page.ks`. Testing it via
|
|
343
|
-
`kopular/testing`'s `runKopularApp` needs no extra setup — it compiles any file in your
|
|
344
|
-
`srcDir` the entry doesn't reach.
|
|
345
|
-
|
|
346
|
-
## Testing
|
|
347
|
-
|
|
348
|
-
```js
|
|
349
|
-
import { runKopularApp } from "kopular/testing";
|
|
350
|
-
const { window, cleanup } = await runKopularApp(srcDir, "app.ks", { includeKopularPackage: true });
|
|
351
|
-
// assert against window.document, then:
|
|
352
|
-
cleanup();
|
|
353
|
-
```
|
|
354
|
-
Real jsdom, real compile, real DOM assertions — not a mock.
|
|
262
|
+
Nested components (`*mount`, `VElement.Mount`) and content projection, scoped styles
|
|
263
|
+
(`styles from`), lazy routes (`AddLazyRoute`), and testing (`kopular/testing`'s
|
|
264
|
+
`runKopularApp`).
|
|
355
265
|
|
|
356
266
|
## Common mistakes
|
|
357
267
|
|
package/LLM.md
CHANGED
|
@@ -125,8 +125,12 @@ extern class VElement {
|
|
|
125
125
|
(Event) => void OnInput { get; set; }
|
|
126
126
|
(Event) => void OnBlur { get; set; }
|
|
127
127
|
(Event) => void OnChange { get; set; }
|
|
128
|
+
// Calls preventDefault() before the handler — a form submit never reloads the page.
|
|
129
|
+
(Event) => void OnSubmit { get; set; }
|
|
128
130
|
void AppendChild(VElement child);
|
|
129
131
|
void SetAttr(string name, string value);
|
|
132
|
+
// A bare text node, for text next to elements in one parent.
|
|
133
|
+
static VElement Text(string content);
|
|
130
134
|
// Embeds a live child Component as this slot's content — see "Nested
|
|
131
135
|
// component composition" below. Declared taking a concrete Component
|
|
132
136
|
// rather than Kopular's own Mountable interface: extern declarations
|
|
@@ -299,8 +303,8 @@ w.Bump(); // re-render + diff + patch
|
|
|
299
303
|
`innerHTML`, unescaped: only ever assign static/developer-authored content — a `raw
|
|
300
304
|
string ... from "<path>.html";` compile-time constant, in every real use in this
|
|
301
305
|
ecosystem today — never anything reachable from user input, a fetched `Http` response
|
|
302
|
-
body, or a `FormField<T>`'s `.Value`, or it's a real XSS hole**), the
|
|
303
|
-
event fields `OnClick`/`OnInput`/`OnBlur`/`OnChange` (each a real no-op by default, never
|
|
306
|
+
body, or a `FormField<T>`'s `.Value`, or it's a real XSS hole**), the five fixed named
|
|
307
|
+
event fields `OnClick`/`OnInput`/`OnBlur`/`OnChange`/`OnSubmit` (each a real no-op by default, never
|
|
304
308
|
null — no nullable function type to fall back on), `AppendChild(child)`, and
|
|
305
309
|
`SetAttr(name, value)` (the escape hatch for any other real HTML attribute — `href`,
|
|
306
310
|
`src`, `alt`, `placeholder`, ...; never `Value`, see above). `Id` doubles as a stable key
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.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",
|