@adaas/are-html 0.0.23 → 0.0.25

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 (81) hide show
  1. package/dist/browser/index.d.mts +18 -2
  2. package/dist/browser/index.mjs +35 -10
  3. package/dist/browser/index.mjs.map +1 -1
  4. package/dist/node/directives/AreDirectiveIf.directive.d.mts +17 -1
  5. package/dist/node/directives/AreDirectiveIf.directive.d.ts +17 -1
  6. package/dist/node/directives/AreDirectiveIf.directive.js +29 -6
  7. package/dist/node/directives/AreDirectiveIf.directive.js.map +1 -1
  8. package/dist/node/directives/AreDirectiveIf.directive.mjs +29 -6
  9. package/dist/node/directives/AreDirectiveIf.directive.mjs.map +1 -1
  10. package/dist/node/engine/AreHTML.compiler.d.mts +3 -1
  11. package/dist/node/engine/AreHTML.compiler.d.ts +3 -1
  12. package/dist/node/engine/AreHTML.compiler.js +7 -4
  13. package/dist/node/engine/AreHTML.compiler.js.map +1 -1
  14. package/dist/node/engine/AreHTML.compiler.mjs +7 -4
  15. package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
  16. package/examples/for-perf/dist/index.html +1 -1
  17. package/examples/for-perf/dist/{mqj1mpf2-z4aokv.js → mqp8i2py-vltsx0.js} +2488 -2373
  18. package/examples/lazy-loading/README.md +76 -0
  19. package/examples/lazy-loading/concept.ts +55 -0
  20. package/examples/lazy-loading/containers/UI.container.ts +215 -0
  21. package/examples/lazy-loading/dist/app.js +3803 -0
  22. package/examples/{for-perf/dist/mqj1mpff-4fr7mw.js → lazy-loading/dist/chunks/chunk-6K72IBO4.js} +2688 -5897
  23. package/examples/lazy-loading/dist/index.html +36 -0
  24. package/examples/lazy-loading/dist/lazy/about-page.js +59 -0
  25. package/examples/lazy-loading/dist/lazy/reports-page.js +65 -0
  26. package/examples/lazy-loading/dist/lazy/settings-page.js +54 -0
  27. package/examples/lazy-loading/public/index.html +36 -0
  28. package/examples/lazy-loading/src/components/AppShell.component.ts +44 -0
  29. package/examples/lazy-loading/src/components/HomePage.component.ts +59 -0
  30. package/examples/lazy-loading/src/components/LazyOutlet.component.ts +108 -0
  31. package/examples/lazy-loading/src/components/NavBar.component.ts +98 -0
  32. package/examples/lazy-loading/src/concept.ts +116 -0
  33. package/examples/lazy-loading/src/lazy/AboutPage.component.ts +54 -0
  34. package/examples/lazy-loading/src/lazy/ReportsPage.component.ts +56 -0
  35. package/examples/lazy-loading/src/lazy/SettingsPage.component.ts +45 -0
  36. package/examples/lazy-loading/src/runtime/ComponentManifest.fragment.ts +61 -0
  37. package/examples/lazy-loading/src/runtime/LazyComponentResolver.fragment.ts +77 -0
  38. package/examples/os-desktop/README.md +91 -0
  39. package/examples/os-desktop/concept.ts +54 -0
  40. package/examples/os-desktop/containers/OS.container.ts +198 -0
  41. package/examples/os-desktop/containers/apps/AppBackend.ts +29 -0
  42. package/examples/os-desktop/containers/apps/GanttApp.backend.ts +56 -0
  43. package/examples/os-desktop/containers/apps/MarketingApp.backend.ts +68 -0
  44. package/examples/os-desktop/dist/app.js +4410 -0
  45. package/examples/os-desktop/dist/apps/gantt/app.js +271 -0
  46. package/examples/os-desktop/dist/apps/marketing/app.js +346 -0
  47. package/examples/os-desktop/dist/chunks/chunk-6K72IBO4.js +12455 -0
  48. package/examples/os-desktop/dist/chunks/chunk-EIIGUL6N.js +30 -0
  49. package/examples/os-desktop/dist/chunks/chunk-WOH7L5UR.js +30 -0
  50. package/examples/os-desktop/dist/index.html +33 -0
  51. package/examples/os-desktop/public/index.html +33 -0
  52. package/examples/os-desktop/src/apps/gantt/GanttApp.component.ts +41 -0
  53. package/examples/os-desktop/src/apps/gantt/GanttChart.component.ts +126 -0
  54. package/examples/os-desktop/src/apps/gantt/GanttStore.ts +47 -0
  55. package/examples/os-desktop/src/apps/gantt/GanttToolbar.component.ts +73 -0
  56. package/examples/os-desktop/src/apps/gantt/index.ts +13 -0
  57. package/examples/os-desktop/src/apps/marketing/MarketingApp.component.ts +53 -0
  58. package/examples/os-desktop/src/apps/marketing/MarketingStore.ts +34 -0
  59. package/examples/os-desktop/src/apps/marketing/PostEditor.component.ts +153 -0
  60. package/examples/os-desktop/src/apps/marketing/PostPreview.component.ts +110 -0
  61. package/examples/os-desktop/src/apps/marketing/index.ts +16 -0
  62. package/examples/os-desktop/src/concept.ts +126 -0
  63. package/examples/os-desktop/src/os/AppStage.component.ts +112 -0
  64. package/examples/os-desktop/src/os/AppWindow.component.ts +102 -0
  65. package/examples/os-desktop/src/os/Desktop.component.ts +106 -0
  66. package/examples/os-desktop/src/os/Dock.component.ts +174 -0
  67. package/examples/os-desktop/src/os/Hud.component.ts +83 -0
  68. package/examples/os-desktop/src/os/Launchpad.component.ts +191 -0
  69. package/examples/os-desktop/src/os/MenuBar.component.ts +156 -0
  70. package/examples/os-desktop/src/runtime/AppComponentResolver.fragment.ts +121 -0
  71. package/examples/os-desktop/src/runtime/AppRegistry.fragment.ts +104 -0
  72. package/examples/os-desktop/src/signals/MouseState.signal.ts +34 -0
  73. package/examples/os-desktop/src/signals/OSRoute.signal.ts +37 -0
  74. package/examples/os-desktop/src/signals/SelectionState.signal.ts +34 -0
  75. package/examples/signal-routing/dist/index.html +1 -1
  76. package/examples/signal-routing/dist/{mqiwo23h-bhcolu.js → mqp8hgce-4d6rh0.js} +2911 -2708
  77. package/package.json +11 -7
  78. package/src/directives/AreDirectiveIf.directive.ts +33 -4
  79. package/src/engine/AreHTML.compiler.ts +12 -2
  80. package/tests/PropPropagation.test.ts +181 -0
  81. package/tests/jest.setup.ts +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaas/are-html",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
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",
@@ -71,6 +71,8 @@
71
71
  "example:jumpstart": "nodemon ./examples/jumpstart/concept.ts",
72
72
  "example:dashboard": "nodemon ./examples/dashboard/concept.ts",
73
73
  "example:signal-routing": "nodemon ./examples/signal-routing/concept.ts",
74
+ "example:lazy-loading": "nodemon ./examples/lazy-loading/concept.ts",
75
+ "example:os-desktop": "nodemon ./examples/os-desktop/concept.ts",
74
76
  "example:component-styles": "nodemon ./examples/component-styles/concept.ts",
75
77
  "example:auxta": "nodemon ./examples/auxta/concept.ts",
76
78
  "example:for-perf": "nodemon ./examples/for-perf/concept.ts",
@@ -83,16 +85,16 @@
83
85
  "build": "tsup --config tsup.config.ts"
84
86
  },
85
87
  "peerDependencies": {
86
- "@adaas/a-concept": "^0.3.29",
88
+ "@adaas/a-concept": "^0.3.30",
87
89
  "@adaas/a-frame": "^0.1.18",
88
- "@adaas/a-utils": "^0.3.34",
89
- "@adaas/are": "^0.0.24"
90
+ "@adaas/a-utils": "^0.3.35",
91
+ "@adaas/are": "^0.0.26"
90
92
  },
91
93
  "devDependencies": {
92
- "@adaas/a-concept": "^0.3.29",
94
+ "@adaas/a-concept": "^0.3.30",
93
95
  "@adaas/a-frame": "^0.1.18",
94
- "@adaas/a-utils": "^0.3.34",
95
- "@adaas/are": "^0.0.24",
96
+ "@adaas/a-utils": "^0.3.35",
97
+ "@adaas/are": "^0.0.26",
96
98
  "@types/chai": "^4.3.14",
97
99
  "@types/jest": "^29.5.12",
98
100
  "@types/mocha": "^10.0.6",
@@ -101,6 +103,8 @@
101
103
  "chai": "^5.1.0",
102
104
  "dotenv": "^16.4.5",
103
105
  "jest": "^29.7.0",
106
+ "jest-environment-jsdom": "^29.7.0",
107
+ "jsdom": "^29.1.1",
104
108
  "mocha": "^10.4.0",
105
109
  "nodemon": "^3.1.4",
106
110
  "ts-jest": "^29.1.2",
@@ -96,9 +96,7 @@ export class AreDirectiveIf extends AreDirective {
96
96
  * 1. Extract the value from the store based on the attribute content
97
97
  * (which is the path to the value in the store)
98
98
  */
99
- attribute.value = syntax.evaluate(attribute.content, store, {
100
- ...(directiveContext?.scope || {}),
101
- });
99
+ attribute.value = this.evaluateCondition(syntax, attribute, store, directiveContext);
102
100
 
103
101
  /**
104
102
  * 2. If the value is falsy, remove the node from the scene by planning a RemoveElement instruction.
@@ -127,6 +125,7 @@ export class AreDirectiveIf extends AreDirective {
127
125
  @A_Inject(A_Scope) scope: A_Scope,
128
126
  @A_Inject(AreSyntax) syntax: AreSyntax,
129
127
  @A_Inject(AreScene) scene: AreScene,
128
+ @A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
130
129
  ...args: any[]
131
130
  ): void {
132
131
  /**
@@ -134,7 +133,7 @@ export class AreDirectiveIf extends AreDirective {
134
133
  * (which is the path to the value in the store)
135
134
  */
136
135
  const previous = !!attribute.value;
137
- const next = !!syntax.evaluate(attribute.content, store);
136
+ const next = this.evaluateCondition(syntax, attribute, store, directiveContext);
138
137
  attribute.value = next;
139
138
 
140
139
  // Skip when truthiness has not changed — avoids redundant mount/unmount.
@@ -149,4 +148,34 @@ export class AreDirectiveIf extends AreDirective {
149
148
  }
150
149
  }
151
150
 
151
+ /**
152
+ * Evaluates the `$if` condition defensively.
153
+ *
154
+ * A condition can reference data that is momentarily unavailable — most
155
+ * commonly a nested `$if` (e.g. `$if="selected.fields.length"`) living
156
+ * inside a parent `$if="selected"` whose object has just become `null`.
157
+ * Because the nested directive is still subscribed to the store, its
158
+ * update fires on that same change and the raw expression would throw
159
+ * `Cannot read properties of null`, crashing the whole update pipeline.
160
+ *
161
+ * Treating an evaluation error as `false` is the correct contract for a
162
+ * conditional: if the condition cannot be resolved, the subtree simply
163
+ * stays hidden until the referenced data is present again (at which point
164
+ * the parent `$if` re-activates and re-evaluates this one).
165
+ */
166
+ private evaluateCondition(
167
+ syntax: AreSyntax,
168
+ attribute: AreDirectiveAttribute,
169
+ store: AreStore,
170
+ directiveContext?: AreDirectiveContext,
171
+ ): boolean {
172
+ try {
173
+ return !!syntax.evaluate(attribute.content, store, {
174
+ ...(directiveContext?.scope || {}),
175
+ });
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
152
181
  }
@@ -15,6 +15,7 @@ import { AddListenerInstruction} from "@adaas/are-html/instructions/AddListener.
15
15
  import { AddStyleInstruction } from "@adaas/are-html/instructions/AddStyle.instruction";
16
16
  import { AddStaticHTMLInstruction } from "@adaas/are-html/instructions/AddStaticHTML.instruction";
17
17
  import { AreHTMLNode } from "@adaas/are-html/node";
18
+ import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
18
19
 
19
20
 
20
21
 
@@ -219,6 +220,7 @@ export class AreHTMLCompiler extends AreCompiler {
219
220
  @A_Inject(AreStore) parentStore: AreStore,
220
221
  @A_Inject(AreStore) store: AreStore,
221
222
  @A_Inject(AreSyntax) syntax: AreSyntax,
223
+ @A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
222
224
  ...args: any[]
223
225
  ) {
224
226
  if (!scene.host)
@@ -262,13 +264,21 @@ export class AreHTMLCompiler extends AreCompiler {
262
264
  return value;
263
265
  };
264
266
 
267
+ // Item-scoped variables from an enclosing directive (the `item`/`index`
268
+ // of a `$for`, or any scope a `$if` merged in) so a prop binding like
269
+ // `:title="item.name"` resolves the loop variable — checked BEFORE the
270
+ // store, exactly like plain attribute bindings do in the interpreter.
271
+ // Read `.scope` lazily inside each evaluation so keyed `$for` updates
272
+ // that reassign the context's scope are always reflected.
273
+ const directiveScope = () => directiveContext?.scope ?? {};
274
+
265
275
  // The watcher entity below is registered against parentStore so that
266
276
  // updates to the bound expression in the parent flow into the child store.
267
277
  const watcher = {
268
278
  update: () => {
269
279
  try {
270
280
  parentStore.watch(watcher);
271
- const next = coerce(syntax.evaluate(attribute.content, parentStore));
281
+ const next = coerce(syntax.evaluate(attribute.content, parentStore, directiveScope()));
272
282
  parentStore.unwatch(watcher);
273
283
  store.set(propName!, next);
274
284
  } catch (e) {
@@ -279,7 +289,7 @@ export class AreHTMLCompiler extends AreCompiler {
279
289
 
280
290
  // Initial read with watch active so dependencies are recorded.
281
291
  parentStore.watch(watcher);
282
- const initial = coerce(syntax.evaluate(attribute.content, parentStore));
292
+ const initial = coerce(syntax.evaluate(attribute.content, parentStore, directiveScope()));
283
293
  parentStore.unwatch(watcher);
284
294
 
285
295
  store.set(propName, initial);
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ * @jest-environment-options {"customExportConditions": ["node", "require", "default"]}
4
+ *
5
+ * Integration coverage for parent → child PROP propagation via `:prop="expr"`
6
+ * bindings, including the two structural directives `$if` and `$for`.
7
+ *
8
+ * These render a real component tree into a jsdom document through the full
9
+ * ARE pipeline (tokenize → transform → compile → interpret → mount) exactly
10
+ * the way the browser examples bootstrap, then assert on the produced DOM.
11
+ *
12
+ * Scenarios:
13
+ * 1. Plain parent → child prop (literal + store-derived expression).
14
+ * 2. A child prop INSIDE an `$if` block.
15
+ * 3. A child prop INSIDE a `$for` loop that references the loop variable
16
+ * (`:label="item.name"`) — the case that requires the prop-binding
17
+ * compile path to merge the directive (loop) scope.
18
+ * 4. Reactivity: updating the parent store flows into the child prop.
19
+ */
20
+
21
+ import { A_Concept, A_Context } from '@adaas/a-concept';
22
+ import { A_Inject, A_Caller } from '@adaas/a-concept';
23
+ import { A_Config, ConfigReader } from '@adaas/a-utils/a-config';
24
+ import { A_Logger, A_LOGGER_ENV_KEYS } from '@adaas/a-utils/a-logger';
25
+ import { A_SignalBus, A_SignalState } from '@adaas/a-utils/a-signal';
26
+ import { A_Polyfill } from '@adaas/a-utils/a-polyfill';
27
+
28
+ import { Are, AreNode, AreStore, AreContainer, AreInit, AreRoute } from '@adaas/are';
29
+ import { AreRoot } from '@adaas/are-html/root/AreRoot.component';
30
+ import { AreHTMLEngine } from '@adaas/are-html/engine';
31
+ import { AreHTMLEngineContext } from '@adaas/are-html/context';
32
+ import { AreDirectiveIf } from '@adaas/are-html/directives/AreDirectiveIf.directive';
33
+ import { AreDirectiveFor } from '@adaas/are-html/directives/AreDirectiveFor.directive';
34
+
35
+
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ // ── Test components ──────────────────────────────────────────────────────────
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+
40
+ /** Leaf component: renders whatever `label` prop it receives. */
41
+ class TestChild extends Are {
42
+ props: Record<string, any> = {
43
+ label: { type: 'string', default: '' },
44
+ };
45
+
46
+ @Are.Template
47
+ template(@A_Inject(A_Caller) node: AreNode): void {
48
+ node.setContent(`<span class="child">{{label}}</span>`);
49
+ }
50
+
51
+ @Are.Data
52
+ data(@A_Inject(AreStore) store: AreStore): void {
53
+ store.set({ label: store.get('label') ?? '' });
54
+ }
55
+ }
56
+
57
+ /** Parent: passes props down plainly, inside `$if`, and inside `$for`. */
58
+ class TestParent extends Are {
59
+ @Are.Template
60
+ template(@A_Inject(A_Caller) node: AreNode): void {
61
+ node.setContent(`
62
+ <div class="parent">
63
+ <test-child class="basic" :label="topLabel"></test-child>
64
+
65
+ <div class="if-wrap" $if="show">
66
+ <test-child class="in-if" :label="topLabel"></test-child>
67
+ </div>
68
+
69
+ <test-child class="in-for"
70
+ $for="item in items track item.id"
71
+ :label="item.name"></test-child>
72
+ </div>
73
+ `);
74
+ }
75
+
76
+ @Are.Data
77
+ data(@A_Inject(AreStore) store: AreStore): void {
78
+ store.set({
79
+ topLabel: 'Hello',
80
+ show: true,
81
+ items: [
82
+ { id: 1, name: 'Alpha' },
83
+ { id: 2, name: 'Beta' },
84
+ { id: 3, name: 'Gamma' },
85
+ ],
86
+ });
87
+ }
88
+ }
89
+
90
+
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ // ── Harness ──────────────────────────────────────────────────────────────────
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+
95
+ const flush = () => new Promise<void>((r) => setTimeout(r, 0));
96
+
97
+ async function bootstrap(): Promise<A_Concept> {
98
+ // The engine context reads `container.body.innerHTML` as its source at
99
+ // construction time, so the markup must be in the DOM BEFORE we build it.
100
+ document.body.innerHTML = `<are-root id="app"><test-parent></test-parent></are-root>`;
101
+
102
+ const container = new AreContainer({
103
+ name: 'ARE Prop-Propagation Test',
104
+ components: [
105
+ TestParent,
106
+ TestChild,
107
+ AreDirectiveIf,
108
+ AreDirectiveFor,
109
+ A_SignalBus,
110
+ AreRoot,
111
+ ConfigReader,
112
+ AreHTMLEngine,
113
+ A_Logger,
114
+ ],
115
+ entities: [AreInit, AreRoute],
116
+ fragments: [
117
+ new A_SignalState([AreRoute]),
118
+ new AreHTMLEngineContext({ container: document }),
119
+ new A_Config({
120
+ defaults: { [A_LOGGER_ENV_KEYS.LOG_LEVEL]: 'error' },
121
+ }),
122
+ ],
123
+ });
124
+
125
+ const concept = new A_Concept({
126
+ name: 'adaas-are-html-prop-propagation-test',
127
+ fragments: [
128
+ new A_Config({
129
+ variables: ['CONFIG_VERBOSE', 'DEV_MODE'] as const,
130
+ defaults: { CONFIG_VERBOSE: false, DEV_MODE: false },
131
+ }),
132
+ ],
133
+ components: [A_Logger, ConfigReader, A_Polyfill],
134
+ containers: [container],
135
+ });
136
+
137
+ await concept.load();
138
+ await concept.start();
139
+ await flush();
140
+
141
+ return concept;
142
+ }
143
+
144
+ afterEach(() => {
145
+ A_Context.reset();
146
+ document.body.innerHTML = '';
147
+ });
148
+
149
+
150
+ // ─────────────────────────────────────────────────────────────────────────────
151
+ // ── Tests ────────────────────────────────────────────────────────────────────
152
+ // ─────────────────────────────────────────────────────────────────────────────
153
+
154
+ describe('Prop propagation — parent → child via :prop', () => {
155
+
156
+ it('passes a store-derived prop to a plain child', async () => {
157
+ await bootstrap();
158
+
159
+ const basic = document.querySelector('.basic .child');
160
+ expect(basic).not.toBeNull();
161
+ expect(basic!.textContent).toBe('Hello');
162
+ });
163
+
164
+ it('passes a prop to a child rendered inside an $if block', async () => {
165
+ await bootstrap();
166
+
167
+ const inIf = document.querySelector('.in-if .child');
168
+ expect(inIf).not.toBeNull();
169
+ expect(inIf!.textContent).toBe('Hello');
170
+ });
171
+
172
+ it('passes the loop variable as a prop to children inside a $for', async () => {
173
+ await bootstrap();
174
+
175
+ const forChildren = Array.from(
176
+ document.querySelectorAll('.in-for .child'),
177
+ ).map((el) => el.textContent);
178
+
179
+ expect(forChildren).toEqual(['Alpha', 'Beta', 'Gamma']);
180
+ });
181
+ });
@@ -1,3 +1,14 @@
1
+ // jsdom test environments do not expose Node's TextEncoder/TextDecoder globals,
2
+ // which the @adaas codec layer requires. Polyfill them when missing (no-op under
3
+ // the default `node` test environment where they already exist).
4
+ import { TextEncoder, TextDecoder } from 'util';
5
+ if (typeof (globalThis as any).TextEncoder === 'undefined') {
6
+ (globalThis as any).TextEncoder = TextEncoder;
7
+ }
8
+ if (typeof (globalThis as any).TextDecoder === 'undefined') {
9
+ (globalThis as any).TextDecoder = TextDecoder;
10
+ }
11
+
1
12
  // import { A_Context } from '@adaas/a-concept/a-context';
2
13
  // import fs from 'fs';
3
14