@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.
- package/.conf/tsconfig.base.json +1 -0
- package/.conf/tsconfig.browser.json +1 -0
- package/.conf/tsconfig.node.json +1 -0
- package/dist/browser/index.d.mts +206 -7
- package/dist/browser/index.mjs +527 -65
- package/dist/browser/index.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.mts +44 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.ts +44 -1
- package/dist/node/directives/AreDirectiveFor.directive.js +102 -6
- package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.mjs +102 -6
- package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveShow.directive.d.mts +32 -0
- package/dist/node/directives/AreDirectiveShow.directive.d.ts +32 -0
- package/dist/node/directives/AreDirectiveShow.directive.js +81 -0
- package/dist/node/directives/AreDirectiveShow.directive.js.map +1 -0
- package/dist/node/directives/AreDirectiveShow.directive.mjs +71 -0
- package/dist/node/directives/AreDirectiveShow.directive.mjs.map +1 -0
- package/dist/node/engine/AreHTML.engine.d.mts +2 -1
- package/dist/node/engine/AreHTML.engine.d.ts +2 -1
- package/dist/node/engine/AreHTML.engine.js +8 -2
- package/dist/node/engine/AreHTML.engine.js.map +1 -1
- package/dist/node/engine/AreHTML.engine.mjs +8 -2
- package/dist/node/engine/AreHTML.engine.mjs.map +1 -1
- package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
- package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
- package/dist/node/engine/AreHTML.interpreter.js +29 -0
- package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
- package/dist/node/engine/AreHTML.interpreter.mjs +29 -0
- package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.d.mts +8 -1
- package/dist/node/engine/AreHTML.lifecycle.d.ts +8 -1
- package/dist/node/engine/AreHTML.lifecycle.js +46 -3
- package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.mjs +46 -3
- package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
- package/dist/node/helpers/AreScheduler.helper.d.mts +39 -0
- package/dist/node/helpers/AreScheduler.helper.d.ts +39 -0
- package/dist/node/helpers/AreScheduler.helper.js +40 -0
- package/dist/node/helpers/AreScheduler.helper.js.map +1 -0
- package/dist/node/helpers/AreScheduler.helper.mjs +40 -0
- package/dist/node/helpers/AreScheduler.helper.mjs.map +1 -0
- package/dist/node/index.d.mts +4 -1
- package/dist/node/index.d.ts +4 -1
- package/dist/node/index.js +21 -0
- package/dist/node/index.mjs +3 -0
- package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.js +2 -1
- package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
- package/dist/node/instructions/AreHTML.instructions.constants.mjs +2 -1
- package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
- package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
- package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
- package/dist/node/instructions/HideElement.instruction.d.mts +13 -0
- package/dist/node/instructions/HideElement.instruction.d.ts +13 -0
- package/dist/node/instructions/HideElement.instruction.js +31 -0
- package/dist/node/instructions/HideElement.instruction.js.map +1 -0
- package/dist/node/instructions/HideElement.instruction.mjs +24 -0
- package/dist/node/instructions/HideElement.instruction.mjs.map +1 -0
- package/dist/node/lib/AreRoot/AreRoot.component.d.mts +57 -3
- package/dist/node/lib/AreRoot/AreRoot.component.d.ts +57 -3
- package/dist/node/lib/AreRoot/AreRoot.component.js +138 -49
- package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.mjs +140 -51
- package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
- package/dist/node/lib/AreRoot/AreRootCache.context.d.mts +58 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.d.ts +58 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.js +106 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.js.map +1 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.mjs +99 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.mjs.map +1 -0
- package/examples/dashboard/dist/index.html +1 -1
- package/examples/dashboard/dist/{mq19zxz4-mnlgmd.js → mqh9ryml-xat335.js} +1922 -1316
- package/examples/dashboard/src/concept.ts +3 -2
- package/examples/for-perf/concept.ts +45 -0
- package/examples/for-perf/containers/UI.container.ts +161 -0
- package/examples/for-perf/dist/index.html +270 -0
- package/examples/for-perf/dist/mqh9ryde-m243t8.js +15223 -0
- package/examples/for-perf/dist/mqh9ryfo-6a8d0o.js +15223 -0
- package/examples/for-perf/dist/mqh9ryfq-4pf5cv.js +15223 -0
- package/examples/for-perf/public/index.html +270 -0
- package/examples/for-perf/src/components/PerfApp.component.ts +37 -0
- package/examples/for-perf/src/components/PerfControls.component.ts +34 -0
- package/examples/for-perf/src/components/PerfGrid.component.ts +225 -0
- package/examples/for-perf/src/components/PerfHeader.component.ts +34 -0
- package/examples/for-perf/src/components/PerfStats.component.ts +43 -0
- package/examples/for-perf/src/concept.ts +94 -0
- package/examples/jumpstart/dist/index.html +1 -1
- package/examples/jumpstart/dist/{mq1a0fv0-ccgtz6.js → mq7mgf58-vbf07e.js} +895 -521
- package/examples/signal-routing/dist/index.html +1 -1
- package/examples/signal-routing/dist/{mq1bzrik-4lec86.js → mqh9ryc9-dkcbkx.js} +2024 -1300
- package/examples/signal-routing/src/components/SettingsPage.component.ts +39 -0
- package/examples/signal-routing/src/concept.ts +2 -0
- package/jest.config.ts +1 -0
- package/package.json +10 -9
- package/src/directives/AreDirectiveFor.directive.ts +185 -12
- package/src/directives/AreDirectiveShow.directive.ts +127 -0
- package/src/engine/AreHTML.engine.ts +11 -1
- package/src/engine/AreHTML.interpreter.ts +50 -0
- package/src/engine/AreHTML.lifecycle.ts +83 -6
- package/src/helpers/AreScheduler.helper.ts +61 -0
- package/src/index.ts +3 -0
- package/src/instructions/AreHTML.instructions.constants.ts +1 -0
- package/src/instructions/AreHTML.instructions.types.ts +9 -0
- package/src/instructions/HideElement.instruction.ts +29 -0
- package/src/lib/AreRoot/AreRoot.component.ts +205 -72
- package/src/lib/AreRoot/AreRootCache.context.ts +133 -0
- 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.
|
|
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.
|
|
86
|
-
"@adaas/a-frame": "^0.1.
|
|
87
|
-
"@adaas/a-utils": "^0.3.
|
|
88
|
-
"@adaas/are": "^0.0.
|
|
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.
|
|
92
|
-
"@adaas/a-frame": "^0.1.
|
|
93
|
-
"@adaas/a-utils": "^0.3.
|
|
94
|
-
"@adaas/are": "^0.0.
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
188
|
-
|
|
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
|
-
|
|
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
|
// ─────────────────────────────────────────────────────────────────────────────
|