@symbiote-native/angular 2.0.1 → 3.0.1
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/README.md +5 -4
- package/babel-register-composed.cjs +0 -8
- package/build/angular/callback-host.d.ts +25 -0
- package/build/angular/callback-host.js +211 -0
- package/build/angular/callback-host.js.map +1 -0
- package/build/angular/change-detection-flush.d.ts +14 -2
- package/build/angular/change-detection-flush.js +54 -6
- package/build/angular/change-detection-flush.js.map +1 -1
- package/build/angular/components/flat-list/index.js +39 -86
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image-shared.js +49 -110
- package/build/angular/components/image-shared.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +33 -72
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.js +58 -103
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable-props.d.ts +2 -0
- package/build/angular/components/section-list/index.js +20 -103
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/touchable-props.d.ts +1 -0
- package/build/angular/components/virtualized-list/directives.js +32 -22
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +7 -1
- package/build/angular/components/virtualized-list/index.js +350 -359
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +20 -12
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.js +36 -142
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +1 -1
- package/build/angular/components.js +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +9 -8
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +13 -12
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +6 -6
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +13 -0
- package/build/angular/diagnostics.js +18 -0
- package/build/angular/diagnostics.js.map +1 -1
- package/build/angular/element-props.d.ts +1 -3
- package/build/angular/elements.d.ts +46 -32
- package/build/angular/elements.js +217 -434
- package/build/angular/elements.js.map +1 -1
- package/build/angular/index.d.ts +3 -1
- package/build/angular/index.js +44 -13
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.js +0 -2
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.js +69 -74
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/animated/index.d.ts +30 -19
- package/build/angular/modules/status-bar/index.js +2 -8
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +14 -28
- package/build/angular/primitives/index.js +44 -107
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +1 -0
- package/build/angular/primitives/shared.js +47 -32
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +40 -3
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +80 -0
- package/build/angular/renderer/index.js +324 -114
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/runtime-matching.d.ts +9 -0
- package/build/angular/runtime-matching.js +82 -0
- package/build/angular/runtime-matching.js.map +1 -0
- package/build/angular/services/color-scheme.service.js +4 -4
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +4 -4
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/build/angular/style-host.d.ts +11 -0
- package/build/angular/style-host.js +86 -0
- package/build/angular/style-host.js.map +1 -0
- package/package.json +6 -6
- package/src/callback-host.ts +229 -0
- package/src/change-detection-flush.ts +62 -9
- package/src/components/flat-list/index.ts +2 -1
- package/src/components/keyboard-avoiding-view/index.ts +8 -0
- package/src/components/pressable-props.ts +8 -0
- package/src/components/touchable-props.ts +4 -0
- package/src/components/virtualized-list/index.ts +98 -45
- package/src/components.ts +0 -2
- package/src/diagnostics.ts +18 -0
- package/src/element-props.ts +1 -5
- package/src/elements.ts +159 -38
- package/src/index.ts +47 -14
- package/src/primitives/index.ts +14 -70
- package/src/primitives/shared.ts +21 -6
- package/src/render/index.ts +56 -2
- package/src/renderer/index.ts +378 -104
- package/src/runtime-matching.ts +80 -0
- package/src/style-host.ts +94 -0
- package/tsconfig.angular.base.json +11 -0
|
@@ -4,76 +4,35 @@
|
|
|
4
4
|
// engine's tiny mutation API; the engine owns all Fabric clone-on-write, shared with
|
|
5
5
|
// every other adapter. This is the Angular twin of adapters/vue/src/renderer.ts — proof
|
|
6
6
|
// that the same engine mutation API drives both frameworks.
|
|
7
|
-
import { appendChild, createAnchor, createElement, createRawText, dlog, getExplicitStyle, insertBefore, isDebug, isSymbioteEvent, isSymbioteNode, removeChild, routeProp, setEventListener,
|
|
7
|
+
import { appendChild, createAnchor, createElement, createRawText, componentOf, dlog, getExplicitStyle, insertBefore, isDebug, isRawTextNode, isSymbioteEvent, isSymbioteNode, isTextContainer, isSameShallowStyle, nextSiblingOf, parentOf, propOf, registerBeforeFlush, removeChild, routeProp, setEventListener, setText, textOf, toPublicInstance, SymbioteSurface, } from '@symbiote-native/engine';
|
|
8
8
|
import { COMPONENT_DESCRIPTORS, descriptorFor, } from '@symbiote-native/components';
|
|
9
|
-
import { foldHostBag } from '@symbiote-native/components/fold-host-bag';
|
|
10
9
|
import { isAnchorHostComponent } from '../anchor-host-registry';
|
|
11
|
-
import { countAngular, noteAngularCreate, noteAngularWrite, } from '../diagnostics';
|
|
10
|
+
import { countAngular, noteAngularCreate, noteAngularStyleWrite, noteAngularWrite, } from '../diagnostics';
|
|
12
11
|
function isSurface(parent) {
|
|
13
12
|
return parent instanceof SymbioteSurface;
|
|
14
13
|
}
|
|
15
14
|
function isRawText(node) {
|
|
16
|
-
return node
|
|
15
|
+
return isRawTextNode(node);
|
|
17
16
|
}
|
|
18
17
|
function isRecord(value) {
|
|
19
18
|
return typeof value === 'object' && value !== null;
|
|
20
19
|
}
|
|
21
|
-
// RN's
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// doesn't have this split" reasoning elsewhere in this file), so createElement runs for its INNER
|
|
26
|
-
// text node too; seeding here therefore covers both the composed Text and any bare
|
|
27
|
-
// `text` a future lowering emits, uniformly. Found missing 2026-08-31 (a cross-adapter
|
|
28
|
-
// key-count diff against Vue's real BenchmarkRow.vue) — without this a lowered Text's
|
|
29
|
-
// `numberOfLines` clips with no ellipsis, silently, on device only. Vue's renderer already does
|
|
30
|
-
// this (`adapters/vue/src/renderer/index.ts`'s `seedTextDefaults`); Angular's simply never did.
|
|
20
|
+
// RN's TWO TEXT DEFAULTS LEFT THIS RENDERER ENTIRELY ON 2026-09-18, in two steps a fortnight apart.
|
|
21
|
+
// The SEED went first, to the payload builder — writing them as props cost a crossing every time an
|
|
22
|
+
// app authored the same value (6 000 per 1 000-row create, `writesOfUnchanged`). What stayed was a
|
|
23
|
+
// resolver for the clear-back path: a write of `undefined` looked up the default instead of clearing.
|
|
31
24
|
//
|
|
32
|
-
//
|
|
33
|
-
// `
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// `
|
|
37
|
-
//
|
|
38
|
-
// exactly the seed this function needs.
|
|
39
|
-
function seedTextDefaults(node) {
|
|
40
|
-
const seeded = foldHostBag('text', {});
|
|
41
|
-
for (const [key, value] of Object.entries(seeded))
|
|
42
|
-
setProp(node, key, value);
|
|
43
|
-
}
|
|
44
|
-
// An explicit `undefined` must NOT clear one of those defaults — RN treats a missing prop and an
|
|
45
|
-
// explicit undefined alike, and only a literal `false` opts allowFontScaling out. Reached only
|
|
46
|
-
// when a later write clears a key back to undefined, so it costs nothing on the common path.
|
|
47
|
-
// `foldHostBag` folds every Text default when called this way (not just `key`), because its
|
|
48
|
-
// contract is "fold a whole bag" — the extra key computed alongside `key` is simply unread here.
|
|
49
|
-
function textDefaultFor(el, key) {
|
|
50
|
-
if (isSurface(el) || !el.isText)
|
|
51
|
-
return undefined;
|
|
52
|
-
return foldHostBag('text', { [key]: undefined })[key];
|
|
53
|
-
}
|
|
54
|
-
// RN's `id` is the modern W3C-named alias for `nativeID` (core/components/host-primitives.cjs's
|
|
55
|
-
// `ID_ALIAS`) — View.js/Text.js copy it over unconditionally, so the two name ONE native prop.
|
|
56
|
-
// React/Solid/Svelte fold it in a wrapper or transform; Angular had it nowhere, so `<view
|
|
57
|
-
// id="x">`/`[id]="x"` reached Fabric with an unknown `id` key and no `nativeID` — silently, on
|
|
58
|
-
// device only. Lives in the renderer (mirroring Vue's `PROP_ALIASES`) so it covers every path
|
|
59
|
-
// that can set a prop — `setAttribute`, `setProperty`, and (should a future lowering emit one) a
|
|
60
|
-
// hand-built call — not just the composed component's own `id` @Input.
|
|
61
|
-
// `symbioteStyle` is what the lowering transform emits in place of `[style]`: Angular routes a
|
|
62
|
-
// `style` binding to its own CSS styling engine, which cannot represent an RN StyleProp (an array
|
|
63
|
-
// throws inside change detection). Under any other name it is an ordinary property binding and
|
|
64
|
-
// arrives here.
|
|
25
|
+
// That is gone too, and for the reason the seed was: `applyTextDefaults` (and its C++ twin) runs on
|
|
26
|
+
// EVERY commit of every `RCTText`, so a cleared key is absent for exactly as long as it takes the
|
|
27
|
+
// payload builder to supply the default again. The resolver was answering a question nothing asks.
|
|
28
|
+
//
|
|
29
|
+
// `PROP_ALIASES` (`id` -> `nativeID`) left this renderer on 2026-09-18 — `routeProp` resolves it
|
|
30
|
+
// for every adapter now, so every path that can set a prop still reaches it.
|
|
65
31
|
// Angular's two-way sugar `[(value)]` compiles to a `(valueChange)` binding; the engine knows the
|
|
66
32
|
// same fold as the function prop `onValueChange`. See `listen()`. The two names live in a leaf
|
|
67
33
|
// module so `elements.ts`'s ControlValueAccessor can name them without importing this cyclic file.
|
|
68
34
|
import { VALUE_CHANGE_EVENT, VALUE_CHANGE_PROP } from './value-change';
|
|
69
|
-
import { flushViewFor } from '../change-detection-flush';
|
|
70
|
-
const PROP_ALIASES = new Map([
|
|
71
|
-
['id', 'nativeID'],
|
|
72
|
-
['symbioteStyle', 'style'],
|
|
73
|
-
]);
|
|
74
|
-
function aliasedPropName(name) {
|
|
75
|
-
return PROP_ALIASES.get(name) ?? name;
|
|
76
|
-
}
|
|
35
|
+
import { createCallbackWrapper, flushViewFor, isWrappableCallback, } from '../change-detection-flush';
|
|
77
36
|
// The app callbacks an engine behavior READS BACK inside the same microtask turn, as an Angular
|
|
78
37
|
// `(event)` binding — `valueChange` is handled in `listen` on its own, since it also needs the field
|
|
79
38
|
// unwrapped. Zoneless change detection is a macrotask, so without a flush the behavior reads the
|
|
@@ -84,6 +43,42 @@ function aliasedPropName(name) {
|
|
|
84
43
|
// wrapped by `SymbioteElement` itself, which reaches every one of them rather than a named three,
|
|
85
44
|
// and adds the `markForCheck` a prop callback needs and an event binding gets from Angular.
|
|
86
45
|
const READ_BACK_EVENTS = new Set(['refresh']);
|
|
46
|
+
// How many distinct style objects the renderer keeps to hand back by identity. A screen's styles are
|
|
47
|
+
// a handful; the bench row has four. See `publishedStyles`.
|
|
48
|
+
const STYLE_CACHE = 16;
|
|
49
|
+
// `listen(VALUE_CHANGE_EVENT)` can be called TWICE for the SAME logical `(valueChange)`/`[(value)]`
|
|
50
|
+
// binding on a MATCHED element — Angular's own compiled output-binding codegen wires one listener,
|
|
51
|
+
// `ValueChangeElement.ngOnInit`'s manual bridge wires a second (`lowered-two-way-value.test.ts`,
|
|
52
|
+
// "delivers a bound handler exactly once per change"). Those two must DEDUPE (last one replaces,
|
|
53
|
+
// not composes — firing both double-delivers the identical value). A genuinely different explicit
|
|
54
|
+
// `[onValueChange]` app-level handler (routed here via `ngOnChanges`'s generic per-input loop) must
|
|
55
|
+
// instead be PRESERVED and called alongside the bridge, or it silently stops firing the moment
|
|
56
|
+
// `[(value)]`/`(valueChange)` is also bound. This set is what tells the two cases apart.
|
|
57
|
+
const bridgedValueChangeHandlers = new WeakSet();
|
|
58
|
+
// The genuinely explicit `[onValueChange]` app handler for a node, if one is bound — read LIVE by
|
|
59
|
+
// the composed forward function on every event rather than captured once at compose time. That is
|
|
60
|
+
// what makes handler installation order-independent: `ngOnChanges` (the explicit handler) and the
|
|
61
|
+
// two `listen(VALUE_CHANGE_EVENT)` calls (the bridge, see below) can happen in any interleaving —
|
|
62
|
+
// at initial mount OR on a later `[onValueChange]` rebind (`ngOnChanges` again, same code path,
|
|
63
|
+
// routed through `setProperty`) — without one silently overwriting the other's closure.
|
|
64
|
+
const explicitValueChangeHandlers = new WeakMap();
|
|
65
|
+
function composeValueChangeHandler(target, bridgeCallback) {
|
|
66
|
+
const forwardValue = withChangeDetection(target, (event) => {
|
|
67
|
+
explicitValueChangeHandlers.get(target)?.(event);
|
|
68
|
+
if (isSymbioteEvent(event)) {
|
|
69
|
+
if ('text' in event)
|
|
70
|
+
return bridgeCallback(event.text);
|
|
71
|
+
if ('value' in event)
|
|
72
|
+
return bridgeCallback(event.value);
|
|
73
|
+
}
|
|
74
|
+
return bridgeCallback(event);
|
|
75
|
+
});
|
|
76
|
+
bridgedValueChangeHandlers.add(forwardValue);
|
|
77
|
+
return forwardValue;
|
|
78
|
+
}
|
|
79
|
+
function isReadBackListener(value) {
|
|
80
|
+
return typeof value === 'function';
|
|
81
|
+
}
|
|
87
82
|
function withChangeDetection(node, listener) {
|
|
88
83
|
return (event) => {
|
|
89
84
|
const result = listener(event);
|
|
@@ -112,8 +107,8 @@ function describeHost(node) {
|
|
|
112
107
|
return 'surface';
|
|
113
108
|
const anchorId = anchorDebugIds.get(node);
|
|
114
109
|
return anchorId !== undefined
|
|
115
|
-
? `${node
|
|
116
|
-
: node
|
|
110
|
+
? `${componentOf(node)}#${anchorId}`
|
|
111
|
+
: componentOf(node);
|
|
117
112
|
}
|
|
118
113
|
// A hand-written tag ngtsc accepts must contain a HYPHEN, so the six dashless intrinsics
|
|
119
114
|
// (`view`/`text`/`pressable`/`image`/`switch`/`modal`) need a second spelling that has one.
|
|
@@ -154,23 +149,162 @@ const PRIMITIVE_SELECTOR_ALIAS = {
|
|
|
154
149
|
// stray RCTRawText would paint). Angular's ɵɵtext only ever lands text inside a <text>,
|
|
155
150
|
// but guard anyway for parity with the Vue adapter and to fail loudly on a bad template.
|
|
156
151
|
function assertTextPlacement(child, parent) {
|
|
157
|
-
if (isRawText(child) && (isSurface(parent) || !parent
|
|
158
|
-
throw new Error(`Text string "${
|
|
152
|
+
if (isRawText(child) && (isSurface(parent) || !isTextContainer(parent))) {
|
|
153
|
+
throw new Error(`Text string "${textOf(child) ?? ''}" must be rendered inside a <text>`);
|
|
159
154
|
}
|
|
160
155
|
}
|
|
161
156
|
// One renderer per mounted surface. Every mutation asks the surface to (microtask-
|
|
162
157
|
// coalesced) recommit — the same seam Vue uses; a burst of Angular change-detection
|
|
163
158
|
// mutations collapses into one completeRoot.
|
|
164
159
|
export class SymbioteRenderer {
|
|
165
|
-
surface;
|
|
166
|
-
data = {};
|
|
167
|
-
// Angular calls destroyNode per-node only when this is non-null; teardown happens in
|
|
168
|
-
// render.ts (unmount), so per-node cleanup is a no-op.
|
|
169
|
-
destroyNode = null;
|
|
170
160
|
constructor(surface) {
|
|
171
161
|
this.surface = surface;
|
|
162
|
+
this.data = {};
|
|
163
|
+
// Angular calls destroyNode per-node only when this is non-null; teardown happens in
|
|
164
|
+
// render.ts (unmount), so per-node cleanup is a no-op.
|
|
165
|
+
this.destroyNode = null;
|
|
166
|
+
this.pendingStyle = {};
|
|
167
|
+
/**
|
|
168
|
+
* Style objects this renderer has already published, so a list of identical rows sends ONE.
|
|
169
|
+
*
|
|
170
|
+
* The intern table keys by IDENTITY, so a fresh object per node is a fresh entry per node and the
|
|
171
|
+
* host converts every entry across JSI. Measured on the bench arm: `values` 7 001 against Vue's
|
|
172
|
+
* 3 004 for the identical tree, with `convert` 19.5 ms against 0.6 — and the gap is the KIND
|
|
173
|
+
* rather than the count, ~4 000 of Angular's being objects that convert recursively where Vue's
|
|
174
|
+
* are scalars plus four hoisted styles every row shares.
|
|
175
|
+
*
|
|
176
|
+
* Angular cannot share them on its own: `ɵɵstyleMap` hands over KEYS, so the object is this
|
|
177
|
+
* renderer's own construction. Recognising one it has already built is what puts an Angular app
|
|
178
|
+
* back on the footing of a framework whose author hoisted the constant.
|
|
179
|
+
*
|
|
180
|
+
* SMALL AND MRU. A screen has a handful of distinct styles and re-publishes them thousands of
|
|
181
|
+
* times, so a hit is almost always at the front; a miss costs at most `STYLE_CACHE` shallow
|
|
182
|
+
* compares, which is nothing beside the conversion it saves. An app with more distinct styles than
|
|
183
|
+
* this simply stops sharing — it never stops being correct.
|
|
184
|
+
*/
|
|
185
|
+
this.publishedStyles = [];
|
|
186
|
+
// Ivy compiles every class= / [class.foo] / [ngClass] form down to per-token addClass/
|
|
187
|
+
// removeClass calls (never a single setAttribute('class', ...) call), so a per-node token set
|
|
188
|
+
// is accumulated here and re-joined into one string on every change, then handed to
|
|
189
|
+
// routeProp('class', ...) exactly like Vue's template `class="..."` and React's JSX
|
|
190
|
+
// `className="..."` — all three resolve through the SAME centralized class+style merge in
|
|
191
|
+
// core/engine/src/node.ts, so a class registered via the SFC/CSS-Modules style compiler
|
|
192
|
+
// resolves identically regardless of adapter.
|
|
193
|
+
this.classTokens = new WeakMap();
|
|
194
|
+
// The OTHER class source: a whole string written as a prop. Kept apart from the token set rather
|
|
195
|
+
// than merged into it, because the two are replaced independently — a new `[class]` value
|
|
196
|
+
// replaces this string entirely while leaving every `[class.foo]` token standing.
|
|
197
|
+
this.classStrings = new WeakMap();
|
|
198
|
+
this.callbackWrappers = new WeakMap();
|
|
199
|
+
// A READ AND A COMMIT BOTH ARRIVE FROM ELSEWHERE, so the renderer cannot close the run on its
|
|
200
|
+
// own: a turn whose last act is a style change has no next call to close it, and the commit
|
|
201
|
+
// would paint the node without it. `flushOps` is the one door in front of every drain.
|
|
202
|
+
this.releaseBeforeFlush = registerBeforeFlush(() => this.flushStyling());
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Angular is done with this renderer FOR ONE COMPONENT — which is not the same as this renderer
|
|
206
|
+
* being done, and the difference was a shipped bug.
|
|
207
|
+
*
|
|
208
|
+
* The factory hands ONE instance to every component on the surface, so Angular calls this
|
|
209
|
+
* whenever ANY component's views are torn down; a keyed `@for` replace destroys a thousand. So it
|
|
210
|
+
* publishes what it is holding and nothing more. Releasing the `beforeFlush` registration here
|
|
211
|
+
* closed the only door a style run has — `flushOps` is what asks a renderer for what it holds —
|
|
212
|
+
* while every still-living view went on writing into the accumulator. The style then reached
|
|
213
|
+
* Fabric only when some OTHER node's `openStyleRun` happened to close the run, which on the
|
|
214
|
+
* benchmark screen was two steps after the selection that asked for it.
|
|
215
|
+
*
|
|
216
|
+
* The registration follows the SURFACE now, and `SymbioteRendererFactory.dispose` releases it.
|
|
217
|
+
*/
|
|
218
|
+
destroy() {
|
|
219
|
+
this.flushStyling();
|
|
220
|
+
}
|
|
221
|
+
/** The surface is going away — publish what is held and stop listening. See `destroy` above. */
|
|
222
|
+
dispose() {
|
|
223
|
+
this.flushStyling();
|
|
224
|
+
this.releaseBeforeFlush();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Publish whatever the styling run is holding. Idempotent, and free when it holds nothing.
|
|
228
|
+
*
|
|
229
|
+
* IT DOES NOT REQUEST A COMMIT, and that is not an omission — the request is made when the style
|
|
230
|
+
* is ACCUMULATED, exactly where it was made before the run existed. This runs from inside
|
|
231
|
+
* `flushOps`, which the commit itself calls first, so asking there schedules a SECOND commit whose
|
|
232
|
+
* tree is already current: a full `completeRoot` plus a Yoga pass for nothing.
|
|
233
|
+
*
|
|
234
|
+
* Measured, because it did not look like a cost. The bench arm's `select` fell 17.1 -> 3.8 ms and
|
|
235
|
+
* its `remove` rose 7.1 -> 18.2 in the same runs — the extra commit lands in whichever step's
|
|
236
|
+
* microtask happens to run it, so the work had MOVED between steps rather than gone. Two rows
|
|
237
|
+
* moving by the same amount in opposite directions is what that always looks like.
|
|
238
|
+
*/
|
|
239
|
+
flushStyling() {
|
|
240
|
+
const styled = this.pendingStyleNode;
|
|
241
|
+
if (styled !== undefined) {
|
|
242
|
+
const style = this.pendingStyle;
|
|
243
|
+
this.pendingStyleNode = undefined;
|
|
244
|
+
this.pendingStyle = {};
|
|
245
|
+
routeProp(styled, 'style', this.canonicalStyle(style));
|
|
246
|
+
}
|
|
247
|
+
const classed = this.pendingClassNode;
|
|
248
|
+
if (classed !== undefined) {
|
|
249
|
+
this.pendingClassNode = undefined;
|
|
250
|
+
routeProp(classed, 'class', this.classStringFor(classed));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The union of a node's two class sources, or `undefined` when it has none.
|
|
255
|
+
*
|
|
256
|
+
* A node can be told its classes BOTH ways in one pass: Ivy compiles `[class.foo]`, `[ngClass]`
|
|
257
|
+
* and a static `class=` down to per-token `addClass`/`removeClass`, while a whole-string `class`
|
|
258
|
+
* arrives at `setProperty` — which is the shape Angular uses when a directive declares `class` as
|
|
259
|
+
* an input and the styling binding is shadowed into it (`setShadowStylingInputFlags`,
|
|
260
|
+
* `view/directives.ts`). Publishing either one alone erases the other, and the DOM renderer this
|
|
261
|
+
* mirrors has no such problem because `classList` accumulates for it.
|
|
262
|
+
*/
|
|
263
|
+
classStringFor(el) {
|
|
264
|
+
const tokens = this.classTokens.get(el);
|
|
265
|
+
const whole = this.classStrings.get(el);
|
|
266
|
+
if (whole === undefined) {
|
|
267
|
+
return tokens !== undefined && tokens.size > 0
|
|
268
|
+
? [...tokens].join(' ')
|
|
269
|
+
: undefined;
|
|
270
|
+
}
|
|
271
|
+
if (tokens === undefined || tokens.size === 0) {
|
|
272
|
+
return whole.length > 0 ? whole : undefined;
|
|
273
|
+
}
|
|
274
|
+
const union = new Set(whole.split(/\s+/u).filter(token => token.length > 0));
|
|
275
|
+
for (const token of tokens)
|
|
276
|
+
union.add(token);
|
|
277
|
+
return union.size > 0 ? [...union].join(' ') : undefined;
|
|
278
|
+
}
|
|
279
|
+
/** A published object equal to this one, or this one — which then becomes the published copy. */
|
|
280
|
+
canonicalStyle(style) {
|
|
281
|
+
for (let at = 0; at < this.publishedStyles.length; at += 1) {
|
|
282
|
+
const known = this.publishedStyles[at];
|
|
283
|
+
if (!isSameShallowStyle(style, known))
|
|
284
|
+
continue;
|
|
285
|
+
if (at > 0) {
|
|
286
|
+
this.publishedStyles.splice(at, 1);
|
|
287
|
+
this.publishedStyles.unshift(known);
|
|
288
|
+
}
|
|
289
|
+
return known;
|
|
290
|
+
}
|
|
291
|
+
this.publishedStyles.unshift(style);
|
|
292
|
+
if (this.publishedStyles.length > STYLE_CACHE)
|
|
293
|
+
this.publishedStyles.pop();
|
|
294
|
+
return style;
|
|
295
|
+
}
|
|
296
|
+
/** The accumulator for this node's style run, opening one (and closing any other) if needed. */
|
|
297
|
+
openStyleRun(el) {
|
|
298
|
+
if (this.pendingStyleNode === el)
|
|
299
|
+
return this.pendingStyle;
|
|
300
|
+
this.flushStyling();
|
|
301
|
+
// Seeded from what is STANDING, because Angular sends only the keys that changed — an update
|
|
302
|
+
// that moves one key must not drop the rest.
|
|
303
|
+
const current = getExplicitStyle(el);
|
|
304
|
+
this.pendingStyle = isRecord(current) ? { ...current } : {};
|
|
305
|
+
this.pendingStyleNode = el;
|
|
306
|
+
return this.pendingStyle;
|
|
172
307
|
}
|
|
173
|
-
destroy() { }
|
|
174
308
|
createElement(name) {
|
|
175
309
|
// `name` is the component's host tag — a symbiote intrinsic (`view`,
|
|
176
310
|
// `text`, …), a public ergonomic alias (`View`, `Text`), or a raw Fabric view
|
|
@@ -207,8 +341,6 @@ export class SymbioteRenderer {
|
|
|
207
341
|
// (e.g. 'RCTView') and any registered behavior silently never attaches — Vue's renderer
|
|
208
342
|
// already passes this correctly (`createElement(descriptor.component, descriptor.isText, type)`).
|
|
209
343
|
const node = createElement(descriptor.component, descriptor.isText, engineName);
|
|
210
|
-
if (descriptor.isText)
|
|
211
|
-
seedTextDefaults(node);
|
|
212
344
|
if (isDebug()) {
|
|
213
345
|
dlog(`angular createElement ${name} -> ${descriptor.component}`);
|
|
214
346
|
}
|
|
@@ -283,13 +415,21 @@ export class SymbioteRenderer {
|
|
|
283
415
|
// Detach from the child's own retained parent (a top-level node lives in
|
|
284
416
|
// surface.children with no parent). Angular's `parent` arg is ignored in favor of the
|
|
285
417
|
// authoritative link, mirroring the Vue adapter's remove.
|
|
418
|
+
// Angular tears down a root view by removing its HOST, which here is the surface itself. There
|
|
419
|
+
// is nothing above it to detach from — the old retained tree absorbed the call (`indexOf` of a
|
|
420
|
+
// node that is not in the list is -1), and the mutation buffer cannot: it would record a remove
|
|
421
|
+
// naming the surface as a child of its own node.
|
|
422
|
+
if (isSurface(oldChild))
|
|
423
|
+
return;
|
|
286
424
|
countAngular('nodesRemoved');
|
|
287
425
|
if (isDebug()) {
|
|
288
426
|
const angularParent = _parent !== null ? describeHost(_parent) : 'null';
|
|
289
|
-
const retainedParent = oldChild
|
|
427
|
+
const retainedParent = parentOf(oldChild) !== undefined
|
|
428
|
+
? describeHost(parentOf(oldChild))
|
|
429
|
+
: 'none';
|
|
290
430
|
dlog(`Angular renderer removeChild angularParent=${angularParent} retainedParent=${retainedParent} child=${describeHost(oldChild)}`);
|
|
291
431
|
}
|
|
292
|
-
const parent = oldChild
|
|
432
|
+
const parent = parentOf(oldChild);
|
|
293
433
|
if (parent !== undefined)
|
|
294
434
|
removeChild(parent, oldChild);
|
|
295
435
|
else
|
|
@@ -310,12 +450,12 @@ export class SymbioteRenderer {
|
|
|
310
450
|
// does `inject(ViewContainerRef)`, e.g. VListOutletDirective) forwards this null straight into
|
|
311
451
|
// insertBefore without checking it; without that guard it crashed on-device.
|
|
312
452
|
parentNode(node) {
|
|
313
|
-
return node
|
|
453
|
+
return parentOf(node) ?? null;
|
|
314
454
|
}
|
|
315
455
|
nextSibling(node) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return
|
|
456
|
+
// `?? null` because Renderer2 types the miss as null; the engine answers undefined
|
|
457
|
+
// uniformly and owns the top-level fallback through the surface it is handed.
|
|
458
|
+
return nextSiblingOf(node, this.surface) ?? null;
|
|
319
459
|
}
|
|
320
460
|
// locateHostElement always routes createComponent's `hostElement` THROUGH here as
|
|
321
461
|
// `selectorOrNode` (Angular's own core.mjs) — it is never bypassed just because a real
|
|
@@ -324,31 +464,27 @@ export class SymbioteRenderer {
|
|
|
324
464
|
selectRootElement(selectorOrNode) {
|
|
325
465
|
return typeof selectorOrNode === 'string' ? this.surface : selectorOrNode;
|
|
326
466
|
}
|
|
467
|
+
// The three prop writers below close the styling run FIRST. A read or a commit would do it through
|
|
468
|
+
// `registerBeforeFlush`, but neither happens here: this is one prop write landing on the same node
|
|
469
|
+
// whose `style` or `class` is still held, and a run published afterwards would overwrite it.
|
|
327
470
|
setAttribute(el, name, value) {
|
|
328
471
|
if (isSurface(el))
|
|
329
472
|
return;
|
|
473
|
+
this.flushStyling();
|
|
330
474
|
countAngular('rendererWrites');
|
|
331
475
|
noteAngularWrite(name);
|
|
332
|
-
routeProp(el,
|
|
476
|
+
routeProp(el, name, value);
|
|
333
477
|
this.surface.requestCommit();
|
|
334
478
|
}
|
|
335
479
|
removeAttribute(el, name) {
|
|
336
480
|
if (isSurface(el))
|
|
337
481
|
return;
|
|
482
|
+
this.flushStyling();
|
|
338
483
|
countAngular('rendererWrites');
|
|
339
484
|
noteAngularWrite(name);
|
|
340
|
-
|
|
341
|
-
routeProp(el, aliased, textDefaultFor(el, aliased));
|
|
485
|
+
routeProp(el, name, undefined);
|
|
342
486
|
this.surface.requestCommit();
|
|
343
487
|
}
|
|
344
|
-
// Ivy compiles every class= / [class.foo] / [ngClass] form down to per-token addClass/
|
|
345
|
-
// removeClass calls (never a single setAttribute('class', ...) call), so a per-node token set
|
|
346
|
-
// is accumulated here and re-joined into one string on every change, then handed to
|
|
347
|
-
// routeProp('class', ...) exactly like Vue's template `class="..."` and React's JSX
|
|
348
|
-
// `className="..."` — all three resolve through the SAME centralized class+style merge in
|
|
349
|
-
// core/engine/src/node.ts, so a class registered via the SFC/CSS-Modules style compiler
|
|
350
|
-
// resolves identically regardless of adapter.
|
|
351
|
-
classTokens = new WeakMap();
|
|
352
488
|
addClass(el, name) {
|
|
353
489
|
if (isSurface(el))
|
|
354
490
|
return;
|
|
@@ -357,7 +493,7 @@ export class SymbioteRenderer {
|
|
|
357
493
|
const tokens = this.classTokens.get(el) ?? new Set();
|
|
358
494
|
tokens.add(name);
|
|
359
495
|
this.classTokens.set(el, tokens);
|
|
360
|
-
|
|
496
|
+
this.openClassRun(el);
|
|
361
497
|
this.surface.requestCommit();
|
|
362
498
|
}
|
|
363
499
|
removeClass(el, name) {
|
|
@@ -369,9 +505,17 @@ export class SymbioteRenderer {
|
|
|
369
505
|
countAngular('rendererWrites');
|
|
370
506
|
noteAngularWrite('class');
|
|
371
507
|
tokens.delete(name);
|
|
372
|
-
|
|
508
|
+
this.openClassRun(el);
|
|
373
509
|
this.surface.requestCommit();
|
|
374
510
|
}
|
|
511
|
+
// The token SET is the accumulator here — `addClass` has already put the token in it — so this
|
|
512
|
+
// only has to remember whose run is open. Closing it re-joins the set once.
|
|
513
|
+
openClassRun(el) {
|
|
514
|
+
if (this.pendingClassNode === el)
|
|
515
|
+
return;
|
|
516
|
+
this.flushStyling();
|
|
517
|
+
this.pendingClassNode = el;
|
|
518
|
+
}
|
|
375
519
|
// Angular decomposes a [style] binding into per-key setStyle calls (ɵɵstyleMap). RN wants
|
|
376
520
|
// the whole style object as one `style` prop, so merge each key into it — onto the explicit
|
|
377
521
|
// style half tracked by routeProp's centralized class+style merge (core/engine/src/node.ts),
|
|
@@ -381,22 +525,23 @@ export class SymbioteRenderer {
|
|
|
381
525
|
if (isSurface(el))
|
|
382
526
|
return;
|
|
383
527
|
countAngular('rendererWrites');
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const base = isRecord(current) ? current : {};
|
|
387
|
-
routeProp(el, 'style', { ...base, [style]: value });
|
|
528
|
+
noteAngularStyleWrite(style);
|
|
529
|
+
this.openStyleRun(el)[style] = value;
|
|
388
530
|
this.surface.requestCommit();
|
|
389
531
|
}
|
|
390
532
|
removeStyle(el, style) {
|
|
391
533
|
if (isSurface(el))
|
|
392
534
|
return;
|
|
393
|
-
|
|
394
|
-
|
|
535
|
+
// A remove on a node with no style at all is Angular clearing a binding it never set. Opening a
|
|
536
|
+
// run for it would publish an empty style onto a node that had none, which the old early return
|
|
537
|
+
// was there to avoid.
|
|
538
|
+
if (this.pendingStyleNode !== el && !isRecord(getExplicitStyle(el)))
|
|
395
539
|
return;
|
|
396
540
|
countAngular('rendererWrites');
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
541
|
+
noteAngularStyleWrite(style);
|
|
542
|
+
const run = this.openStyleRun(el);
|
|
543
|
+
// The accumulator is this renderer's own object, never the node's — see `openStyleRun`.
|
|
544
|
+
delete run[style];
|
|
400
545
|
this.surface.requestCommit();
|
|
401
546
|
}
|
|
402
547
|
// [prop]="x" bindings. routeProp makes the prop-vs-event decision from the node's
|
|
@@ -406,10 +551,63 @@ export class SymbioteRenderer {
|
|
|
406
551
|
return;
|
|
407
552
|
countAngular('rendererWrites');
|
|
408
553
|
noteAngularWrite(name);
|
|
409
|
-
|
|
410
|
-
|
|
554
|
+
// A WHOLE CLASS STRING IS NOT AN ORDINARY PROP: it is one of two sources the node's class list
|
|
555
|
+
// is built from, and writing it straight through would erase every `[class.foo]` token the
|
|
556
|
+
// other one put there. It joins the class run instead, exactly as `addClass` does.
|
|
557
|
+
if (name === 'class') {
|
|
558
|
+
this.openClassRun(el);
|
|
559
|
+
this.classStrings.set(el, typeof value === 'string' ? value : '');
|
|
560
|
+
this.surface.requestCommit();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// `ngOnChanges` routes every `[onValueChange]` write here (both the initial bind and any
|
|
564
|
+
// later rebind to a new function reference) — never through `listen()`. When the write is a
|
|
565
|
+
// genuinely explicit app handler (not a prior bridge's own forward function), update the live
|
|
566
|
+
// slot `composeValueChangeHandler`'s forward function reads on every event; when a bridge is
|
|
567
|
+
// already installed on this node, that's ALL that's needed — the installed forward function
|
|
568
|
+
// picks the new handler up on its own, no re-route. Without this, a rebind after mount would
|
|
569
|
+
// silently kill the `[(value)]` two-way sync the moment it overwrote the composed prop raw.
|
|
570
|
+
if (name === VALUE_CHANGE_PROP) {
|
|
571
|
+
if (isReadBackListener(value) && !bridgedValueChangeHandlers.has(value)) {
|
|
572
|
+
explicitValueChangeHandlers.set(el, value);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
explicitValueChangeHandlers.delete(el);
|
|
576
|
+
}
|
|
577
|
+
const current = propOf(el, VALUE_CHANGE_PROP);
|
|
578
|
+
const bridgeActive = isReadBackListener(current) && bridgedValueChangeHandlers.has(current);
|
|
579
|
+
if (bridgeActive) {
|
|
580
|
+
this.surface.requestCommit();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
this.flushStyling();
|
|
585
|
+
routeProp(el, name, this.wrapCallback(el, name, value));
|
|
411
586
|
this.surface.requestCommit();
|
|
412
587
|
}
|
|
588
|
+
// AN `on*` PROP IS CALLED BY THE ENGINE, so Angular is told nothing and a plain field mutation
|
|
589
|
+
// inside the app's handler dirties no view — the "pan readout stuck at dx 0" bug
|
|
590
|
+
// `change-detection-flush.ts` records. It used to be wrapped by `SymbioteElement.ngOnChanges`,
|
|
591
|
+
// which is the wrong place now that most element directives are withheld from runtime matching
|
|
592
|
+
// (`../runtime-matching`): a withheld tag's binding reaches the renderer DIRECTLY through
|
|
593
|
+
// `ɵɵproperty` and never passes through a directive at all.
|
|
594
|
+
//
|
|
595
|
+
// Here it is reached by both paths and by every adapter surface — an element binding, a composed
|
|
596
|
+
// component's flat bag, an imperative write — which is a better home than the one it left.
|
|
597
|
+
//
|
|
598
|
+
// The cache is per NODE rather than per caller, because a node is what both paths agree on, and the
|
|
599
|
+
// wrapper is built on the first callback a node receives: the overwhelming majority of tags carry
|
|
600
|
+
// no `on*` prop, and an eager one would be a closure and a `WeakMap` per element for nothing.
|
|
601
|
+
wrapCallback(node, name, value) {
|
|
602
|
+
if (!isWrappableCallback(name, value))
|
|
603
|
+
return value;
|
|
604
|
+
let wrapper = this.callbackWrappers.get(node);
|
|
605
|
+
if (wrapper === undefined) {
|
|
606
|
+
wrapper = createCallbackWrapper(node);
|
|
607
|
+
this.callbackWrappers.set(node, wrapper);
|
|
608
|
+
}
|
|
609
|
+
return wrapper(name, value);
|
|
610
|
+
}
|
|
413
611
|
setValue(node, value) {
|
|
414
612
|
countAngular('rendererWrites');
|
|
415
613
|
noteAngularWrite('#text');
|
|
@@ -429,11 +627,11 @@ export class SymbioteRenderer {
|
|
|
429
627
|
if (!isSymbioteNode(target))
|
|
430
628
|
return () => { };
|
|
431
629
|
// `[(value)]` desugars to `(valueChange)`, which is the spelling every Angular template writes
|
|
432
|
-
// for a Switch or a TextInput.
|
|
433
|
-
//
|
|
434
|
-
//
|
|
630
|
+
// for a Switch or a TextInput. It used to be an `@Output()` a wrapper derived from the raw
|
|
631
|
+
// `change` payload; a tag has no component, and registering `valueChange` as an engine event
|
|
632
|
+
// would wait forever for a Fabric event of that name.
|
|
435
633
|
//
|
|
436
|
-
// The
|
|
634
|
+
// The same fold already exists under RN's own spelling: both behaviors call
|
|
437
635
|
// `node.props.onValueChange(event)` — a plain function PROP, not an event
|
|
438
636
|
// (`behaviors/switch.ts`, `behaviors/text-input.ts`), with `text`/`value` carried as a FIELD on
|
|
439
637
|
// the event object rather than a second argument (Svelte forces every individual `on*` prop
|
|
@@ -442,17 +640,20 @@ export class SymbioteRenderer {
|
|
|
442
640
|
// unwrap that field back to a bare value before handing it to Angular's callback, or `text =
|
|
443
641
|
// $event` would assign the whole event object instead of the typed string/boolean.
|
|
444
642
|
if (eventName === VALUE_CHANGE_EVENT) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
643
|
+
// `ngOnChanges` may already have written an explicit `[onValueChange]` binding onto this
|
|
644
|
+
// same prop key via `routeProp` (`setProperty`'s generic reflect loop) — seed the live
|
|
645
|
+
// explicit-handler slot from it, unless what's there is a PRIOR bridge's own forward
|
|
646
|
+
// function (the Angular-vs-manual-bridge double-registration this branch already dedupes,
|
|
647
|
+
// `listen(VALUE_CHANGE_EVENT)` fires twice per element — see `bridgedValueChangeHandlers`).
|
|
648
|
+
// Composing with the explicit handler (never replacing it) is the RN-parity behavior: RN's
|
|
649
|
+
// `onChange` always fires regardless of whether `value` is controlled.
|
|
650
|
+
const currentOnValueChange = propOf(target, VALUE_CHANGE_PROP);
|
|
651
|
+
if (isReadBackListener(currentOnValueChange) &&
|
|
652
|
+
!bridgedValueChangeHandlers.has(currentOnValueChange)) {
|
|
653
|
+
explicitValueChangeHandlers.set(target, currentOnValueChange);
|
|
654
|
+
}
|
|
655
|
+
routeProp(target, VALUE_CHANGE_PROP, composeValueChangeHandler(target, callback));
|
|
656
|
+
return () => routeProp(target, VALUE_CHANGE_PROP, explicitValueChangeHandlers.get(target));
|
|
456
657
|
}
|
|
457
658
|
const listener = READ_BACK_EVENTS.has(eventName)
|
|
458
659
|
? withChangeDetection(target, callback)
|
|
@@ -465,14 +666,23 @@ export class SymbioteRenderer {
|
|
|
465
666
|
// surface-bound renderer for every component (begin/end commit-coalescing is unnecessary —
|
|
466
667
|
// requestCommit already microtask-coalesces).
|
|
467
668
|
export class SymbioteRendererFactory {
|
|
468
|
-
surface;
|
|
469
|
-
renderer;
|
|
470
669
|
constructor(surface) {
|
|
471
670
|
this.surface = surface;
|
|
472
671
|
}
|
|
473
672
|
createRenderer(_hostElement, _type) {
|
|
474
673
|
return (this.renderer ??= new SymbioteRenderer(this.surface));
|
|
475
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* The surface is being torn down, so the one renderer it shares can stop listening.
|
|
677
|
+
*
|
|
678
|
+
* IT IS THE FACTORY'S JOB and not Angular's, because the instance outlives any single component:
|
|
679
|
+
* Angular's own `Renderer2.destroy()` arrives once per destroyed component and must not close a
|
|
680
|
+
* door every surviving view still writes through. See `SymbioteRenderer.destroy`.
|
|
681
|
+
*/
|
|
682
|
+
dispose() {
|
|
683
|
+
this.renderer?.dispose();
|
|
684
|
+
this.renderer = undefined;
|
|
685
|
+
}
|
|
476
686
|
// Not commit coalescing (requestCommit owns that) — a per-CD-pass counter only, now that the
|
|
477
687
|
// ScrollView projection bridge this used to also flush is gone (`../register.ts`'s
|
|
478
688
|
// `registerScrollViewBehavior` owns the content node and the sticky seam for every adapter).
|