From 420706ca3b1dd88819c85869d46ebeaef2a052df Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 14 Jul 2026 00:43:07 +0530 Subject: [PATCH] feat: make state-based attributes reactive --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 22 +++++------ .publish/compiler/README.md | 2 +- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/README.md | 13 ++++++- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 26 ++++++------- .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +-- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 4 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/package.json | 2 +- packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 8 ++++ packages/compiler/README.md | 2 +- packages/compiler/package.json | 2 +- packages/compiler/src/codegen.ts | 51 +++++++++++++++++++------ packages/compiler/test/compiler.test.ts | 14 +++++++ packages/core/package.json | 2 +- packages/csr/README.md | 13 ++++++- packages/csr/package.json | 2 +- packages/csr/src/reactive-runtime.ts | 32 ++++++++++++++++ packages/csr/test/reactive.test.ts | 17 +++++++++ packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- 61 files changed, 221 insertions(+), 99 deletions(-) diff --git a/.publish/ai/package.json b/.publish/ai/package.json index e38cc8ee..6ce11900 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 91a6ea2d..7cd23b32 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 95e6360d..bafa4136 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.24", - "@wrnexus/router": "^0.2.24", - "@wrnexus/csr": "^0.2.24", - "@wrnexus/compiler": "^0.2.24", - "@wrnexus/styles": "^0.2.24", - "@wrnexus/dev-server": "^0.2.24", - "@wrnexus/ui": "^0.2.24", - "@wrnexus/validation": "^0.2.24", - "@wrnexus/i18n": "^0.2.24", - "@wrnexus/db": "^0.2.24" + "@wrnexus/core": "^0.2.25", + "@wrnexus/router": "^0.2.25", + "@wrnexus/csr": "^0.2.25", + "@wrnexus/compiler": "^0.2.25", + "@wrnexus/styles": "^0.2.25", + "@wrnexus/dev-server": "^0.2.25", + "@wrnexus/ui": "^0.2.25", + "@wrnexus/validation": "^0.2.25", + "@wrnexus/i18n": "^0.2.25", + "@wrnexus/db": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/compiler/README.md b/.publish/compiler/README.md index 463a0d7f..e997cf08 100644 --- a/.publish/compiler/README.md +++ b/.publish/compiler/README.md @@ -149,7 +149,7 @@ A file opens with `page ` or `component ` followed by a `{ ... }` bo - `layout = ""` — selects `app/layouts/.wrn` (pages only). - `props { name = ... }` — component props; each default's type drives coercion. - `state = ` — reactive state seeded from a raw JS expression. -- `view { }` — plain HTML with `{expr}` interpolation, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and ``. +- `view { }` — plain HTML with `{expr}` interpolation in text and attributes, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and ``. Attribute expressions that reference `state` keep an SSR value and update reactively in the browser. - `seo { key = "value" ... }` — metadata merged into the generated `meta`. - `style { }` — inlined page/component stylesheet (repeatable). - `functions { }` — shared server-side helpers (repeatable). diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 34e1b3e1..cd2d9cd6 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index 8cd0b04d..d205efd7 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/README.md b/.publish/csr/README.md index e43b7038..7383bed4 100644 --- a/.publish/csr/README.md +++ b/.publish/csr/README.md @@ -138,14 +138,25 @@ Bun.serve({ Browser side — server-rendered HTML that the reactive runtime hydrates: ```html -
+

Total: {{count}}

+ +
``` +State interpolation in ordinary attributes is reactive. The compiler keeps the +initial SSR value and emits an internal binding so attributes such as `type`, +`aria-label`, `aria-pressed`, `class`, and `href` update after state changes. + A realtime chat, fully declarative: ```html diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 0ab80ea7..cf7859b6 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.24" + "@wrnexus/core": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 3a1e63a9..c2adba75 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index f973123d..05e7664e 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.24", - "@wrnexus/router": "^0.2.24", - "@wrnexus/ssr": "^0.2.24", - "@wrnexus/csr": "^0.2.24", - "@wrnexus/compiler": "^0.2.24", - "@wrnexus/styles": "^0.2.24", - "@wrnexus/ui": "^0.2.24", - "@wrnexus/validation": "^0.2.24", - "@wrnexus/i18n": "^0.2.24", - "@wrnexus/db": "^0.2.24", - "@wrnexus/pubsub": "^0.2.24", - "@wrnexus/uploader": "^0.2.24" + "@wrnexus/core": "^0.2.25", + "@wrnexus/router": "^0.2.25", + "@wrnexus/ssr": "^0.2.25", + "@wrnexus/csr": "^0.2.25", + "@wrnexus/compiler": "^0.2.25", + "@wrnexus/styles": "^0.2.25", + "@wrnexus/ui": "^0.2.25", + "@wrnexus/validation": "^0.2.25", + "@wrnexus/i18n": "^0.2.25", + "@wrnexus/db": "^0.2.25", + "@wrnexus/pubsub": "^0.2.25", + "@wrnexus/uploader": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 4d3601b5..fb07d66e 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index 2cdd0b71..a7b7ea95 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.24" + "@wrnexus/core": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index db01d6f5..6fafc7fb 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.24" + "@wrnexus/core": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index e7089556..64071836 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index 433abc89..cb564da4 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.24" + "@wrnexus/native": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 5ea90376..297f4a82 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 23deae20..2db52bd1 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index fd3758ef..666434a1 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 11f21c14..b6c1d5e7 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 86659942..1a691fe2 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 9a72f089..b3e2e0fb 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.24", - "@wrnexus/core": "^0.2.24" + "@wrnexus/compiler": "^0.2.25", + "@wrnexus/core": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 1abe3fe8..b201402d 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.24" + "@wrnexus/core": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index ab4aadba..25804b6e 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.24" + "@wrnexus/uploader": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index 45493b3a..fc4e3e4a 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 811c9c8b..03342dad 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index a93ea421..f59f67e7 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.24" + "@wrnexus/core": "^0.2.25" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 93fe50c1..37f51a37 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.24" + "@wrnexus/core": "^0.2.25" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index af1fb9a6..01925f87 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/packages/ai/package.json b/packages/ai/package.json index 4e5631d6..075c1eba 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/authz/package.json b/packages/authz/package.json index 43936963..26a24026 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 9acad155..caeb90ca 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index e670d5b5..124d5850 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -296,6 +296,14 @@ const MIGRATIONS: Migration[] = [ // Explicit no-op: this release updates package and portal documentation only. }, }, + { + version: "0.2.25", + id: "reactive-state-attributes", + description: "No project-file changes; state expressions in attributes are reactive at runtime", + apply() { + // Explicit no-op: updating @wrnexus/compiler and @wrnexus/csr is sufficient. + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ diff --git a/packages/compiler/README.md b/packages/compiler/README.md index 463a0d7f..e997cf08 100644 --- a/packages/compiler/README.md +++ b/packages/compiler/README.md @@ -149,7 +149,7 @@ A file opens with `page ` or `component ` followed by a `{ ... }` bo - `layout = ""` — selects `app/layouts/.wrn` (pages only). - `props { name = ... }` — component props; each default's type drives coercion. - `state = ` — reactive state seeded from a raw JS expression. -- `view { }` — plain HTML with `{expr}` interpolation, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and ``. +- `view { }` — plain HTML with `{expr}` interpolation in text and attributes, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and ``. Attribute expressions that reference `state` keep an SSR value and update reactively in the browser. - `seo { key = "value" ... }` — metadata merged into the generated `meta`. - `style { }` — inlined page/component stylesheet (repeatable). - `functions { }` — shared server-side helpers (repeatable). diff --git a/packages/compiler/package.json b/packages/compiler/package.json index b85f1a3a..62142a2b 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index 8adec0fc..8528a138 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -79,8 +79,35 @@ function eventAttribute(name: string): string { return `data-on-${name}`; } -function renderAttrs(attrs: Attr[], csrId?: string): string { - const rendered = attrs.map(renderAttr).join(""); +function reactiveAttrValue(raw: string, reactive: PageReactive): string | null { + let found = false; + const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => { + const expr = inner.trim(); + if (!exprRefsState(expr, reactive.stateNames)) return whole; + found = true; + try { + const result = new Function("with(this){return (" + expr + ");}").call(reactive.scope); + return result == null ? "" : String(result); + } catch { + return whole; + } + }); + return found ? value : null; +} + +function renderAttrs(attrs: Attr[], csrId?: string, reactive: PageReactive | null = null): string { + let bindIndex = 0; + const rendered = attrs + .map((attr) => { + const base = renderAttr(attr); + if (!reactive || attr.event || attr.boolean || !base || !attr.value.includes("{")) + return base; + const initial = reactiveAttrValue(attr.value, reactive); + if (initial === null) return base; + const marker = JSON.stringify([attr.name, attr.value]); + return ` ${attr.name}="${attrEscape(initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`; + }) + .join(""); return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered; } @@ -312,7 +339,7 @@ function renderNode( // Void elements (
, , …) have no closing tag and no children. if (VOID_ELEMENTS.has(node.tag.toLowerCase())) { - return `<${node.tag}${renderAttrs(node.attrs, csrId)}>`; + return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>`; } const inner = @@ -331,7 +358,7 @@ function renderNode( ) .join(""); - return `<${node.tag}${renderAttrs(node.attrs, csrId)}>${inner}`; + return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}`; } function ssrMarker(bindings: SsrBinding[], binding: RenderBinding): string { @@ -745,14 +772,16 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { ); } + let bindIndex = 0; const attrs = node.attrs - .map((a) => - a.event - ? ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"` - : a.boolean - ? ` ${a.name}` - : ` ${a.name}="${compileAttrValue(a.value, ctx)}"`, - ) + .map((a) => { + if (a.event) return ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"`; + if (a.boolean) return ` ${a.name}`; + const rendered = ` ${a.name}="${compileAttrValue(a.value, ctx)}"`; + if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) return rendered; + const marker = attrEscape(JSON.stringify([a.name, a.value])); + return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`; + }) .join(""); // A `data-for` element introduces loop variables for its subtree. diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index d63e156a..e6abe268 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -116,6 +116,20 @@ test("page state text bakes its initial value into a reactive data-text span", ( expect(page).toContain("data-scope"); // state still forces a reactive scope }); +test("page state attributes bake their initial value and retain a reactive binding", () => { + const page = compileWireFile(`page Password { + state show = false + view { + + + } +}`); + expect(page).toContain('type="password"'); + expect(page).toContain('aria-label="Show"'); + expect(page.match(/data-wrn-bind-/g)).toHaveLength(2); + expect(page).toContain("show ? 'text' : 'password'"); +}); + test("data-for: loop-variable mustaches stay literal (not baked server-side)", () => { const comp = compileWireFile( `component TodoList {\n state todos = []\n view {
  • {t.text}
}\n}`, diff --git a/packages/core/package.json b/packages/core/package.json index 7b5bcc0f..1d3c5947 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/README.md b/packages/csr/README.md index e43b7038..7383bed4 100644 --- a/packages/csr/README.md +++ b/packages/csr/README.md @@ -138,14 +138,25 @@ Bun.serve({ Browser side — server-rendered HTML that the reactive runtime hydrates: ```html -
+

Total: {{count}}

+ +
``` +State interpolation in ordinary attributes is reactive. The compiler keeps the +initial SSR value and emits an internal binding so attributes such as `type`, +`aria-label`, `aria-pressed`, `class`, and `href` update after state changes. + A realtime chat, fully declarative: ```html diff --git a/packages/csr/package.json b/packages/csr/package.json index 9b0a9b6a..d08b897c 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index 37ad503b..7dba1ff1 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -199,6 +199,38 @@ export const REACTIVE_RUNTIME = String.raw` }); }); + // Reactive ordinary attributes emitted by the compiler. Each marker stores + // [attributeName, originalTemplate], preserving an SSR value while allowing + // state changes to update type, aria-*, class, href, and other attributes. + var bindNodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*"))); + bindNodes.forEach(function (node) { + if (!owns(node)) return; + Array.prototype.slice.call(node.attributes).forEach(function (marker) { + if (marker.name.indexOf("data-wrn-bind-") !== 0) return; + var binding; + try { binding = JSON.parse(marker.value); } catch (e) { return; } + if (!binding || binding.length !== 2) return; + var name = binding[0]; + var template = binding[1]; + reactive(function () { + var exact = /^\{([^{}]+)\}$/.exec(template); + var raw; + if (exact) { + try { raw = evalExpr(exact[1].trim()); } catch (e) { return; } + } else { + raw = template.replace(/\{([^{}]+)\}/g, function (_, expr) { + try { + var value = evalExpr(expr.trim()); + return value == null ? "" : String(value); + } catch (e) { return ""; } + }); + } + if (raw === false || raw == null) node.removeAttribute(name); + else node.setAttribute(name, raw === true ? "" : String(raw)); + }); + }); + }); + // {{expr}} / {expr} interpolation in text nodes var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); var textNode; diff --git a/packages/csr/test/reactive.test.ts b/packages/csr/test/reactive.test.ts index 16d30bdc..330d198c 100644 --- a/packages/csr/test/reactive.test.ts +++ b/packages/csr/test/reactive.test.ts @@ -128,6 +128,23 @@ test("data-show toggles visibility on a reactive expression (tabs pattern)", () expect(disp("b")).toBe(""); }); +test("reactive attribute bindings update input and accessibility attributes", () => { + const win = mount( + `
+ + +
`, + ); + const input = win.document.getElementById("password") as unknown as HTMLInputElement; + const button = win.document.querySelector("button") as unknown as HTMLElement; + expect(input.type).toBe("password"); + expect(button.getAttribute("aria-label")).toBe("Show password"); + button.click(); + expect(input.type).toBe("text"); + expect(button.getAttribute("aria-label")).toBe("Hide password"); +}); + test("independent signals in one scope update correctly (dependency tracking)", () => { const win = mount( `
diff --git a/packages/db/package.json b/packages/db/package.json index 0c156694..f59b8128 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 3405c997..9e8b3e6b 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 867b9bf8..3d745cd9 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index b22e741f..6247a752 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 435d047c..9eebd901 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index f55f0973..266d5458 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 0f402791..110e4864 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 385707c5..16e606b6 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index 381ae2a8..4dfc1b9c 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index a004ac5e..54c76eb4 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index 2489c641..67ab2bc4 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 7446761c..77e42857 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index 4432ef9a..01050851 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 8f061ca9..a569c818 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index 05d66678..bc9d8780 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.24", + "version": "0.2.25", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index 1cd26e25..81ea33de 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index eb03f5f9..2abb39e3 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index b1170355..3b1d619c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index a7eca1c9..06443e5f 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index 7394766f..a6c785ea 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "module", "main": "src/index.ts",