@adaas/are-html 0.0.20 → 0.0.22

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.
Files changed (109) hide show
  1. package/.conf/tsconfig.base.json +1 -0
  2. package/.conf/tsconfig.browser.json +1 -0
  3. package/.conf/tsconfig.node.json +1 -0
  4. package/dist/browser/index.d.mts +206 -7
  5. package/dist/browser/index.mjs +527 -65
  6. package/dist/browser/index.mjs.map +1 -1
  7. package/dist/node/directives/AreDirectiveFor.directive.d.mts +44 -1
  8. package/dist/node/directives/AreDirectiveFor.directive.d.ts +44 -1
  9. package/dist/node/directives/AreDirectiveFor.directive.js +102 -6
  10. package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
  11. package/dist/node/directives/AreDirectiveFor.directive.mjs +102 -6
  12. package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
  13. package/dist/node/directives/AreDirectiveShow.directive.d.mts +32 -0
  14. package/dist/node/directives/AreDirectiveShow.directive.d.ts +32 -0
  15. package/dist/node/directives/AreDirectiveShow.directive.js +81 -0
  16. package/dist/node/directives/AreDirectiveShow.directive.js.map +1 -0
  17. package/dist/node/directives/AreDirectiveShow.directive.mjs +71 -0
  18. package/dist/node/directives/AreDirectiveShow.directive.mjs.map +1 -0
  19. package/dist/node/engine/AreHTML.engine.d.mts +2 -1
  20. package/dist/node/engine/AreHTML.engine.d.ts +2 -1
  21. package/dist/node/engine/AreHTML.engine.js +8 -2
  22. package/dist/node/engine/AreHTML.engine.js.map +1 -1
  23. package/dist/node/engine/AreHTML.engine.mjs +8 -2
  24. package/dist/node/engine/AreHTML.engine.mjs.map +1 -1
  25. package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
  26. package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
  27. package/dist/node/engine/AreHTML.interpreter.js +29 -0
  28. package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
  29. package/dist/node/engine/AreHTML.interpreter.mjs +29 -0
  30. package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
  31. package/dist/node/engine/AreHTML.lifecycle.d.mts +8 -1
  32. package/dist/node/engine/AreHTML.lifecycle.d.ts +8 -1
  33. package/dist/node/engine/AreHTML.lifecycle.js +46 -3
  34. package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
  35. package/dist/node/engine/AreHTML.lifecycle.mjs +46 -3
  36. package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
  37. package/dist/node/helpers/AreScheduler.helper.d.mts +39 -0
  38. package/dist/node/helpers/AreScheduler.helper.d.ts +39 -0
  39. package/dist/node/helpers/AreScheduler.helper.js +40 -0
  40. package/dist/node/helpers/AreScheduler.helper.js.map +1 -0
  41. package/dist/node/helpers/AreScheduler.helper.mjs +40 -0
  42. package/dist/node/helpers/AreScheduler.helper.mjs.map +1 -0
  43. package/dist/node/index.d.mts +4 -1
  44. package/dist/node/index.d.ts +4 -1
  45. package/dist/node/index.js +21 -0
  46. package/dist/node/index.mjs +3 -0
  47. package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
  48. package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
  49. package/dist/node/instructions/AreHTML.instructions.constants.js +2 -1
  50. package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
  51. package/dist/node/instructions/AreHTML.instructions.constants.mjs +2 -1
  52. package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
  53. package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
  54. package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
  55. package/dist/node/instructions/HideElement.instruction.d.mts +13 -0
  56. package/dist/node/instructions/HideElement.instruction.d.ts +13 -0
  57. package/dist/node/instructions/HideElement.instruction.js +31 -0
  58. package/dist/node/instructions/HideElement.instruction.js.map +1 -0
  59. package/dist/node/instructions/HideElement.instruction.mjs +24 -0
  60. package/dist/node/instructions/HideElement.instruction.mjs.map +1 -0
  61. package/dist/node/lib/AreRoot/AreRoot.component.d.mts +57 -3
  62. package/dist/node/lib/AreRoot/AreRoot.component.d.ts +57 -3
  63. package/dist/node/lib/AreRoot/AreRoot.component.js +138 -49
  64. package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
  65. package/dist/node/lib/AreRoot/AreRoot.component.mjs +140 -51
  66. package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
  67. package/dist/node/lib/AreRoot/AreRootCache.context.d.mts +58 -0
  68. package/dist/node/lib/AreRoot/AreRootCache.context.d.ts +58 -0
  69. package/dist/node/lib/AreRoot/AreRootCache.context.js +106 -0
  70. package/dist/node/lib/AreRoot/AreRootCache.context.js.map +1 -0
  71. package/dist/node/lib/AreRoot/AreRootCache.context.mjs +99 -0
  72. package/dist/node/lib/AreRoot/AreRootCache.context.mjs.map +1 -0
  73. package/examples/dashboard/dist/index.html +1 -1
  74. package/examples/dashboard/dist/{mq19zxz4-mnlgmd.js → mqh9ryml-xat335.js} +1922 -1316
  75. package/examples/dashboard/src/concept.ts +3 -2
  76. package/examples/for-perf/concept.ts +45 -0
  77. package/examples/for-perf/containers/UI.container.ts +161 -0
  78. package/examples/for-perf/dist/index.html +270 -0
  79. package/examples/for-perf/dist/mqh9ryde-m243t8.js +15223 -0
  80. package/examples/for-perf/dist/mqh9ryfo-6a8d0o.js +15223 -0
  81. package/examples/for-perf/dist/mqh9ryfq-4pf5cv.js +15223 -0
  82. package/examples/for-perf/public/index.html +270 -0
  83. package/examples/for-perf/src/components/PerfApp.component.ts +37 -0
  84. package/examples/for-perf/src/components/PerfControls.component.ts +34 -0
  85. package/examples/for-perf/src/components/PerfGrid.component.ts +225 -0
  86. package/examples/for-perf/src/components/PerfHeader.component.ts +34 -0
  87. package/examples/for-perf/src/components/PerfStats.component.ts +43 -0
  88. package/examples/for-perf/src/concept.ts +94 -0
  89. package/examples/jumpstart/dist/index.html +1 -1
  90. package/examples/jumpstart/dist/{mq1a0fv0-ccgtz6.js → mq7mgf58-vbf07e.js} +895 -521
  91. package/examples/signal-routing/dist/index.html +1 -1
  92. package/examples/signal-routing/dist/{mq1bzrik-4lec86.js → mqh9ryc9-dkcbkx.js} +2024 -1300
  93. package/examples/signal-routing/src/components/SettingsPage.component.ts +39 -0
  94. package/examples/signal-routing/src/concept.ts +2 -0
  95. package/jest.config.ts +1 -0
  96. package/package.json +10 -9
  97. package/src/directives/AreDirectiveFor.directive.ts +185 -12
  98. package/src/directives/AreDirectiveShow.directive.ts +127 -0
  99. package/src/engine/AreHTML.engine.ts +11 -1
  100. package/src/engine/AreHTML.interpreter.ts +50 -0
  101. package/src/engine/AreHTML.lifecycle.ts +83 -6
  102. package/src/helpers/AreScheduler.helper.ts +61 -0
  103. package/src/index.ts +3 -0
  104. package/src/instructions/AreHTML.instructions.constants.ts +1 -0
  105. package/src/instructions/AreHTML.instructions.types.ts +9 -0
  106. package/src/instructions/HideElement.instruction.ts +29 -0
  107. package/src/lib/AreRoot/AreRoot.component.ts +205 -72
  108. package/src/lib/AreRoot/AreRootCache.context.ts +133 -0
  109. package/tsconfig.json +1 -0
@@ -30,6 +30,33 @@ export class SettingsPage extends Are {
30
30
  </label>
31
31
  </div>
32
32
 
33
+ <div class="settings-group">
34
+ <h2>Directive demo · <code>$show</code> vs <code>$if</code></h2>
35
+ <p class="hint">
36
+ Type something into both boxes below, then toggle <strong>Compact layout</strong>
37
+ twice. Both panels react to the same <code>compact</code> store flag, but:
38
+ </p>
39
+
40
+ <div class="demo-panel demo-show" $show="!compact">
41
+ <span class="demo-tag">$show</span>
42
+ <p>
43
+ Toggled with <code>$show</code> — I stay <strong>mounted</strong> and only my
44
+ inline <code>display</code> flips. Your text below <strong>survives</strong>
45
+ the toggle because the DOM node is never destroyed.
46
+ </p>
47
+ <input type="text" placeholder="Scratch text (survives toggle)…" />
48
+ </div>
49
+
50
+ <div class="demo-panel demo-if" $if="!compact">
51
+ <span class="demo-tag">$if</span>
52
+ <p>
53
+ Toggled with <code>$if</code> — I am <strong>unmounted</strong> and rebuilt
54
+ each time I reappear. Your text below is <strong>wiped</strong> on every toggle.
55
+ </p>
56
+ <input type="text" placeholder="Scratch text (lost on toggle)…" />
57
+ </div>
58
+ </div>
59
+
33
60
  <div class="settings-group">
34
61
  <h2>Display name</h2>
35
62
  <div class="input-row">
@@ -75,6 +102,18 @@ export class SettingsPage extends Are {
75
102
  .preview strong { color: #a78bfa; }
76
103
  .hint { font-size: 13px; color: #71717a; line-height: 1.7; }
77
104
  .hint code { background: #27272a; padding: 1px 6px; border-radius: 4px; color: #a78bfa; font-size: 12px; }
105
+ .settings-group h2 code { background: #27272a; padding: 1px 6px; border-radius: 4px; color: #a78bfa; font-size: 12px; font-weight: 600; }
106
+ .demo-panel { position: relative; background: #1c1c1f; border: 1px solid #27272a; border-radius: 10px; padding: 18px 18px 18px 20px; margin-top: 14px; }
107
+ .demo-panel p { font-size: 13px; color: #a1a1aa; line-height: 1.7; margin: 0 0 12px; }
108
+ .demo-panel p code { background: #27272a; padding: 1px 5px; border-radius: 3px; color: #a78bfa; font-size: 12px; }
109
+ .demo-panel strong { color: #e4e4e7; }
110
+ .demo-panel input[type="text"] { background: #131316; border: 1px solid #3f3f46; border-radius: 8px; color: #f4f4f5; padding: 8px 14px; font-size: 13px; outline: none; width: 100%; box-sizing: border-box; }
111
+ .demo-panel input[type="text"]:focus { border-color: #a78bfa; }
112
+ .demo-tag { display: inline-block; font-family: monospace; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 6px; margin-bottom: 10px; }
113
+ .demo-show { border-left: 3px solid #34d399; }
114
+ .demo-show .demo-tag { background: rgba(52, 211, 153, 0.12); color: #34d399; }
115
+ .demo-if { border-left: 3px solid #f59e0b; }
116
+ .demo-if .demo-tag { background: rgba(245, 158, 11, 0.12); color: #f59e0b; }
78
117
  `);
79
118
  }
80
119
 
@@ -14,6 +14,7 @@ import {
14
14
  AreHTMLEngineContext,
15
15
  AreDirectiveIf,
16
16
  AreDirectiveFor,
17
+ AreDirectiveShow,
17
18
  AreRouteWatcher,
18
19
  } from "src";
19
20
 
@@ -64,6 +65,7 @@ import { AreRoute as AreRouteSignal } from "src/signals/AreRoute.signal";
64
65
  // ── Directives ───────────────────────────────────────────
65
66
  AreDirectiveIf,
66
67
  AreDirectiveFor,
68
+ AreDirectiveShow,
67
69
  // ── Engine ───────────────────────────────────────────────
68
70
  A_SignalBus,
69
71
  AreRoot,
package/jest.config.ts CHANGED
@@ -17,6 +17,7 @@ const config: Config.InitialOptions = {
17
17
  "@adaas/are-html/instructions/(.*)": "<rootDir>/src/instructions/$1",
18
18
  "@adaas/are-html/watchers/(.*)": "<rootDir>/src/watchers/$1",
19
19
  "@adaas/are-html/signals/(.*)": "<rootDir>/src/signals/$1",
20
+ "@adaas/are-html/helpers/(.*)": "<rootDir>/src/helpers/$1",
20
21
  // Custom lib exports
21
22
  "@adaas/are-html/style/(.*)": "<rootDir>/src/lib/AreStyle/$1",
22
23
  "@adaas/are-html/directive/(.*)": "<rootDir>/src/lib/AreDirective/$1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaas/are-html",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "A-Concept Rendering Engine (ARE) is a powerful rendering engine designed to work seamlessly with the A-Concept framework. This library provides an HTML engine implementation of ARE, enabling developers to create dynamic and interactive user interfaces for web applications using standard HTML syntax.",
5
5
  "keywords": [
6
6
  "a-concept",
@@ -73,6 +73,7 @@
73
73
  "example:signal-routing": "nodemon ./examples/signal-routing/concept.ts",
74
74
  "example:component-styles": "nodemon ./examples/component-styles/concept.ts",
75
75
  "example:auxta": "nodemon ./examples/auxta/concept.ts",
76
+ "example:for-perf": "nodemon ./examples/for-perf/concept.ts",
76
77
  "test:test": "nodemon ./src/test/app.ts",
77
78
  "release": " npm run build && git add . && git commit -m \"new version created :: $(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g')\" && npm version patch && npm publish --access public",
78
79
  "preversion": "echo test",
@@ -82,16 +83,16 @@
82
83
  "build": "tsup --config tsup.config.ts"
83
84
  },
84
85
  "peerDependencies": {
85
- "@adaas/a-concept": "^0.3.27",
86
- "@adaas/a-frame": "^0.1.14",
87
- "@adaas/a-utils": "^0.3.32",
88
- "@adaas/are": "^0.0.20"
86
+ "@adaas/a-concept": "^0.3.29",
87
+ "@adaas/a-frame": "^0.1.17",
88
+ "@adaas/a-utils": "^0.3.34",
89
+ "@adaas/are": "^0.0.23"
89
90
  },
90
91
  "devDependencies": {
91
- "@adaas/a-concept": "^0.3.27",
92
- "@adaas/a-frame": "^0.1.14",
93
- "@adaas/a-utils": "^0.3.32",
94
- "@adaas/are": "^0.0.20",
92
+ "@adaas/a-concept": "^0.3.29",
93
+ "@adaas/a-frame": "^0.1.17",
94
+ "@adaas/a-utils": "^0.3.34",
95
+ "@adaas/are": "^0.0.23",
95
96
  "@types/chai": "^4.3.14",
96
97
  "@types/jest": "^29.5.12",
97
98
  "@types/mocha": "^10.0.6",
@@ -7,6 +7,7 @@ import { AddCommentInstruction } from "@adaas/are-html/instructions/AddComment.i
7
7
  import { AreHTMLNode } from "@adaas/are-html/node";
8
8
  import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
9
9
  import { A_Frame } from "@adaas/a-frame/core";
10
+ import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
10
11
 
11
12
 
12
13
  type AreForExpression = {
@@ -17,6 +18,12 @@ type AreForExpression = {
17
18
  trackExpr: string | undefined;
18
19
  };
19
20
 
21
+ /**
22
+ * Per-`$for` reentrancy state used to serialize chunked (async) renders.
23
+ * Keyed by the directive attribute instance (one per `$for` in the template).
24
+ */
25
+ type AreForRenderState = { running: boolean; pending: boolean };
26
+
20
27
 
21
28
  @A_Frame.Define({
22
29
  namespace: 'a-are-html',
@@ -25,6 +32,32 @@ type AreForExpression = {
25
32
  @AreDirective.Priority(1)
26
33
  export class AreDirectiveFor extends AreDirective {
27
34
 
35
+ /**
36
+ * Lists whose number of NEW item nodes is at or below this threshold render
37
+ * fully synchronously — byte-for-byte the previous behavior. Typical UIs
38
+ * (menus, small tables) are therefore completely unaffected; only genuinely
39
+ * large lists pay the (tiny) scheduling cost to keep the main thread responsive.
40
+ */
41
+ private static readonly SYNC_THRESHOLD = 100;
42
+
43
+ /**
44
+ * Per-chunk time budget (ms). During a large-list render we mount item nodes
45
+ * until this much time has elapsed, then yield to the browser so it can paint
46
+ * and process input before the next chunk. ~16ms targets one animation frame.
47
+ */
48
+ private static readonly CHUNK_BUDGET_MS = 16;
49
+
50
+ /**
51
+ * Per-attribute serialization state. A new update() that arrives while a
52
+ * chunked render of the SAME `$for` is still in flight does NOT start a second
53
+ * concurrent pass (which could interleave mutations on the shared children
54
+ * list); instead it marks `pending` and the in-flight run re-runs once more
55
+ * with the latest data when it finishes. This guarantees the children list is
56
+ * only ever mutated by one pass at a time and the final state always reflects
57
+ * the most recent store value.
58
+ */
59
+ private static readonly renderState = new WeakMap<object, AreForRenderState>();
60
+
28
61
 
29
62
  @AreDirective.Transform
30
63
  transform(
@@ -122,7 +155,40 @@ export class AreDirectiveFor extends AreDirective {
122
155
  @A_Inject(AreStore) store: AreStore,
123
156
  @A_Inject(AreScene) scene: AreScene,
124
157
  ...args: any[]
125
- ): void {
158
+ ): void | Promise<void> {
159
+ /**
160
+ * Serialize chunked renders per `$for`. If a previous large-list render
161
+ * is still streaming item nodes across macrotasks, do NOT start a second
162
+ * concurrent pass — that would interleave two diffs over the same shared
163
+ * children list (and leave half-compiled item nodes that the next diff
164
+ * would wrongly "reuse"). Mark a pass as pending instead; the in-flight
165
+ * run re-diffs once more from the latest store value when it completes.
166
+ */
167
+ let state = AreDirectiveFor.renderState.get(attribute);
168
+ if (!state) {
169
+ state = { running: false, pending: false };
170
+ AreDirectiveFor.renderState.set(attribute, state);
171
+ }
172
+ if (state.running) {
173
+ state.pending = true;
174
+ return;
175
+ }
176
+
177
+ return this.performUpdate(attribute, store, scene, state);
178
+ }
179
+
180
+ /**
181
+ * Core of the `$for` update: re-diff the source array against the current
182
+ * children, reconcile reused/removed items, then mount the new ones (small
183
+ * lists synchronously, large lists time-sliced). Never called while another
184
+ * pass for the same `$for` is in flight (see `update`).
185
+ */
186
+ private performUpdate(
187
+ attribute: AreDirectiveAttribute,
188
+ store: AreStore,
189
+ scene: AreScene,
190
+ state: AreForRenderState,
191
+ ): void | Promise<void> {
126
192
  /**
127
193
  * Re-evaluate the source array.
128
194
  */
@@ -134,6 +200,25 @@ export class AreDirectiveFor extends AreDirective {
134
200
 
135
201
  attribute.value = newArray;
136
202
 
203
+ /**
204
+ * Is this `$for`'s subtree currently rendered into the DOM?
205
+ *
206
+ * A `$for` can update while its subtree is detached — e.g. it lives
207
+ * inside a `$if` whose condition is currently false (the documented
208
+ * `<div $if><x $for></div>` nesting). The directive still receives the
209
+ * store change and re-diffs, but it must NOT mount/unmount item nodes
210
+ * directly while detached: the `$for` anchor (and its ancestors) are
211
+ * not in the DOM, so the interpreter's mount-point walk would fall
212
+ * through to the nearest *mounted* ancestor (the `$if` comment in the
213
+ * grandparent) and HOIST the items out of their intended container.
214
+ * When the ancestor `$if` later activates, its mount cascade applies
215
+ * the already-compiled item instructions in the correct place.
216
+ *
217
+ * Detached === any ancestor scene is inactive (regular nodes default
218
+ * to an active scene; only structural directives deactivate one).
219
+ */
220
+ const attached = this.isAttached(owner);
221
+
137
222
  const computeKey = this.makeKeyFn(key, index, trackExpr);
138
223
 
139
224
  // ── 1. Index existing children by stable key ────────────────────────
@@ -148,9 +233,13 @@ export class AreDirectiveFor extends AreDirective {
148
233
  remaining.add(child);
149
234
  }
150
235
 
151
- // ── 2. Walk desired list; reuse existing or spawn new ───────────────
152
- const desired: AreHTMLNode[] = [];
153
- const newOnes: AreHTMLNode[] = [];
236
+ // ── 2. Walk desired list; reuse existing or record items to create ──
237
+ // NOTE: new item nodes are NOT spawned here. Spawning (cloneWithScope +
238
+ // subtree init + scene activation) is the dominant cost of a large
239
+ // render, so it is deferred into the time-sliced loop below alongside
240
+ // transform/compile/mount. Existing (keyed) children are reconciled in
241
+ // place synchronously — that is cheap and keeps reused rows stable.
242
+ const toCreate: Array<{ item: any; idx: number }> = [];
154
243
 
155
244
  for (let i = 0; i < newArray.length; i++) {
156
245
  const item = newArray[i];
@@ -170,26 +259,110 @@ export class AreDirectiveFor extends AreDirective {
170
259
  [key]: item,
171
260
  [index || 'index']: i,
172
261
  };
173
- desired.push(existing);
174
262
  } else {
175
- const itemNode = this.spawnItemNode(attribute.template!, owner, key, index, item, i);
176
- desired.push(itemNode);
177
- newOnes.push(itemNode);
263
+ toCreate.push({ item, idx: i });
178
264
  }
179
265
  }
180
266
 
181
267
  // ── 3. Unmount + detach removed children ─────────────────────────────
182
268
  for (const child of remaining) {
183
- child.unmount();
269
+ // Only revert DOM if the subtree is live; a detached subtree's item
270
+ // nodes were never mounted (see `attached` rationale above), so
271
+ // unmounting them is a no-op at best and risks reverting stale state.
272
+ if (attached) child.unmount();
184
273
  owner.removeChild(child);
185
274
  }
186
275
 
187
- // ── 4. Mount only the new ones (kept children stay where they are).
188
- for (const child of newOnes) {
276
+ // ── 4. Create + mount the new item nodes. ───────────────────────────
277
+ // `spawnItemNode` appends to `owner.children` immediately, so iterating
278
+ // `toCreate` in source order preserves list order (reused children keep
279
+ // their positions, new rows are appended in order) — identical to the
280
+ // previous synchronous behavior.
281
+ const createItem = (desc: { item: any; idx: number }) => {
282
+ const child = this.spawnItemNode(attribute.template!, owner, key, index, desc.item, desc.idx);
189
283
  child.transform();
190
284
  child.compile();
191
- child.mount();
285
+ // While detached, stop after compile: the item's instructions are
286
+ // planned and the ancestor `$if`'s mount cascade will apply them in
287
+ // the correct container once the condition becomes truthy. Mounting
288
+ // here would hoist the item to the nearest mounted ancestor.
289
+ if (attached) child.mount();
290
+ };
291
+
292
+ // Small lists → fully synchronous, identical to the previous behavior.
293
+ if (toCreate.length <= AreDirectiveFor.SYNC_THRESHOLD) {
294
+ for (const desc of toCreate) createItem(desc);
295
+ return this.finishUpdate(attribute, store, scene, state);
296
+ }
297
+
298
+ // Large lists → time-sliced render. Create item nodes until the frame
299
+ // budget elapses, then yield to the browser (zero-delay macrotask) so
300
+ // it can paint and stay responsive instead of blocking for the whole
301
+ // batch. The `state.running` flag (see `update`) prevents any other
302
+ // update() for this `$for` from interleaving while we stream.
303
+ state.running = true;
304
+ let cursor = 0;
305
+
306
+ const processChunk = (): void | Promise<void> => {
307
+ try {
308
+ const start = AreSchedulerHelper.now();
309
+ while (cursor < toCreate.length) {
310
+ createItem(toCreate[cursor]);
311
+ cursor++;
312
+ if (AreSchedulerHelper.now() - start >= AreDirectiveFor.CHUNK_BUDGET_MS) break;
313
+ }
314
+ } catch (error) {
315
+ // Never leave the `$for` wedged in the running state on failure,
316
+ // or every future update would be silently deferred forever.
317
+ state.running = false;
318
+ state.pending = false;
319
+ throw error;
320
+ }
321
+
322
+ if (cursor < toCreate.length) {
323
+ return new Promise<void>(resolve => {
324
+ AreSchedulerHelper.scheduleMacrotask(() => resolve(processChunk()));
325
+ });
326
+ }
327
+
328
+ return this.finishUpdate(attribute, store, scene, state);
329
+ };
330
+
331
+ return processChunk();
332
+ }
333
+
334
+ /**
335
+ * Completes an update pass. If another update() arrived while a chunked
336
+ * render was streaming, run exactly one more pass now from the latest store
337
+ * value so the final DOM always reflects the most recent data.
338
+ */
339
+ private finishUpdate(
340
+ attribute: AreDirectiveAttribute,
341
+ store: AreStore,
342
+ scene: AreScene,
343
+ state: AreForRenderState,
344
+ ): void | Promise<void> {
345
+ state.running = false;
346
+ if (state.pending) {
347
+ state.pending = false;
348
+ return this.performUpdate(attribute, store, scene, state);
349
+ }
350
+ }
351
+
352
+
353
+ /**
354
+ * Walks the node's ancestor chain (inclusive) and reports whether the
355
+ * whole path is currently active — i.e. the subtree is actually rendered
356
+ * into the DOM. A single inactive ancestor scene (e.g. a `$if` whose
357
+ * condition is false) means the subtree is detached.
358
+ */
359
+ private isAttached(node: AreHTMLNode): boolean {
360
+ let current: AreHTMLNode | undefined = node;
361
+ while (current) {
362
+ if (current.scene?.isInactive) return false;
363
+ current = current.parent as AreHTMLNode | undefined;
192
364
  }
365
+ return true;
193
366
  }
194
367
 
195
368
 
@@ -0,0 +1,127 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { A_Logger } from "@adaas/a-utils/a-logger";
3
+ import { AreDirectiveAttribute } from "@adaas/are-html/attributes/AreDirective.attribute";
4
+ import { AreScene, AreStore, AreSyntax } from "@adaas/are";
5
+ import { AreDirective } from "@adaas/are-html/directive/AreDirective.component";
6
+ import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
7
+ import { HideElementInstruction } from "@adaas/are-html/instructions/HideElement.instruction";
8
+ import { A_Frame } from "@adaas/a-frame/core";
9
+
10
+
11
+
12
+ /**
13
+ * `$show` directive — conditionally toggles an element's visibility.
14
+ *
15
+ * Unlike `$if`, `$show` keeps the element fully mounted at all times and only
16
+ * flips its inline `display` (Vue `v-show` semantics). The element's subtree,
17
+ * event listeners and scene state are preserved across toggles, which makes it
18
+ * far cheaper than `$if` for things that flip on/off frequently. Use `$if` when
19
+ * the hidden branch is expensive and rarely shown; use `$show` when it toggles
20
+ * often.
21
+ *
22
+ * ⚠️ Known limitations:
23
+ * - Do NOT combine `$show` with `$if`/`$for` on the SAME element — they share
24
+ * an owner node and would fight over its host instruction. Wrap one in a
25
+ * parent element instead.
26
+ * - `$show` forces inline `display:none`, which beats stylesheet rules but will
27
+ * NOT override the element's own inline `:style="display:..."` binding.
28
+ */
29
+ @A_Frame.Define({
30
+ namespace: 'a-are-html',
31
+ description: 'Built-in $show directive. Toggles an element\'s visibility by flipping its inline display value based on a store expression, keeping the element mounted (subtree, listeners and scene state preserved) instead of unmounting it like $if.'
32
+ })
33
+ @AreDirective.Priority(3)
34
+ export class AreDirectiveShow extends AreDirective {
35
+
36
+
37
+ @AreDirective.Transform
38
+ transform(
39
+ @A_Inject(A_Caller) attribute: AreDirectiveAttribute,
40
+ @A_Inject(A_Logger) logger: A_Logger,
41
+ ...args: any[]
42
+ ) {
43
+ // $show makes no structural change to the tree — the element stays in
44
+ // place and is only hidden/shown at interpret time. Nothing to do here
45
+ // beyond overriding the base directive's default transform warning.
46
+ logger.debug(`[Transform] directive $SHOW for <${attribute.owner.aseid.toString()}> (no structural change)`)
47
+ }
48
+
49
+
50
+ @AreDirective.Compile
51
+ compile(
52
+ @A_Inject(A_Caller) attribute: AreDirectiveAttribute,
53
+ @A_Inject(AreStore) store: AreStore,
54
+ @A_Inject(AreScene) scene: AreScene,
55
+ @A_Inject(AreSyntax) syntax: AreSyntax,
56
+
57
+ @A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
58
+ ...args: any[]
59
+ ): void {
60
+ /**
61
+ * 1. Evaluate the expression to determine the initial visibility.
62
+ */
63
+ const visible = !!syntax.evaluate(attribute.content, store, {
64
+ ...(directiveContext?.scope || {}),
65
+ });
66
+
67
+ attribute.value = visible;
68
+
69
+ /**
70
+ * 2. Create a single reusable HideElement mutation parented to the
71
+ * element's host instruction, and cache it on the attribute so
72
+ * update() can plan/unplan the exact same instance.
73
+ */
74
+ const hide = new HideElementInstruction(scene.host!, {});
75
+ attribute.cache = hide;
76
+
77
+ /**
78
+ * 3. When initially hidden, plan the mutation so the first interpret
79
+ * applies `display:none`. When visible, leave it unplanned.
80
+ */
81
+ if (!visible)
82
+ scene.plan(hide);
83
+ }
84
+
85
+
86
+ @AreDirective.Update
87
+ update(
88
+ @A_Inject(A_Caller) attribute: AreDirectiveAttribute,
89
+ @A_Inject(AreStore) store: AreStore,
90
+ @A_Inject(AreScene) scene: AreScene,
91
+ @A_Inject(AreSyntax) syntax: AreSyntax,
92
+
93
+ @A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
94
+ ...args: any[]
95
+ ): void {
96
+ /**
97
+ * 1. Re-evaluate the expression, forwarding the directive context scope
98
+ * (e.g. a `$for` loop variable) so `$show` reacts correctly inside
99
+ * loops.
100
+ */
101
+ const previous = !!attribute.value;
102
+ const next = !!syntax.evaluate(attribute.content, store, {
103
+ ...(directiveContext?.scope || {}),
104
+ });
105
+
106
+ attribute.value = next;
107
+
108
+ // Skip when visibility has not changed — avoids redundant DOM writes.
109
+ if (previous === next) return;
110
+
111
+ const hide = attribute.cache as HideElementInstruction | undefined;
112
+
113
+ if (!hide) return;
114
+
115
+ /**
116
+ * 2. Toggle the cached mutation: unplan to reveal, plan to hide. Then
117
+ * re-interpret the owner so the scene diff applies/reverts it.
118
+ */
119
+ if (next)
120
+ scene.unPlan(hide);
121
+ else
122
+ scene.plan(hide);
123
+
124
+ attribute.owner.interpret();
125
+ }
126
+
127
+ }
@@ -14,6 +14,7 @@ import { AreHTMLLifecycle } from "@adaas/are-html/lifecycle";
14
14
  import { AreHTMLTransformer } from "@adaas/are-html/transformer";
15
15
  import { AreHTMLCompiler } from "./AreHTML.compiler";
16
16
  import { isVoidElement } from "./AreHTML.constants";
17
+ import { AreRootCache } from "../lib/AreRoot/AreRootCache.context";
17
18
 
18
19
 
19
20
 
@@ -81,7 +82,8 @@ export class AreHTMLEngine extends AreEngine {
81
82
  })
82
83
  async init(
83
84
  @A_Inject(A_Scope) scope: A_Scope,
84
- @A_Inject(AreSignalsContext) signalContext?: AreSignalsContext
85
+ @A_Inject(AreSignalsContext) signalContext?: AreSignalsContext,
86
+ @A_Inject(AreRootCache) rootCache?: AreRootCache
85
87
  ) {
86
88
  this.package(scope, {
87
89
  context: new AreHTMLEngineContext({}),
@@ -97,6 +99,14 @@ export class AreHTMLEngine extends AreEngine {
97
99
  signalContext = new AreSignalsContext();
98
100
  scope.register(signalContext);
99
101
  }
102
+
103
+ // Default per-root subtree cache used by AreRoot for fast route-back
104
+ // re-injection. Apps may register their own (e.g. with a custom limit)
105
+ // before load to override this.
106
+ if (!rootCache) {
107
+ rootCache = new AreRootCache();
108
+ scope.register(rootCache);
109
+ }
100
110
  }
101
111
 
102
112
 
@@ -15,6 +15,7 @@ import { AddElementInstruction } from "@adaas/are-html/instructions/AddElement.i
15
15
  import { AddListenerInstruction } from "@adaas/are-html/instructions/AddListener.instruction";
16
16
  import { AddTextInstruction } from "@adaas/are-html/instructions/AddText.instruction";
17
17
  import { AddStyleInstruction } from "@adaas/are-html/instructions/AddStyle.instruction";
18
+ import { HideElementInstruction } from "@adaas/are-html/instructions/HideElement.instruction";
18
19
  import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
19
20
  import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
20
21
  import { AreHTMLEngineContext } from "./AreHTML.context";
@@ -302,6 +303,55 @@ export class AreHTMLInterpreter extends AreInterpreter {
302
303
  }
303
304
 
304
305
 
306
+ // ─────────────────────────────────────────────────────────────────────────────
307
+ // ── HideElement — Apply / Revert ─────────────────────────────────────────────
308
+ // ─────────────────────────────────────────────────────────────────────────────
309
+ // Drives the `$show` directive. Apply hides the element by forcing its inline
310
+ // `display:none` (which beats stylesheet rules and, unlike rewriting the whole
311
+ // `style` attribute, does NOT clobber other inline styles or `:style`
312
+ // bindings). The element stays mounted — its subtree, listeners and scene
313
+ // state are preserved — so toggling visibility is far cheaper than $if's
314
+ // mount/unmount cycle. Revert restores the element's previous inline display.
315
+ @A_Frame.Define({
316
+ description: 'Hide an element by setting inline display:none, caching its previous inline display value for restoration on revert.'
317
+ })
318
+ @AreInterpreter.Apply(AreHTMLInstructions.HideElement)
319
+ hideElement(
320
+ @A_Inject(A_Caller) mutation: HideElementInstruction,
321
+ @A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
322
+ ): void {
323
+ const element = context.getElementByInstruction(mutation.parent!);
324
+
325
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
326
+
327
+ const el = element as HTMLElement;
328
+
329
+ // Remember the element's own inline display so it can be restored exactly.
330
+ mutation.cache = el.style.display;
331
+ el.style.display = 'none';
332
+ }
333
+
334
+ @A_Frame.Define({
335
+ description: 'Restore an element hidden by a HideElement instruction back to its previous inline display value.'
336
+ })
337
+ @AreInterpreter.Revert(AreHTMLInstructions.HideElement)
338
+ showElement(
339
+ @A_Inject(A_Caller) mutation: HideElementInstruction,
340
+ @A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
341
+ ): void {
342
+ const element = context.getElementByInstruction(mutation.parent!);
343
+
344
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
345
+
346
+ const el = element as HTMLElement;
347
+
348
+ // Restore the cached inline display. An explicit payload display, when
349
+ // provided, takes precedence; otherwise fall back to the cached value
350
+ // (empty string clears the inline rule and reverts to the CSS default).
351
+ el.style.display = mutation.payload?.display ?? mutation.cache ?? '';
352
+ }
353
+
354
+
305
355
  // ─────────────────────────────────────────────────────────────────────────────
306
356
  // ── addEventListener — Apply / Revert ────────────────────────────────────────
307
357
  // ─────────────────────────────────────────────────────────────────────────────