@adaas/are-html 0.0.22 → 0.0.23
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/dist/browser/index.d.mts +176 -8
- package/dist/browser/index.mjs +661 -235
- package/dist/browser/index.mjs.map +1 -1
- package/dist/node/{AreBinding.attribute-doUvtOjc.d.mts → AreBinding.attribute-BWzEIw6H.d.mts} +45 -0
- package/dist/node/{AreBinding.attribute-Bm5LlOyE.d.ts → AreBinding.attribute-GpT-5Qmf.d.ts} +45 -0
- package/dist/node/attributes/AreBinding.attribute.d.mts +1 -1
- package/dist/node/attributes/AreBinding.attribute.d.ts +1 -1
- package/dist/node/attributes/AreDirective.attribute.d.mts +1 -1
- package/dist/node/attributes/AreDirective.attribute.d.ts +1 -1
- package/dist/node/attributes/AreEvent.attribute.d.mts +1 -1
- package/dist/node/attributes/AreEvent.attribute.d.ts +1 -1
- package/dist/node/attributes/AreStatic.attribute.d.mts +1 -1
- package/dist/node/attributes/AreStatic.attribute.d.ts +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.mts +18 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.ts +18 -1
- package/dist/node/directives/AreDirectiveFor.directive.js +57 -9
- package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.mjs +57 -9
- package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.d.mts +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.d.ts +1 -1
- package/dist/node/directives/AreDirectiveShow.directive.d.mts +1 -1
- package/dist/node/directives/AreDirectiveShow.directive.d.ts +1 -1
- package/dist/node/engine/AreHTML.compiler.d.mts +1 -1
- package/dist/node/engine/AreHTML.compiler.d.ts +1 -1
- package/dist/node/engine/AreHTML.compiler.js +4 -0
- package/dist/node/engine/AreHTML.compiler.js.map +1 -1
- package/dist/node/engine/AreHTML.compiler.mjs +4 -0
- package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
- package/dist/node/engine/AreHTML.constants.d.mts +33 -1
- package/dist/node/engine/AreHTML.constants.d.ts +33 -1
- package/dist/node/engine/AreHTML.constants.js +166 -0
- package/dist/node/engine/AreHTML.constants.js.map +1 -1
- package/dist/node/engine/AreHTML.constants.mjs +165 -1
- package/dist/node/engine/AreHTML.constants.mjs.map +1 -1
- package/dist/node/engine/AreHTML.context.d.mts +66 -0
- package/dist/node/engine/AreHTML.context.d.ts +66 -0
- package/dist/node/engine/AreHTML.context.js +98 -0
- package/dist/node/engine/AreHTML.context.js.map +1 -1
- package/dist/node/engine/AreHTML.context.mjs +98 -0
- package/dist/node/engine/AreHTML.context.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 +66 -10
- package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
- package/dist/node/engine/AreHTML.interpreter.mjs +66 -10
- package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.d.mts +1 -8
- package/dist/node/engine/AreHTML.lifecycle.d.ts +1 -8
- package/dist/node/engine/AreHTML.lifecycle.js +29 -44
- package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.mjs +29 -44
- package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
- package/dist/node/engine/AreHTML.tokenizer.d.mts +1 -1
- package/dist/node/engine/AreHTML.tokenizer.d.ts +1 -1
- package/dist/node/engine/AreHTML.tokenizer.js +7 -1
- package/dist/node/engine/AreHTML.tokenizer.js.map +1 -1
- package/dist/node/engine/AreHTML.tokenizer.mjs +7 -1
- package/dist/node/engine/AreHTML.tokenizer.mjs.map +1 -1
- package/dist/node/engine/AreHTML.transformer.d.mts +1 -1
- package/dist/node/engine/AreHTML.transformer.d.ts +1 -1
- package/dist/node/index.d.mts +4 -3
- package/dist/node/index.d.ts +4 -3
- package/dist/node/index.js +7 -0
- package/dist/node/index.mjs +1 -0
- package/dist/node/instructions/AddStaticHTML.instruction.d.mts +8 -0
- package/dist/node/instructions/AddStaticHTML.instruction.d.ts +8 -0
- package/dist/node/instructions/AddStaticHTML.instruction.js +31 -0
- package/dist/node/instructions/AddStaticHTML.instruction.js.map +1 -0
- package/dist/node/instructions/AddStaticHTML.instruction.mjs +24 -0
- package/dist/node/instructions/AddStaticHTML.instruction.mjs.map +1 -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 +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
- package/dist/node/instructions/AreHTML.instructions.constants.mjs +1 -0
- 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/lib/AreDirective/AreDirective.component.d.mts +1 -1
- package/dist/node/lib/AreDirective/AreDirective.component.d.ts +1 -1
- package/dist/node/lib/AreDirective/AreDirective.types.d.mts +1 -1
- package/dist/node/lib/AreDirective/AreDirective.types.d.ts +1 -1
- package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.mts +1 -1
- package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.ts +1 -1
- package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.mts +1 -1
- package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.ts +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.mts +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.ts +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.js +51 -0
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.js.map +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs +51 -0
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs.map +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
- package/dist/node/nodes/AreComment.d.mts +1 -1
- package/dist/node/nodes/AreComment.d.ts +1 -1
- package/dist/node/nodes/AreComponent.d.mts +1 -1
- package/dist/node/nodes/AreComponent.d.ts +1 -1
- package/dist/node/nodes/AreInterpolation.d.mts +1 -1
- package/dist/node/nodes/AreInterpolation.d.ts +1 -1
- package/dist/node/nodes/AreRoot.d.mts +1 -1
- package/dist/node/nodes/AreRoot.d.ts +1 -1
- package/dist/node/nodes/AreText.d.mts +1 -1
- package/dist/node/nodes/AreText.d.ts +1 -1
- package/examples/dashboard/concept.ts +1 -1
- package/examples/dashboard/dist/index.html +1 -1
- package/examples/dashboard/dist/{mqh9ryml-xat335.js → mqiw5sqa-ypckmj.js} +403 -57
- package/examples/for-perf/dist/index.html +1 -1
- package/examples/for-perf/dist/{mqh9ryfo-6a8d0o.js → mqj1mpf2-z4aokv.js} +558 -117
- package/examples/for-perf/dist/{mqh9ryfq-4pf5cv.js → mqj1mpff-4fr7mw.js} +558 -117
- package/examples/signal-routing/dist/index.html +1 -1
- package/examples/signal-routing/dist/{mqh9ryc9-dkcbkx.js → mqiwo23h-bhcolu.js} +413 -60
- package/package.json +5 -5
- package/src/directives/AreDirectiveFor.directive.ts +99 -16
- package/src/engine/AreHTML.compiler.ts +13 -0
- package/src/engine/AreHTML.constants.ts +142 -0
- package/src/engine/AreHTML.context.ts +112 -0
- package/src/engine/AreHTML.interpreter.ts +114 -13
- package/src/engine/AreHTML.lifecycle.ts +81 -74
- package/src/engine/AreHTML.tokenizer.ts +30 -1
- package/src/index.ts +1 -0
- package/src/instructions/AddStaticHTML.instruction.ts +23 -0
- package/src/instructions/AreHTML.instructions.constants.ts +1 -0
- package/src/instructions/AreHTML.instructions.types.ts +9 -0
- package/src/lib/AreHTMLNode/AreHTMLNode.ts +74 -0
- package/src/lib/AreRoot/AreRoot.component.ts +3 -3
- package/tests/StaticIsland.test.ts +115 -0
- package/examples/for-perf/dist/mqh9ryde-m243t8.js +0 -15223
|
@@ -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 { AddStaticHTMLInstruction } from "@adaas/are-html/instructions/AddStaticHTML.instruction";
|
|
18
19
|
import { HideElementInstruction } from "@adaas/are-html/instructions/HideElement.instruction";
|
|
19
20
|
import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
|
|
20
21
|
import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
|
|
@@ -84,16 +85,25 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
84
85
|
? context.container.createElementNS(SVG_NAMESPACE, tag)
|
|
85
86
|
: context.container.createElement(tag);
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
// Index the element synchronously so descendants can resolve this
|
|
89
|
+
// node as their mount point during the same (still off-document) pass.
|
|
90
|
+
context.setInstructionElement(declaration, element);
|
|
91
|
+
|
|
92
|
+
const attach = mountPoint.nodeType === Node.ELEMENT_NODE
|
|
88
93
|
// parent is a real element — just append
|
|
89
|
-
mountPoint.appendChild(element)
|
|
90
|
-
} else {
|
|
94
|
+
? () => mountPoint.appendChild(element)
|
|
91
95
|
// parent is an anchor (comment/text node) — insert before it
|
|
92
96
|
// so content always appears before the anchor marker
|
|
93
|
-
mountPoint.parentNode?.insertBefore(element, mountPoint);
|
|
94
|
-
}
|
|
97
|
+
: () => { mountPoint.parentNode?.insertBefore(element, mountPoint); };
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
// When batching, only the mutation that touches the *live* document
|
|
100
|
+
// needs deferring; appends into an already-detached subtree are free
|
|
101
|
+
// and run immediately so the offline tree keeps taking shape.
|
|
102
|
+
if (context.isBatching && mountPoint.isConnected) {
|
|
103
|
+
context.deferAttach(attach);
|
|
104
|
+
} else {
|
|
105
|
+
attach();
|
|
106
|
+
}
|
|
97
107
|
|
|
98
108
|
} else {
|
|
99
109
|
const mountPoint = context.container.getElementById(node.id);
|
|
@@ -108,9 +118,17 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
108
118
|
? context.container.createElementNS(SVG_NAMESPACE, tag)
|
|
109
119
|
: context.container.createElement(tag);
|
|
110
120
|
|
|
111
|
-
mountPoint.parentNode?.replaceChild(element, mountPoint);
|
|
112
|
-
|
|
113
121
|
context.setInstructionElement(declaration, element);
|
|
122
|
+
|
|
123
|
+
const attach = () => { mountPoint.parentNode?.replaceChild(element, mountPoint); };
|
|
124
|
+
|
|
125
|
+
// The placeholder lives in the live DOM — defer the swap so the
|
|
126
|
+
// whole subtree built into `element` lands in one mutation.
|
|
127
|
+
if (context.isBatching && mountPoint.isConnected) {
|
|
128
|
+
context.deferAttach(attach);
|
|
129
|
+
} else {
|
|
130
|
+
attach();
|
|
131
|
+
}
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
// Register the element in the context index
|
|
@@ -135,7 +153,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
135
153
|
) {
|
|
136
154
|
const element = context.getElementByInstruction(declaration);
|
|
137
155
|
|
|
138
|
-
|
|
156
|
+
// Skip the live-DOM detach when the element is already off-document: an
|
|
157
|
+
// ancestor removed earlier in the same unmount took this whole subtree
|
|
158
|
+
// with it, and a detached subtree is garbage-collected as a unit. The
|
|
159
|
+
// per-node removeChild would be wasted work. The index bookkeeping below
|
|
160
|
+
// must still run to drop the strong references that would pin it in memory.
|
|
161
|
+
if (element && element.parentNode && element.isConnected) {
|
|
139
162
|
element.parentNode.removeChild(element);
|
|
140
163
|
}
|
|
141
164
|
|
|
@@ -283,7 +306,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
283
306
|
|
|
284
307
|
const { name } = mutation.payload;
|
|
285
308
|
|
|
286
|
-
|
|
309
|
+
// No point stripping attributes off an element that is already
|
|
310
|
+
// detached from the live document (it is about to be discarded with
|
|
311
|
+
// its whole subtree). Only touch attributes while the element is live.
|
|
312
|
+
if (name && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
|
|
287
313
|
const colonIdx = name.indexOf(':');
|
|
288
314
|
if (colonIdx > 0) {
|
|
289
315
|
const ns = SVG_ATTRIBUTE_NS[name.slice(0, colonIdx)];
|
|
@@ -343,6 +369,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
343
369
|
|
|
344
370
|
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
|
|
345
371
|
|
|
372
|
+
// A detached element is being discarded with its subtree — restoring its
|
|
373
|
+
// inline display would be pointless work. Only restore while it is live.
|
|
374
|
+
if (!element.isConnected) return;
|
|
375
|
+
|
|
346
376
|
const el = element as HTMLElement;
|
|
347
377
|
|
|
348
378
|
// Restore the cached inline display. An explicit payload display, when
|
|
@@ -510,7 +540,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
510
540
|
const listener = (mutation.payload as any)._callback as EventListenerOrEventListenerObject | undefined;
|
|
511
541
|
|
|
512
542
|
if (listener) {
|
|
513
|
-
element
|
|
543
|
+
// Detaching a listener from an element that is already off-document is
|
|
544
|
+
// unnecessary — the element and its listeners are collected together.
|
|
545
|
+
// Still clear the context bookkeeping so no strong reference lingers.
|
|
546
|
+
if (element.isConnected) {
|
|
547
|
+
element.removeEventListener(eventName, listener);
|
|
548
|
+
}
|
|
514
549
|
context.removeListener(element, name, listener);
|
|
515
550
|
(mutation.payload as any)._callback = undefined;
|
|
516
551
|
}
|
|
@@ -592,11 +627,73 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
592
627
|
|
|
593
628
|
if (!element) return;
|
|
594
629
|
|
|
595
|
-
|
|
630
|
+
// Off-document text nodes vanish with their detached parent subtree, so
|
|
631
|
+
// skip the removeChild and just release the index reference.
|
|
632
|
+
if (element.isConnected) {
|
|
633
|
+
element.parentNode?.removeChild(element);
|
|
634
|
+
}
|
|
596
635
|
context.removeInstructionElement(declaration);
|
|
597
636
|
}
|
|
598
637
|
|
|
599
638
|
|
|
639
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
640
|
+
// ── AddStaticHTML — Apply / Update / Revert ──────────────────────────────────
|
|
641
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
642
|
+
// Materialises a fully static subtree (a "static island") onto its host
|
|
643
|
+
// element in a single pass. The tokenizer captured the inner markup verbatim
|
|
644
|
+
// instead of exploding it into per-element/per-text AreNodes, so here we hand
|
|
645
|
+
// the markup to the browser parser ONCE (via a cached <template>) and clone
|
|
646
|
+
// the pre-parsed fragment in. This collapses N DOM mutations into one and
|
|
647
|
+
// decodes HTML entities (e.g. ) natively.
|
|
648
|
+
@A_Frame.Define({
|
|
649
|
+
description: 'Inject a static island\'s inner markup onto its host element in one pass via a cached, browser-parsed <template> clone. Decodes HTML entities natively.'
|
|
650
|
+
})
|
|
651
|
+
@AreInterpreter.Apply(AreHTMLInstructions.AddStaticHTML)
|
|
652
|
+
@AreInterpreter.Update(AreHTMLInstructions.AddStaticHTML)
|
|
653
|
+
addStaticHTML(
|
|
654
|
+
@A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
|
|
655
|
+
@A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
|
|
656
|
+
@A_Inject(A_Logger) logger?: A_Logger,
|
|
657
|
+
) {
|
|
658
|
+
const element = context.getElementByInstruction(mutation.parent!);
|
|
659
|
+
|
|
660
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
661
|
+
throw new AreInterpreterError({
|
|
662
|
+
title: 'Element Not Found',
|
|
663
|
+
description: `Could not find a DOM element associated with the instruction ASEID "${mutation.parent}". Ensure the host element is rendered before materialising its static island.`
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const el = element as HTMLElement;
|
|
668
|
+
const { html } = mutation.payload;
|
|
669
|
+
|
|
670
|
+
// Clear any previously injected content (Update path) before re-injecting.
|
|
671
|
+
el.textContent = '';
|
|
672
|
+
|
|
673
|
+
// Parse once (in the host tag's context) and clone the cached fragment.
|
|
674
|
+
const fragment = context.getStaticFragment(el.tagName.toLowerCase(), html);
|
|
675
|
+
el.appendChild(fragment.cloneNode(true));
|
|
676
|
+
|
|
677
|
+
logger?.debug('green', `Static island materialised onto <${(mutation.owner.parent ?? mutation.owner)?.aseid?.toString?.()}>`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@A_Frame.Define({
|
|
682
|
+
description: 'Clear a static island\'s injected markup from its host element on revert.'
|
|
683
|
+
})
|
|
684
|
+
@AreInterpreter.Revert(AreHTMLInstructions.AddStaticHTML)
|
|
685
|
+
removeStaticHTML(
|
|
686
|
+
@A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
|
|
687
|
+
@A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
|
|
688
|
+
) {
|
|
689
|
+
const element = context.getElementByInstruction(mutation.parent!);
|
|
690
|
+
|
|
691
|
+
if (element && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
|
|
692
|
+
(element as HTMLElement).textContent = '';
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
600
697
|
|
|
601
698
|
@A_Frame.Define({
|
|
602
699
|
description: 'Add a comment node to the DOM based on the provided declaration instruction.'
|
|
@@ -667,7 +764,11 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
667
764
|
|
|
668
765
|
if (!element) return;
|
|
669
766
|
|
|
670
|
-
|
|
767
|
+
// Off-document comment anchors are discarded with their detached parent
|
|
768
|
+
// subtree; skip the removeChild and just release the index reference.
|
|
769
|
+
if (element.isConnected) {
|
|
770
|
+
element.parentNode?.removeChild(element);
|
|
771
|
+
}
|
|
671
772
|
context.removeInstructionElement(declaration);
|
|
672
773
|
}
|
|
673
774
|
|
|
@@ -10,7 +10,6 @@ import { AreDirectiveFeatures } from "@adaas/are-html/directive/AreDirective.con
|
|
|
10
10
|
import { AreHTMLEngineContext } from "./AreHTML.context";
|
|
11
11
|
import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
|
|
12
12
|
import { A_Frame } from "@adaas/a-frame/core";
|
|
13
|
-
import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
@A_Frame.Define({
|
|
@@ -19,14 +18,6 @@ import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper"
|
|
|
19
18
|
})
|
|
20
19
|
export class AreHTMLLifecycle extends AreLifecycle {
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
* Per-chunk time budget (ms) for the time-sliced initial mount walk. While
|
|
24
|
-
* mounting a large subtree we keep applying nodes until this much wall-clock
|
|
25
|
-
* time has elapsed, then yield to the browser so it can paint and process
|
|
26
|
-
* input before the next chunk. ~16ms targets a single animation frame.
|
|
27
|
-
*/
|
|
28
|
-
private static readonly MOUNT_BUDGET_MS = 16;
|
|
29
|
-
|
|
30
21
|
@AreLifecycle.Init(AreComponentNode)
|
|
31
22
|
initComponent(
|
|
32
23
|
@A_Inject(A_Caller) node: AreHTMLNode,
|
|
@@ -112,83 +103,99 @@ export class AreHTMLLifecycle extends AreLifecycle {
|
|
|
112
103
|
if (scene.isInactive) return;
|
|
113
104
|
|
|
114
105
|
/**
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
* 2. Walk the descendant subtree iteratively with an explicit enter/exit
|
|
121
|
-
* stack so we can TIME-SLICE the work. The previous implementation
|
|
122
|
-
* recursed via `child.mount()`, which fires onBeforeMount → onMount
|
|
123
|
-
* (interpret + recurse) → onAfterMount per node and runs the whole tree
|
|
124
|
-
* in one synchronous, un-yielding block. For large initial trees that
|
|
125
|
-
* froze the main thread on first page load.
|
|
106
|
+
* Open a batching scope for the whole (synchronous) mount pass. Every
|
|
107
|
+
* element created below is built into a *detached* root and attached to
|
|
108
|
+
* the live DOM only when the batch flushes — turning O(nodes) reflows into
|
|
109
|
+
* a single one per mount root. `try/finally` guarantees the batch closes
|
|
110
|
+
* (and the depth counter stays balanced) even if interpretation throws.
|
|
126
111
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* - exit → onAfterMount (fires AFTER the node's whole subtree, i.e.
|
|
130
|
-
* post-order, matching the recursive `node.mount()` contract)
|
|
131
|
-
*
|
|
132
|
-
* Small trees complete entirely within a single time budget and the
|
|
133
|
-
* handler returns `void` synchronously — a true fast-path with NO
|
|
134
|
-
* behavioural change for typical UIs. Only genuinely large trees exceed
|
|
135
|
-
* the budget, at which point we yield a macrotask (letting the browser
|
|
136
|
-
* paint / stay responsive) and resume the remaining work, returning a
|
|
137
|
-
* Promise that resolves when the whole subtree is mounted.
|
|
112
|
+
* The context is resolved from the node's scope (the scope the onMount
|
|
113
|
+
* feature runs in) so the override keeps the base `mount` signature.
|
|
138
114
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const stack: MountFrame[] = [];
|
|
142
|
-
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
143
|
-
stack.push({ node: node.children[i] as AreHTMLNode, entered: false });
|
|
144
|
-
}
|
|
115
|
+
const context = node.scope.resolve<AreHTMLEngineContext>(AreHTMLEngineContext);
|
|
145
116
|
|
|
146
|
-
|
|
147
|
-
const frame = stack[stack.length - 1];
|
|
148
|
-
const current = frame.node;
|
|
117
|
+
context?.beginBatch();
|
|
149
118
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
119
|
+
/**
|
|
120
|
+
* `onAfterMount` must observe the node already connected to the live
|
|
121
|
+
* document (consumer components may measure layout / focus there). Since
|
|
122
|
+
* the subtree is built off-document and attached only when the batch
|
|
123
|
+
* flushes, we collect the post-order `onAfterMount` targets here and fire
|
|
124
|
+
* them once the flush has connected everything.
|
|
125
|
+
*/
|
|
126
|
+
const afterMountQueue: AreHTMLNode[] = [];
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
/**
|
|
130
|
+
* 1. Render the root of this mount itself.
|
|
131
|
+
*/
|
|
132
|
+
node.interpret();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 2. Walk the descendant subtree iteratively with an explicit enter/exit
|
|
136
|
+
* stack. We keep the iterative (non-recursive) shape — it is cheaper
|
|
137
|
+
* than deep recursion and gives us a single, clear place that owns the
|
|
138
|
+
* per-node hook ordering:
|
|
139
|
+
* - enter → onBeforeMount, then (if active) interpret + queue children
|
|
140
|
+
* - exit → onAfterMount (fires AFTER the node's whole subtree, i.e.
|
|
141
|
+
* post-order, matching the recursive `node.mount()` contract)
|
|
142
|
+
*
|
|
143
|
+
* [!] The initial mount is intentionally ATOMIC (fully synchronous). It
|
|
144
|
+
* does NOT time-slice / yield. Yielding mid-walk exposes a partially
|
|
145
|
+
* mounted tree to the event loop: any update dispatched during a gap
|
|
146
|
+
* (signal-driven re-render, async data load, etc.) interprets a node
|
|
147
|
+
* whose parent has not been mounted yet — producing
|
|
148
|
+
* `mount-point-not-found` and out-of-order DOM. The whole page must
|
|
149
|
+
* therefore appear in the DOM in one uninterrupted pass. Heavy lists
|
|
150
|
+
* are sliced at the source instead (see AreDirectiveFor), where the
|
|
151
|
+
* batching is reentrancy-safe.
|
|
152
|
+
*/
|
|
153
|
+
interface MountFrame { node: AreHTMLNode; entered: boolean; }
|
|
154
|
+
|
|
155
|
+
const stack: MountFrame[] = [];
|
|
156
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
157
|
+
stack.push({ node: node.children[i] as AreHTMLNode, entered: false });
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
while (stack.length > 0) {
|
|
161
|
+
const frame = stack[stack.length - 1];
|
|
162
|
+
const current = frame.node;
|
|
163
|
+
|
|
164
|
+
if (frame.entered) {
|
|
165
|
+
// Post-order exit: the whole subtree below `current` is mounted.
|
|
166
|
+
// Defer the onAfterMount hook until after the batch flush so the
|
|
167
|
+
// node is connected to the live DOM when it runs.
|
|
168
|
+
stack.pop();
|
|
169
|
+
afterMountQueue.push(current);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
158
172
|
|
|
159
|
-
|
|
160
|
-
// recursive AreNode.mount() semantics.
|
|
161
|
-
current.call(AreNodeFeatures.onBeforeMount, current.scope);
|
|
173
|
+
frame.entered = true;
|
|
162
174
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (let i = current.children.length - 1; i >= 0; i--) {
|
|
167
|
-
stack.push({ node: current.children[i] as AreHTMLNode, entered: false });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
};
|
|
175
|
+
// onBeforeMount always fires (even for inactive nodes), matching the
|
|
176
|
+
// recursive AreNode.mount() semantics.
|
|
177
|
+
current.call(AreNodeFeatures.onBeforeMount, current.scope);
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return new Promise<void>((resolve, reject) => {
|
|
179
|
-
AreSchedulerHelper.scheduleMacrotask(() => {
|
|
180
|
-
try {
|
|
181
|
-
resolve(drive());
|
|
182
|
-
} catch (error) {
|
|
183
|
-
reject(error);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
});
|
|
179
|
+
if (!current.scene.isInactive) {
|
|
180
|
+
current.interpret();
|
|
181
|
+
// Push children in reverse so they pop in document order.
|
|
182
|
+
for (let i = current.children.length - 1; i >= 0; i--) {
|
|
183
|
+
stack.push({ node: current.children[i] as AreHTMLNode, entered: false });
|
|
184
|
+
}
|
|
187
185
|
}
|
|
188
186
|
}
|
|
189
|
-
}
|
|
187
|
+
} finally {
|
|
188
|
+
// Flush the deferred attachments — the fully built subtree lands in the
|
|
189
|
+
// live DOM in a single pass.
|
|
190
|
+
context?.endBatch();
|
|
191
|
+
}
|
|
190
192
|
|
|
191
|
-
|
|
193
|
+
// The subtree is now connected; fire onAfterMount in the original
|
|
194
|
+
// post-order, each with its node live in the document.
|
|
195
|
+
for (let i = 0; i < afterMountQueue.length; i++) {
|
|
196
|
+
const mounted = afterMountQueue[i];
|
|
197
|
+
mounted.call(AreNodeFeatures.onAfterMount, mounted.scope);
|
|
198
|
+
}
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
|
|
@@ -8,6 +8,8 @@ import { AreEventAttribute } from "@adaas/are-html/attributes/AreEvent.attribute
|
|
|
8
8
|
import { AreBindingAttribute } from "@adaas/are-html/attributes/AreBinding.attribute";
|
|
9
9
|
import { AreStaticAttribute } from "@adaas/are-html/attributes/AreStatic.attribute";
|
|
10
10
|
import { AreHTMLAttribute } from "../lib/AreHTMLAttribute/AreHTML.attribute";
|
|
11
|
+
import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
|
|
12
|
+
import { isStaticMarkup } from "./AreHTML.constants";
|
|
11
13
|
import { A_Frame } from "@adaas/a-frame/core";
|
|
12
14
|
|
|
13
15
|
|
|
@@ -30,7 +32,34 @@ export class AreHTMLTokenizer extends AreTokenizer {
|
|
|
30
32
|
@A_Inject(A_Logger) logger?: A_Logger
|
|
31
33
|
): void {
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Static-island fast path.
|
|
37
|
+
*
|
|
38
|
+
* When a node's entire inner subtree is fully static — no `{{ }}`
|
|
39
|
+
* interpolations, no dynamic (`$`/`:`/`@`) attributes and only standard
|
|
40
|
+
* HTML tags — we skip exploding it into one child AreNode per element /
|
|
41
|
+
* text run and instead capture the inner markup verbatim. The interpreter
|
|
42
|
+
* later materialises it in a single pass (browser-parsed innerHTML /
|
|
43
|
+
* cached `<template>` clone), which:
|
|
44
|
+
* - eliminates N node + scope + scene + instruction allocations,
|
|
45
|
+
* - collapses N isolated DOM mutations into one, and
|
|
46
|
+
* - decodes HTML entities (e.g. ` `) for free.
|
|
47
|
+
*
|
|
48
|
+
* Routing outlets (AreRootNode) are intentionally excluded — they own
|
|
49
|
+
* their content dynamically. The node's OWN attributes are still
|
|
50
|
+
* extracted below, so a dynamic attribute on the island root itself
|
|
51
|
+
* (e.g. `<div :class="x"> …static… </div>`) keeps working.
|
|
52
|
+
*/
|
|
53
|
+
const isStaticIsland =
|
|
54
|
+
node instanceof AreComponentNode &&
|
|
55
|
+
!!node.content &&
|
|
56
|
+
isStaticMarkup(node.content);
|
|
57
|
+
|
|
58
|
+
if (isStaticIsland) {
|
|
59
|
+
(node as AreHTMLNode).markStatic(node.content);
|
|
60
|
+
} else {
|
|
61
|
+
super.tokenize(node, context, logger);
|
|
62
|
+
}
|
|
34
63
|
|
|
35
64
|
context.startPerformance('attributeExtraction');
|
|
36
65
|
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './instructions/AddAttribute.instruction';
|
|
|
21
21
|
export * from './instructions/AddElement.instruction';
|
|
22
22
|
export * from './instructions/AddInterpolation.instruction';
|
|
23
23
|
export * from './instructions/AddListener.instruction';
|
|
24
|
+
export * from './instructions/AddStaticHTML.instruction';
|
|
24
25
|
export * from './instructions/AddStyle.instruction';
|
|
25
26
|
export * from './instructions/AddText.instruction';
|
|
26
27
|
export * from './instructions/HideElement.instruction';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { A_Frame } from "@adaas/a-frame/core"
|
|
2
|
+
import { AreDeclaration, AreMutation, AreInstructionSerialized } from "@adaas/are";
|
|
3
|
+
import { AreHtmlAddStaticHTMLInstructionPayload } from "./AreHTML.instructions.types";
|
|
4
|
+
import { AreHTMLInstructions } from "./AreHTML.instructions.constants";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@A_Frame.Define({
|
|
8
|
+
namespace: 'a-are-html',
|
|
9
|
+
description: 'Materialises a fully static subtree (a "static island") onto its parent element in a single pass via browser-parsed innerHTML / a cached <template> clone. Apply injects the markup; revert clears it. Decodes HTML entities (e.g. ) for free.'
|
|
10
|
+
})
|
|
11
|
+
export class AddStaticHTMLInstruction extends AreMutation<AreHtmlAddStaticHTMLInstructionPayload> {
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
parent: AreDeclaration,
|
|
15
|
+
props: AreHtmlAddStaticHTMLInstructionPayload | AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>
|
|
16
|
+
) {
|
|
17
|
+
if ('aseid' in props) {
|
|
18
|
+
super(props as AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>);
|
|
19
|
+
} else {
|
|
20
|
+
super(AreHTMLInstructions.AddStaticHTML, parent, props);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -34,6 +34,15 @@ export type AreHtmlAddStyleInstructionPayload = {
|
|
|
34
34
|
styles: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export type AreHtmlAddStaticHTMLInstructionPayload = {
|
|
38
|
+
/**
|
|
39
|
+
* Verbatim inner markup of a static island, materialised on the parent
|
|
40
|
+
* element in a single pass (browser-parsed `innerHTML` / cached `<template>`
|
|
41
|
+
* clone). Decodes HTML entities (e.g. ` `) for free via the parser.
|
|
42
|
+
*/
|
|
43
|
+
html: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
export type AreHtmlHideInstructionPayload = {
|
|
38
47
|
/**
|
|
39
48
|
* Optional explicit display value to restore when the element becomes
|
|
@@ -16,6 +16,19 @@ import { AreDirectiveMeta } from "@adaas/are-html/directive/AreDirective.meta";
|
|
|
16
16
|
description: 'AreHTMLNode represents a node in the HTML structure. It extends the base AreNode and includes properties and methods specific to HTML nodes, such as handling attributes, directives, events, and styles.'
|
|
17
17
|
})
|
|
18
18
|
export class AreHTMLNode extends AreNode {
|
|
19
|
+
/**
|
|
20
|
+
* When set, this node is a *static island* root: its entire inner subtree
|
|
21
|
+
* was detected (at tokenize time) to contain no ARE-reactive constructs —
|
|
22
|
+
* no interpolations, no dynamic attributes and only standard HTML tags.
|
|
23
|
+
*
|
|
24
|
+
* Instead of being exploded into one child AreNode per element/text node,
|
|
25
|
+
* the inner markup is preserved verbatim here and materialised in a single
|
|
26
|
+
* pass by the interpreter (browser-parsed `innerHTML` / cached `<template>`
|
|
27
|
+
* clone). The node's OWN attributes (including any dynamic `:`/`@`/`$` on
|
|
28
|
+
* the island root) still compile and stay reactive as usual.
|
|
29
|
+
*/
|
|
30
|
+
protected _staticInnerHTML?: string;
|
|
31
|
+
|
|
19
32
|
/**
|
|
20
33
|
* Actual node type.
|
|
21
34
|
* By default it's a tag name
|
|
@@ -23,6 +36,67 @@ export class AreHTMLNode extends AreNode {
|
|
|
23
36
|
get tag(): string {
|
|
24
37
|
return this.aseid.entity;
|
|
25
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The verbatim inner markup captured when this node was identified as a
|
|
42
|
+
* static island, or `undefined` for ordinary (per-node) nodes.
|
|
43
|
+
*/
|
|
44
|
+
get staticInnerHTML(): string | undefined {
|
|
45
|
+
return this._staticInnerHTML;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether this node is a static-island root (see `_staticInnerHTML`).
|
|
50
|
+
*/
|
|
51
|
+
get isStaticIsland(): boolean {
|
|
52
|
+
return this._staticInnerHTML !== undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Marks this node as a static-island root, capturing the verbatim inner
|
|
57
|
+
* markup to be materialised in one shot by the interpreter. Called by the
|
|
58
|
+
* tokenizer when the node's inner content is detected to be fully static.
|
|
59
|
+
*/
|
|
60
|
+
markStatic(innerHTML: string): void {
|
|
61
|
+
this._staticInnerHTML = innerHTML;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Deep-clone the node. Overridden to carry over the static-island marker
|
|
66
|
+
* (`_staticInnerHTML`), which lives on AreHTMLNode and is therefore NOT
|
|
67
|
+
* copied by the base AreNode.clone(). Without this, cloning a directive
|
|
68
|
+
* template ($if/$for) that wraps a static island (e.g. `<span $if>★</span>`)
|
|
69
|
+
* would drop the captured inner markup and render an empty element. The
|
|
70
|
+
* base clone() recurses via each child's polymorphic clone(), so nested
|
|
71
|
+
* island children are preserved automatically through this override.
|
|
72
|
+
*/
|
|
73
|
+
clone<T extends AreNode = AreNode>(this: T): T {
|
|
74
|
+
const cloned = super.clone() as unknown as AreHTMLNode;
|
|
75
|
+
const self = this as unknown as AreHTMLNode;
|
|
76
|
+
|
|
77
|
+
if (self._staticInnerHTML !== undefined)
|
|
78
|
+
cloned.markStatic(self._staticInnerHTML);
|
|
79
|
+
|
|
80
|
+
return cloned as unknown as T;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clone the node while transferring its existing scope to the clone (used by
|
|
85
|
+
* the $if/$for directives to turn the original node into a lightweight group
|
|
86
|
+
* container). Overridden for the same reason as `clone()`: the static-island
|
|
87
|
+
* marker must survive so a directive applied to an island root keeps its
|
|
88
|
+
* inner markup.
|
|
89
|
+
*/
|
|
90
|
+
cloneWithScope<T extends AreNode = AreNode>(this: T): T {
|
|
91
|
+
const cloned = super.cloneWithScope() as unknown as AreHTMLNode;
|
|
92
|
+
const self = this as unknown as AreHTMLNode;
|
|
93
|
+
|
|
94
|
+
if (self._staticInnerHTML !== undefined)
|
|
95
|
+
cloned.markStatic(self._staticInnerHTML);
|
|
96
|
+
|
|
97
|
+
return cloned as unknown as T;
|
|
98
|
+
}
|
|
99
|
+
|
|
26
100
|
/**
|
|
27
101
|
* The static attributes defined for the node, which are typically used to represent static properties or characteristics of the node that do not change based on the context or state. These attributes are usually defined in the template and are not reactive.
|
|
28
102
|
*
|
|
@@ -160,9 +160,9 @@ export class AreRoot extends Are {
|
|
|
160
160
|
child.transform();
|
|
161
161
|
|
|
162
162
|
child.compile();
|
|
163
|
-
// The
|
|
164
|
-
//
|
|
165
|
-
//
|
|
163
|
+
// The initial mount is atomic (synchronous): the routed subtree is
|
|
164
|
+
// rendered in one uninterrupted pass so no update can observe a
|
|
165
|
+
// partially mounted tree. The await is a harmless no-op here.
|
|
166
166
|
await child.mount();
|
|
167
167
|
}
|
|
168
168
|
}
|