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 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 two-way binding, no pipes, no stacked directives.
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 (Kopular's DOM surface has no text-node type, only `.textContent`); no
624
- two-way binding, no pipes, no stacking two structural directives on one element.
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
  }
@@ -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
- if (name.startsWith("(") && name.endsWith(")")) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript, with generics and nullable types",
5
5
  "type": "module",
6
6
  "license": "MIT",