kopscript 0.17.0 → 0.18.0
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/LLM.md +7 -1
- package/README.md +14 -2
- package/dist/template_compiler.js +32 -0
- package/dist/template_parser.js +11 -2
- package/package.json +1 -1
package/LLM.md
CHANGED
|
@@ -550,6 +550,12 @@ class Counter : Component {
|
|
|
550
550
|
`[prop]="expr"` (→ a plain field assignment for `id`/`className`/`value`, or
|
|
551
551
|
`VElement.SetAttr("prop", expr)` for anything else), static `attr="..."` (`class` aliases
|
|
552
552
|
to `className`).
|
|
553
|
+
- **Two-way binding**: `[(value)]="Field"` → `[value]="Field"` + an auto-generated
|
|
554
|
+
`(input)="Field = e.target.value"`. `value` only (`KS5017` otherwise — no other
|
|
555
|
+
`VElement` field has an equivalent "user just changed this" event); `Field` must resolve
|
|
556
|
+
to a bare name or `this.Field` (`KS5018` otherwise — anything else has nothing sensible
|
|
557
|
+
to assign back into). A hand-written `Render()` has no equivalent shorthand — it already
|
|
558
|
+
has direct field/handler access, so there's nothing to desugar.
|
|
553
559
|
- Structural directives: `*if="expr"` (→ real `if`), `*for="Type varName of expr"` (→ real
|
|
554
560
|
`for..in`; the element type is explicit — no inference, same stance as Generics).
|
|
555
561
|
At most one structural directive per element.
|
|
@@ -558,7 +564,7 @@ class Counter : Component {
|
|
|
558
564
|
manual `Subscribe` needed for that field. State reached indirectly (through a method call,
|
|
559
565
|
or `this.SomeService.Count`) still needs a manual `Subscribe`, unchanged from before.
|
|
560
566
|
- Exactly one top-level element per template (no auto-wrap — hard error). No mixing text and
|
|
561
|
-
element children under one element. No
|
|
567
|
+
element children under one element. No pipes, no stacked directives.
|
|
562
568
|
- `ks watch` tracks the referenced `.html` file as well as `.ks` dependencies.
|
|
563
569
|
- Layering note: `template from` is kopscript grammar, but what it desugars *to*
|
|
564
570
|
(`VElement.Create`/`.AppendChild`/`.TextContent`/`.SetAttr`/the named `On*` event fields)
|
package/README.md
CHANGED
|
@@ -606,10 +606,22 @@ Supported bindings and directives:
|
|
|
606
606
|
| `{{ expr }}` (in text) | `el.TextContent = $"...{expr}...";` (an `InterpolatedStringLiteral`, same as `$"..."`) |
|
|
607
607
|
| `(event)="stmt"` | `el.OnEvent = (Event e) => { stmt };` — `event` must be one of `click`/`input`/`blur`/`change`, `VElement`'s own fixed set of named event fields |
|
|
608
608
|
| `[prop]="expr"` | `el.Prop = expr;` for `id`/`className`/`value` (`VElement`'s own named fields); `el.SetAttr("prop", expr);` for anything else |
|
|
609
|
+
| `[(value)]="Field"` | sugar for `[value]="Field"` + `(input)="Field = e.target.value"` — see below |
|
|
609
610
|
| `class="..."` (static) | `el.ClassName = "...";` (aliased, since `class` is a KopScript keyword) |
|
|
610
611
|
| `*if="expr"` | a real `if (expr) { ... }` around the element's creation |
|
|
611
612
|
| `*for="Type var of expr"` | a real `for (Type var in expr) { ... }` — the element type is explicit, matching KopScript's no-inference stance elsewhere (Generics, Nullable types) |
|
|
612
613
|
|
|
614
|
+
**Two-way binding**: `[(value)]="Field"` (a template-only sugar — there's no equivalent
|
|
615
|
+
shorthand for a hand-written `Render()`, which already has direct field/handler access)
|
|
616
|
+
desugars to exactly the pair you'd otherwise write by hand: a `[value]` property binding
|
|
617
|
+
plus an auto-generated `(input)` handler assigning back. Restricted to `value`
|
|
618
|
+
specifically — the one `VElement` field a user can change through direct interaction
|
|
619
|
+
(typing, picking an option); `[(id)]`/`[(className)]` are compile errors (`KS5017`), since
|
|
620
|
+
neither has an equivalent "user just changed this" event. The bound expression must be a
|
|
621
|
+
simple field reference (a bare name or `this.Field`, resolving the same way any other
|
|
622
|
+
binding does) — `[(value)]="Field.Trim()"` is a compile error (`KS5018`), since there's
|
|
623
|
+
nothing sensible to assign back into.
|
|
624
|
+
|
|
613
625
|
**Auto-subscribe**: a `state<T>` field declared directly on the component and referenced
|
|
614
626
|
directly in its template (like `Count` above) gets its `Subscribe((v) => this.Update())`
|
|
615
627
|
wired up automatically — no manual `Subscribe` call needed in the constructor. This is a
|
|
@@ -620,8 +632,8 @@ templates existed.
|
|
|
620
632
|
|
|
621
633
|
v1 cuts, same discipline as generics and nullable types: exactly one top-level element per
|
|
622
634
|
template (no auto-wrapping — a clear error instead); no mixing text and element children
|
|
623
|
-
under one element (
|
|
624
|
-
|
|
635
|
+
under one element (`VElement` has no text-node concept, only `.TextContent`); no pipes, no
|
|
636
|
+
stacking two structural directives on one element.
|
|
625
637
|
|
|
626
638
|
**A deliberate layering note**: the `template from` syntax lives in kopscript's own
|
|
627
639
|
grammar (Kopular can't extend a language it doesn't own), but what it desugars *to* —
|
|
@@ -234,6 +234,8 @@ export class TemplateCompiler {
|
|
|
234
234
|
};
|
|
235
235
|
statements.push(assign(member(self, fieldName, bind), lambda, bind));
|
|
236
236
|
}
|
|
237
|
+
for (const bind of node.twoWayBindings)
|
|
238
|
+
statements.push(...this.buildTwoWayBinding(self, bind, localScope));
|
|
237
239
|
const elementChildren = node.children.filter((c) => c.kind === "element");
|
|
238
240
|
const textChildren = node.children.filter((c) => c.kind === "text");
|
|
239
241
|
if (textChildren.length > 0 && elementChildren.length > 0) {
|
|
@@ -301,4 +303,34 @@ export class TemplateCompiler {
|
|
|
301
303
|
}
|
|
302
304
|
return exprStatement(call(member(self, "SetAttr", at), [stringLiteral(name, at), value], at), at);
|
|
303
305
|
}
|
|
306
|
+
// `[(value)]="Field"` desugars to exactly the two-piece pattern you'd
|
|
307
|
+
// otherwise write by hand — `[value]="Field"` (a plain property binding)
|
|
308
|
+
// plus `(input)="Field = e.target.value"` (an auto-generated handler
|
|
309
|
+
// assigning back). Restricted to `value` specifically: it's the one
|
|
310
|
+
// VElement field a user can change through direct interaction (typing,
|
|
311
|
+
// picking an option) with no event of Kopular's own in between — `id`/
|
|
312
|
+
// `className` have no equivalent "user just changed this" event, so
|
|
313
|
+
// there's nothing for a two-way binding to mean for them.
|
|
314
|
+
buildTwoWayBinding(self, bind, localScope) {
|
|
315
|
+
if (bind.name !== "value") {
|
|
316
|
+
this.diagnostics.error("KS5017", `Unsupported two-way binding '[(${bind.name})]' — only [(value)] is supported, the one VElement field a user can change through direct interaction`, bind.line, bind.col);
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const target = this.resolve(bind.target, localScope);
|
|
320
|
+
if (target.kind !== "Identifier" && target.kind !== "MemberExpr") {
|
|
321
|
+
this.diagnostics.error("KS5018", `A two-way binding target must be a simple field reference (a bare name or 'this.Field'), not a complex expression`, bind.line, bind.col);
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
const propAssign = this.buildAttrAssignment(self, bind.name, target, bind);
|
|
325
|
+
const assignBack = assign(target, member(member(ident("e", bind), "target", bind), "value", bind), bind);
|
|
326
|
+
const lambda = {
|
|
327
|
+
kind: "LambdaExpr",
|
|
328
|
+
params: [{ name: "e", type: EVENT_TYPE }],
|
|
329
|
+
body: block([assignBack], bind),
|
|
330
|
+
line: bind.line,
|
|
331
|
+
col: bind.col,
|
|
332
|
+
};
|
|
333
|
+
const eventAssign = assign(member(self, "OnInput", bind), lambda, bind);
|
|
334
|
+
return [propAssign, eventAssign];
|
|
335
|
+
}
|
|
304
336
|
}
|
package/dist/template_parser.js
CHANGED
|
@@ -162,6 +162,7 @@ export class TemplateParser {
|
|
|
162
162
|
const staticAttrs = [];
|
|
163
163
|
const propBindings = [];
|
|
164
164
|
const eventBindings = [];
|
|
165
|
+
const twoWayBindings = [];
|
|
165
166
|
let ifCondition = null;
|
|
166
167
|
let forBinding = null;
|
|
167
168
|
while (this.check(TemplateTokenKind.AttrName)) {
|
|
@@ -171,7 +172,15 @@ export class TemplateParser {
|
|
|
171
172
|
if (!valueTok)
|
|
172
173
|
continue;
|
|
173
174
|
const name = nameTok.lexeme;
|
|
174
|
-
|
|
175
|
+
// Checked before the plain `[prop]` case below — `[(value)]` also
|
|
176
|
+
// starts with `[` and ends with `]`, so it would otherwise be
|
|
177
|
+
// mistaken for a property binding named literally "(value)".
|
|
178
|
+
if (name.startsWith("[(") && name.endsWith(")]")) {
|
|
179
|
+
const propName = name.slice(2, -2);
|
|
180
|
+
const target = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
181
|
+
twoWayBindings.push({ name: propName, target, line: nameTok.line, col: nameTok.col });
|
|
182
|
+
}
|
|
183
|
+
else if (name.startsWith("(") && name.endsWith(")")) {
|
|
175
184
|
const eventName = name.slice(1, -1);
|
|
176
185
|
const handler = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
177
186
|
eventBindings.push({ name: eventName, handler, line: nameTok.line, col: nameTok.col });
|
|
@@ -226,7 +235,7 @@ export class TemplateParser {
|
|
|
226
235
|
this.diagnostics.error("KS5012", `Expected closing tag '</${tag}>'`, tagTok.line, tagTok.col);
|
|
227
236
|
}
|
|
228
237
|
}
|
|
229
|
-
return { kind: "element", tag, staticAttrs, propBindings, eventBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
|
|
238
|
+
return { kind: "element", tag, staticAttrs, propBindings, eventBindings, twoWayBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
|
|
230
239
|
}
|
|
231
240
|
consumeEquals(attrNameTok) {
|
|
232
241
|
this.consume(TemplateTokenKind.Equals, attrNameTok, `Expected '=' after attribute '${attrNameTok.lexeme}'`);
|
package/package.json
CHANGED