@symbiote-native/angular 0.6.2 → 0.8.0
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 +3 -3
- package/bin/symbiote-angular-dev.cjs +20 -19
- package/build/angular/anchor-host-registry.js +2 -2
- package/build/angular/anchor-host-registry.js.map +1 -1
- package/build/angular/bootstrap.js +1 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.android.js +12 -4
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.ios.js +8 -3
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.d.ts +1 -1
- package/build/angular/components/activity-indicator/shared.js +4 -4
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.d.ts +4 -2
- package/build/angular/components/button.js +41 -22
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +6 -4
- package/build/angular/components/flat-list/index.js +80 -50
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.d.ts +8 -3
- package/build/angular/components/image/index.android.js +42 -10
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.d.ts +8 -3
- package/build/angular/components/image/index.ios.js +42 -10
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -1
- package/build/angular/components/image/shared.js +51 -8
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +2 -1
- package/build/angular/components/image-background.js +26 -10
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +9 -4
- package/build/angular/components/input-accessory-view/index.js +49 -10
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +44 -12
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +11 -8
- package/build/angular/components/modal/index.js +51 -29
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +12 -6
- package/build/angular/components/pressable/index.js +66 -13
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +8 -4
- package/build/angular/components/refresh-control/index.js +54 -16
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +9 -4
- package/build/angular/components/safe-area-view/index.js +48 -9
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.d.ts +2 -3
- package/build/angular/components/scroll-view/index.android.js +88 -61
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
- package/build/angular/components/scroll-view/index.ios.js +24 -18
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +35 -0
- package/build/angular/components/scroll-view/projection.js +279 -53
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +14 -5
- package/build/angular/components/scroll-view/shared.js +225 -75
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +21 -22
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +7 -1
- package/build/angular/components/section-list/index.js +91 -54
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.d.ts +7 -3
- package/build/angular/components/switch/index.android.js +52 -10
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.d.ts +7 -3
- package/build/angular/components/switch/index.ios.js +52 -10
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +13 -4
- package/build/angular/components/switch/shared.js +71 -7
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +11 -3
- package/build/angular/components/text-input.js +68 -8
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +40 -9
- package/build/angular/components/touchable/index.js +303 -67
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
- package/build/angular/components/touchable-native-feedback/index.js +63 -27
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +22 -18
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +15 -4
- package/build/angular/components/virtualized-list/index.js +326 -94
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +12 -12
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +19 -1
- package/build/angular/components/virtualized-section-list/index.js +93 -22
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +2 -2
- package/build/angular/components.js +5 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +34 -36
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +19 -22
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +6 -4
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +33 -0
- package/build/angular/diagnostics.js +82 -0
- package/build/angular/diagnostics.js.map +1 -0
- package/build/angular/host-instance/index.js +1 -1
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +9 -4
- package/build/angular/index.js +17 -3
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
- package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
- package/build/angular/modules/animated/create-animated-component.js +106 -55
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +4 -4
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +20 -3
- package/build/angular/primitives/index.js +84 -48
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +48 -2
- package/build/angular/primitives/shared.js +158 -11
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +69 -38
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +37 -2
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js +3 -3
- 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/package.json +10 -8
- package/src/anchor-host-registry.ts +2 -2
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +8 -1
- package/src/components/activity-indicator/index.ios.ts +4 -0
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +46 -19
- package/src/components/flat-list/index.ts +122 -62
- package/src/components/image/index.android.ts +58 -7
- package/src/components/image/index.ios.ts +58 -7
- package/src/components/image/shared.ts +92 -21
- package/src/components/image-background.ts +29 -9
- package/src/components/input-accessory-view/index.ts +67 -12
- package/src/components/keyboard-avoiding-view/index.ts +61 -15
- package/src/components/modal/index.ts +72 -33
- package/src/components/pressable/index.ts +117 -28
- package/src/components/refresh-control/index.ts +74 -18
- package/src/components/safe-area-view/index.ts +64 -10
- package/src/components/scroll-view/index.android.ts +62 -47
- package/src/components/scroll-view/index.ios.ts +19 -17
- package/src/components/scroll-view/projection.ts +363 -64
- package/src/components/scroll-view/shared.ts +312 -90
- package/src/components/scroll-view/sticky-header.ts +36 -24
- package/src/components/section-list/index.ts +86 -53
- package/src/components/switch/index.android.ts +60 -8
- package/src/components/switch/index.ios.ts +60 -8
- package/src/components/switch/shared.ts +93 -6
- package/src/components/text-input.ts +95 -13
- package/src/components/touchable/index.ts +348 -64
- package/src/components/touchable-native-feedback/index.ts +94 -31
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +380 -85
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +146 -33
- package/src/components.ts +9 -1
- package/src/create-portal/index.ts +31 -31
- package/src/create-tunnel/index.ts +16 -17
- package/src/descriptor-to-angular/index.ts +29 -8
- package/src/diagnostics.ts +138 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +61 -3
- package/src/modules/animated/animated-leaf-binder.ts +28 -77
- package/src/modules/animated/create-animated-component.ts +115 -47
- package/src/modules/app-registry/index.ts +9 -9
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +47 -2
- package/src/primitives/shared.ts +170 -10
- package/src/render/index.ts +77 -39
- package/src/renderer/index.ts +76 -14
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
- package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
- package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
package/src/primitives/shared.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ChangeDetectorRef,
|
|
3
3
|
Directive,
|
|
4
|
+
type DoCheck,
|
|
4
5
|
ElementRef,
|
|
5
6
|
inject,
|
|
6
7
|
Input,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
Renderer2,
|
|
9
10
|
type SimpleChanges,
|
|
10
11
|
} from '@angular/core';
|
|
12
|
+
import { countAngular } from '../diagnostics';
|
|
11
13
|
import {
|
|
12
14
|
flattenStyle,
|
|
13
15
|
isSymbioteNode,
|
|
@@ -60,7 +62,9 @@ function isStyleValue<T>(value: unknown): value is IStyleProp<T> {
|
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
export function anchorStyleProp<T>(
|
|
65
|
+
export function anchorStyleProp<T>(
|
|
66
|
+
elementRef: ElementRef<unknown>,
|
|
67
|
+
): IStyleProp<T> | undefined {
|
|
64
68
|
const value = anchorHostStyle(elementRef);
|
|
65
69
|
return isStyleValue<T>(value) ? value : undefined;
|
|
66
70
|
}
|
|
@@ -69,7 +73,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
69
73
|
return typeof value === 'object' && value !== null;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
function shallowStyleEqual(
|
|
76
|
+
function shallowStyleEqual(
|
|
77
|
+
a: Record<string, unknown>,
|
|
78
|
+
b: Record<string, unknown>,
|
|
79
|
+
): boolean {
|
|
73
80
|
const aKeys = Object.keys(a);
|
|
74
81
|
if (aKeys.length !== Object.keys(b).length) return false;
|
|
75
82
|
return aKeys.every(key => a[key] === b[key]);
|
|
@@ -96,11 +103,96 @@ export function stableAnchorStyle(
|
|
|
96
103
|
previous: unknown,
|
|
97
104
|
): Record<string, unknown> {
|
|
98
105
|
const next = flattenStyle([anchorHostStyle(elementRef), explicitStyle]);
|
|
99
|
-
return isRecord(previous) && shallowStyleEqual(previous, next)
|
|
106
|
+
return isRecord(previous) && shallowStyleEqual(previous, next)
|
|
107
|
+
? previous
|
|
108
|
+
: next;
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
const ON_PREFIX = /^on[A-Z]/;
|
|
103
112
|
|
|
113
|
+
// The one `onX` that fires per FRAME rather than per gesture, and with sticky headers RN pins
|
|
114
|
+
// `scrollEventThrottle` to 1 (ScrollView.js), so 60 times a second. Since markForCheck walks
|
|
115
|
+
// RefreshView|Dirty to the ROOT, wrapping it cost one full ancestor-screen template execution per
|
|
116
|
+
// frame - the canary's ~37fps scroll (components/virtualized-list/scroll-cost.test.ts).
|
|
117
|
+
//
|
|
118
|
+
// Nothing lost its refresh: VirtualizedList marks itself when the window moves, sticky rides the
|
|
119
|
+
// native driver, and a caller's own handler is typically an Animated.event touching no Angular
|
|
120
|
+
// state. Drag/momentum begin/end stay wrapped - once per gesture is not a hot path.
|
|
121
|
+
const PER_FRAME_CALLBACK = 'onScroll';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Restores the OnPush dirty-marking Angular skips for a `[style]` binding that a directive
|
|
125
|
+
* declares as an input. Attach it to any component declaring a `style` input:
|
|
126
|
+
* `hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }]`.
|
|
127
|
+
*
|
|
128
|
+
* An ordinary `[foo]` binding compiles to `ɵɵproperty` -> `setPropertyAndInputs`, whose last act
|
|
129
|
+
* for a component host is `markDirtyIfOnPush` (`render3/instructions/shared.ts`) — the flag
|
|
130
|
+
* `detectChangesInView` needs before it will re-execute a non-CheckAlways view. `[style]` and
|
|
131
|
+
* `[class]` do NOT take that path: they compile to the styling instructions, which on finding a
|
|
132
|
+
* directive input of the same name hand off to `setDirectiveInputsWhichShadowsStyling`
|
|
133
|
+
* (`render3/instructions/property.ts`) — two lines that write the input and stop. No dirty flag.
|
|
134
|
+
* The input lands and `ngOnChanges` fires, but the receiving component's own template, and every
|
|
135
|
+
* getter it reads, is frozen at its creation value. Verified against @angular/core 22.0.8.
|
|
136
|
+
*
|
|
137
|
+
* Symbiote sits squarely on that gap: every composed component declares `style` as a real
|
|
138
|
+
* `@Input()` on purpose, because a raw `[style]` binding would otherwise be decomposed key by key
|
|
139
|
+
* by Angular's CSS style engine, which cannot represent an RN StyleProp array or an Animated value
|
|
140
|
+
* (see `SymbiotePrimitiveHost` above). So the one binding name Angular forgets to mark is the one
|
|
141
|
+
* carrying the whole styling surface.
|
|
142
|
+
*
|
|
143
|
+
* `markForCheck()` here is exactly the skipped `markDirtyIfOnPush`, not a bigger hammer: the write
|
|
144
|
+
* happens inside the parent's refresh, and `markViewDirty` uses the `Dirty` bit alone while views
|
|
145
|
+
* are refreshing (`render3/instructions/mark_view_dirty.ts`), which the ancestors then clear at the
|
|
146
|
+
* end of their own `refreshView`. A component that already bridges its inputs into a signal (the
|
|
147
|
+
* `inputsRevision` + `computed` pattern — Pressable, ScrollView) is immune on its own, since a
|
|
148
|
+
* signal write dirties the template consumer, which `detectChangesInView` honors in any mode.
|
|
149
|
+
*/
|
|
150
|
+
@Directive({
|
|
151
|
+
selector: '[symbioteStyleInput]',
|
|
152
|
+
standalone: true,
|
|
153
|
+
inputs: ['style'],
|
|
154
|
+
})
|
|
155
|
+
export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
|
|
156
|
+
private readonly cdr = inject(ChangeDetectorRef);
|
|
157
|
+
// A host directive's ElementRef is the HOST COMPONENT's element - i.e. exactly the anchor a
|
|
158
|
+
// `class=` at the use site resolves onto.
|
|
159
|
+
private readonly elementRef = inject(ElementRef);
|
|
160
|
+
private lastAnchorStyle: unknown;
|
|
161
|
+
|
|
162
|
+
style?: unknown;
|
|
163
|
+
|
|
164
|
+
ngOnChanges(): void {
|
|
165
|
+
this.cdr.markForCheck();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The `class` half of the same gap, and it needs a POLL, not a hook: a class at the use site
|
|
169
|
+
// never becomes an input, so there is no SimpleChanges entry to fire on. Making the view refresh
|
|
170
|
+
// is enough - every composed component re-reads the anchor in a getter on its own.
|
|
171
|
+
//
|
|
172
|
+
// ngDoCheck runs during the PARENT's refresh even when this view would be skipped, so the mark
|
|
173
|
+
// lands in the same pass rather than one behind. The dedup gate is load-bearing: marking on every
|
|
174
|
+
// check re-dirties the view every tick and free-runs CD, the hazard `stableAnchorStyle` above
|
|
175
|
+
// prevents one level up.
|
|
176
|
+
ngDoCheck(): void {
|
|
177
|
+
countAngular('styleChecks');
|
|
178
|
+
const current = anchorHostStyle(this.elementRef);
|
|
179
|
+
if (isAnchorStyleUnchanged(this.lastAnchorStyle, current)) return;
|
|
180
|
+
this.lastAnchorStyle = current;
|
|
181
|
+
countAngular('styleMarks');
|
|
182
|
+
this.cdr.markForCheck();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Reference equality is the normal path (the engine replaces props.style only on a real class
|
|
187
|
+
// change); the shallow compare guards against an equal-but-fresh object, which would turn the poll
|
|
188
|
+
// above into a CD loop.
|
|
189
|
+
function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
|
|
190
|
+
if (Object.is(previous, next)) return true;
|
|
191
|
+
return (
|
|
192
|
+
isRecord(previous) && isRecord(next) && shallowStyleEqual(previous, next)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
104
196
|
/**
|
|
105
197
|
* Base for all Angular primitive host components (`symbiote-view`, `symbiote-text`, ...).
|
|
106
198
|
* RN's `StyleProp` can be an object, an array, nested arrays, falsy entries, and Animated
|
|
@@ -114,10 +206,17 @@ const ON_PREFIX = /^on[A-Z]/;
|
|
|
114
206
|
* custom renderer's `setProperty`, so the engine's `routeProp` handles them as usual.
|
|
115
207
|
*/
|
|
116
208
|
@Directive()
|
|
117
|
-
export abstract class SymbiotePrimitiveHost implements OnChanges {
|
|
209
|
+
export abstract class SymbiotePrimitiveHost implements DoCheck, OnChanges {
|
|
118
210
|
private readonly renderer = inject(Renderer2);
|
|
119
211
|
private readonly elementRef = inject(ElementRef);
|
|
120
212
|
|
|
213
|
+
// Counting only. Angular calls this once per view per check, so it is the only honest measure of
|
|
214
|
+
// how much tree a tick walks - and with CheckAlways primitives that is the whole application, not
|
|
215
|
+
// the screen on top. One integer increment next to a template refresh is not measurable.
|
|
216
|
+
ngDoCheck(): void {
|
|
217
|
+
countAngular('viewsChecked');
|
|
218
|
+
}
|
|
219
|
+
|
|
121
220
|
@Input() style?: IStyleProp<unknown>;
|
|
122
221
|
|
|
123
222
|
/**
|
|
@@ -130,8 +229,18 @@ export abstract class SymbiotePrimitiveHost implements OnChanges {
|
|
|
130
229
|
|
|
131
230
|
ngOnChanges(changes: SimpleChanges): void {
|
|
132
231
|
if (!('style' in changes)) return;
|
|
133
|
-
const value =
|
|
134
|
-
|
|
232
|
+
const value =
|
|
233
|
+
this.style === undefined ? undefined : flattenStyle(this.style);
|
|
234
|
+
this.setHostProp('style', value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Push one resolved prop onto the host element. `protected` rather than exposing the injected
|
|
239
|
+
* Renderer2/ElementRef, so a subclass applying its own defaults (TextHost) uses the same single
|
|
240
|
+
* write path this base already uses for `style`.
|
|
241
|
+
*/
|
|
242
|
+
protected setHostProp(key: string, value: unknown): void {
|
|
243
|
+
this.renderer.setProperty(this.elementRef.nativeElement, key, value);
|
|
135
244
|
}
|
|
136
245
|
}
|
|
137
246
|
|
|
@@ -156,15 +265,58 @@ export class SymbioteHostPropsDirective {
|
|
|
156
265
|
// The directive lives in its HOST component's template, so its injected ChangeDetectorRef IS
|
|
157
266
|
// that component's own view detector — the one thing that can refresh it (see wrapCallback).
|
|
158
267
|
private readonly cdr = inject(ChangeDetectorRef);
|
|
268
|
+
// One wrapper per ORIGINAL handler, for the lifetime of this directive. Without it every push
|
|
269
|
+
// of the props bag allocated a brand-new closure for every `onX` key, and the engine stored it
|
|
270
|
+
// as a new listener (`setEventListener` allocates its own closure on top) - per key, per push,
|
|
271
|
+
// on a path a scroll frame reaches 60 times a second. The wrapper body closes over nothing but
|
|
272
|
+
// `value` and this instance's `cdr`, so the handler alone is a complete cache key; `key` is
|
|
273
|
+
// read only by the ON_PREFIX guard above and never enters the wrapper.
|
|
274
|
+
//
|
|
275
|
+
// Reference stability is the point, not just the allocation: a fresh function on every push
|
|
276
|
+
// defeats every downstream identity check on the bag, so nothing upstream could ever conclude
|
|
277
|
+
// "this bag is unchanged" while a callback prop was in it.
|
|
278
|
+
private readonly wrappers = new WeakMap<
|
|
279
|
+
object,
|
|
280
|
+
(...args: unknown[]) => unknown
|
|
281
|
+
>();
|
|
282
|
+
|
|
283
|
+
// Values pushed by the previous bag, so a push writes only what actually moved. Copied rather
|
|
284
|
+
// than aliased: a component is free to hand back the same object with mutated fields.
|
|
285
|
+
private pushed: Record<string, unknown> = {};
|
|
159
286
|
|
|
160
287
|
get node(): unknown {
|
|
161
288
|
return this.elementRef.nativeElement;
|
|
162
289
|
}
|
|
163
290
|
|
|
291
|
+
// MEASURED 2026-08-20, headless 1000-row create: writing every key of every bag cost 104 000
|
|
292
|
+
// engine setProp calls where Solid's identical screen cost 12 000 — and 90 000 of them carried
|
|
293
|
+
// `undefined`. A composed component's bag is a FIXED-SHAPE literal (Pressable's is ~48 keys,
|
|
294
|
+
// most of them unset on any given instance), so the first push of a fresh node spends most of
|
|
295
|
+
// its work deleting keys the node never had. Diffing per key removes exactly that: an unset key
|
|
296
|
+
// is skipped on mount and still written the moment it changes to or from a real value.
|
|
164
297
|
set symbioteHostProps(props: Record<string, unknown>) {
|
|
298
|
+
const pushed = this.pushed;
|
|
299
|
+
// Only keys the node actually carries, so the vanished-key sweep below walks a handful rather
|
|
300
|
+
// than the bag's whole declared shape.
|
|
301
|
+
const next: Record<string, unknown> = {};
|
|
165
302
|
for (const [key, value] of Object.entries(props)) {
|
|
166
|
-
|
|
303
|
+
if (value !== undefined) next[key] = value;
|
|
304
|
+
if (Object.is(pushed[key], value)) continue;
|
|
305
|
+
this.renderer.setProperty(
|
|
306
|
+
this.elementRef.nativeElement,
|
|
307
|
+
key,
|
|
308
|
+
this.wrapCallback(key, value),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
// A bag's key SET is not fixed across pushes — resolveAccessibilityProps folds aria-* aliases
|
|
312
|
+
// into a different shape once any of them holds a value — and a key that vanishes must clear
|
|
313
|
+
// the prop rather than leave the last value on the native view. A key still IN the bag was
|
|
314
|
+
// already handled above, `undefined` included.
|
|
315
|
+
for (const key of Object.keys(pushed)) {
|
|
316
|
+
if (key in props) continue;
|
|
317
|
+
this.renderer.setProperty(this.elementRef.nativeElement, key, undefined);
|
|
167
318
|
}
|
|
319
|
+
this.pushed = next;
|
|
168
320
|
}
|
|
169
321
|
|
|
170
322
|
// A flat-bag `onX` callback (responder negotiation, onLongPress, …) is invoked by the engine's
|
|
@@ -180,11 +332,19 @@ export class SymbioteHostPropsDirective {
|
|
|
180
332
|
// for free (setState / proxy reactivity).
|
|
181
333
|
private wrapCallback(key: string, value: unknown): unknown {
|
|
182
334
|
if (!ON_PREFIX.test(key) || typeof value !== 'function') return value;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
335
|
+
if (key === PER_FRAME_CALLBACK) return value;
|
|
336
|
+
const cached = this.wrappers.get(value);
|
|
337
|
+
if (cached !== undefined) return cached;
|
|
338
|
+
// Reflect.apply rather than calling a cast-to-signature local: `typeof value === 'function'`
|
|
339
|
+
// narrows to `Function`, which has no call signature TypeScript will accept, and this repo
|
|
340
|
+
// does not use `as` to paper over that. `undefined` as the receiver preserves the previous
|
|
341
|
+
// unbound call.
|
|
342
|
+
const wrapper = (...args: unknown[]): unknown => {
|
|
343
|
+
const result: unknown = Reflect.apply(value, undefined, args);
|
|
186
344
|
this.cdr.markForCheck();
|
|
187
345
|
return result;
|
|
188
346
|
};
|
|
347
|
+
this.wrappers.set(value, wrapper);
|
|
348
|
+
return wrapper;
|
|
189
349
|
}
|
|
190
350
|
}
|
package/src/render/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createSurface,
|
|
11
11
|
disposeRoot,
|
|
12
12
|
dlog,
|
|
13
|
+
reportUncaughtError,
|
|
13
14
|
toPublicInstance,
|
|
14
15
|
type ISymbioteNode,
|
|
15
16
|
type IRootTag,
|
|
@@ -62,7 +63,35 @@ function asAngularHost(hostNode: SymbioteSurface | ISymbioteNode): Element {
|
|
|
62
63
|
// to the wrapper as projectable content.
|
|
63
64
|
function createDetachedViewHost(): ISymbioteNode {
|
|
64
65
|
const descriptor = descriptorFor('View');
|
|
65
|
-
return toPublicInstance(
|
|
66
|
+
return toPublicInstance(
|
|
67
|
+
createEngineElement(descriptor.component, descriptor.isText),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Angular's whole unhandled-error funnel: INTERNAL_APPLICATION_ERROR_HANDLER resolves this token
|
|
72
|
+
// and hands it everything the framework caught on the app's behalf — a scheduled (async) tick that
|
|
73
|
+
// threw, a template listener that threw, a rejected pending task. Routed to the engine so an
|
|
74
|
+
// Angular app reaches the same native redbox as a React one instead of a console line nobody sees
|
|
75
|
+
// off a dev machine.
|
|
76
|
+
//
|
|
77
|
+
// There is deliberately no caught-vs-uncaught split here, unlike the React adapter's
|
|
78
|
+
// onUncaughtError/onCaughtError pair. Angular has no error boundary, and every path where the app
|
|
79
|
+
// DID handle something keeps clear of this seam on its own: a `@defer` block with an `@error`
|
|
80
|
+
// branch renders that branch (defer/rendering.ts calls handleUncaughtError only when there is
|
|
81
|
+
// none), `resource()` parks the failure in its own error signal, and an app-level try/catch or
|
|
82
|
+
// `catchError` never reaches Angular at all. So anything arriving here is by construction
|
|
83
|
+
// unclaimed, and the redbox is the right answer for all of it.
|
|
84
|
+
//
|
|
85
|
+
// Two things this must not do. It must not also call `super.handleError` — reportUncaughtError
|
|
86
|
+
// picks exactly one channel (host reporter or console) precisely because RN routes console.error
|
|
87
|
+
// into LogBox too, so the pair would double-report every error. And it must not throw: the
|
|
88
|
+
// scheduler invokes this from inside its own `catch` (zoneless_scheduling_impl.ts's tick), where a
|
|
89
|
+
// second throw escapes to the host and kills the "report and keep running" behaviour the provider
|
|
90
|
+
// exists for.
|
|
91
|
+
class SymbioteErrorHandler extends ErrorHandler {
|
|
92
|
+
override handleError(error: unknown): void {
|
|
93
|
+
reportUncaughtError(error, { origin: 'angular render' });
|
|
94
|
+
}
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
interface IMountedApp {
|
|
@@ -81,7 +110,10 @@ export interface IMountOptions {
|
|
|
81
110
|
wrapperComponent?: Type<unknown>;
|
|
82
111
|
}
|
|
83
112
|
|
|
84
|
-
function applyInputs(
|
|
113
|
+
function applyInputs(
|
|
114
|
+
cmpRef: ComponentRef<unknown>,
|
|
115
|
+
initialProps: object | undefined,
|
|
116
|
+
): void {
|
|
85
117
|
if (initialProps === undefined) return;
|
|
86
118
|
for (const [key, value] of Object.entries(initialProps)) {
|
|
87
119
|
cmpRef.setInput(key, value);
|
|
@@ -118,45 +150,51 @@ export function mount(
|
|
|
118
150
|
provide: RendererFactory2,
|
|
119
151
|
useValue: new SymbioteRendererFactory(surface),
|
|
120
152
|
},
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
153
|
+
// `getElementById` is not decoration: `resource()` resolves TransferState, whose root
|
|
154
|
+
// factory runs `retrieveTransferredState(doc, appId)` ->
|
|
155
|
+
// `doc.getElementById(appId + '-state')` (core/src/transfer_state.ts:156) to pick up
|
|
156
|
+
// server-rendered state. A stub without it makes that a TypeError, and because the throw
|
|
157
|
+
// happens while the component is being constructed, the ENTIRE screen renders nothing —
|
|
158
|
+
// white body under a native header that navigation drew anyway, no error surfaced. The
|
|
159
|
+
// `optional: true` on the injector lookup does not help: the token resolves, its factory
|
|
160
|
+
// is what throws. Returning null is the honest client answer — there is no server here, so
|
|
161
|
+
// `script?.tagName` short-circuits and the caller gets `{}`.
|
|
162
|
+
{
|
|
163
|
+
provide: DOCUMENT,
|
|
164
|
+
useValue: {
|
|
165
|
+
head: surface,
|
|
166
|
+
body: surface,
|
|
167
|
+
getElementById: (): null => null,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
// createEnvironmentInjector with a null parent scopes this injector to {'environment'}
|
|
171
|
+
// only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
|
|
172
|
+
// included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
|
|
173
|
+
// a null parent always dead-ends in NullInjector). platform-browser solves this the same
|
|
174
|
+
// way for real DOM apps via BROWSER_MODULE_PROVIDERS: hand { provide: INJECTOR_SCOPE,
|
|
175
|
+
// useValue: 'root' } to the app-level providers, which R3Injector's constructor reads to
|
|
176
|
+
// self-tag this.scopes with 'root'. Same trick here, no PlatformRef, no DOM.
|
|
131
177
|
{ provide: INJECTOR_SCOPE, useValue: 'root' },
|
|
132
|
-
// Supplies the real ChangeDetectionSchedulerImpl (microtask-batched
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// go through, see SymbioteHostPropsDirective) unconditionally sets RefreshView on
|
|
143
|
-
// EVERY ancestor up to the root; that is fundamental, unavoidable Angular zoneless
|
|
144
|
-
// behavior, true in every Angular app, not something this swap changes. What DOES
|
|
145
|
-
// still protect a sibling branch from an unrelated press is a genuine child
|
|
146
|
-
// `@Component` boundary — SignalView-compiled children are skip-eligible regardless of
|
|
147
|
-
// this scheduler.
|
|
178
|
+
// Supplies the real ChangeDetectionSchedulerImpl (microtask-batched via
|
|
179
|
+
// ApplicationRef.afterTick) + NoopNgZone + ZONELESS_ENABLED: true — the exact bundle
|
|
180
|
+
// internalCreateApplication() uses. Replaces the old unconditional
|
|
181
|
+
// `rootView.detectChanges(); cmpView.detectChanges()` (force-ran both root views on every
|
|
182
|
+
// tick) with Angular's own tick(), which only enters a view something actually marked
|
|
183
|
+
// dirty. This does NOT stop the root's own template from re-running on a plain press or
|
|
184
|
+
// `markForCheck()` anywhere in the tree — `markViewDirty` unconditionally sets RefreshView
|
|
185
|
+
// on every ancestor up to the root; that's fundamental Angular zoneless behavior, not
|
|
186
|
+
// something this swap changes. A genuine child `@Component` boundary still protects a
|
|
187
|
+
// sibling branch from an unrelated press.
|
|
148
188
|
...provideZonelessChangeDetectionInternal(),
|
|
149
|
-
// Angular's
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
// ships) restores the intended behavior: `console.error('ERROR', e)` and keep running.
|
|
159
|
-
{ provide: ErrorHandler, useClass: ErrorHandler },
|
|
189
|
+
// Angular's INTERNAL_APPLICATION_ERROR_HANDLER reports a tick() exception via
|
|
190
|
+
// `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
|
|
191
|
+
// default, but our from-scratch environment injector never did, so the lookup itself threw
|
|
192
|
+
// NG0201 and replaced the real error with "No provider found for ErrorHandler" — any async
|
|
193
|
+
// tick() exception crashed hard, uncaught, instead of being reported. The token still has to
|
|
194
|
+
// be provided for exactly that reason; what it resolves to is now SymbioteErrorHandler
|
|
195
|
+
// (see above), which reports through the engine instead of Angular's `console.error('ERROR',
|
|
196
|
+
// e)` and, like the default, returns rather than rethrows so the app keeps running.
|
|
197
|
+
{ provide: ErrorHandler, useClass: SymbioteErrorHandler },
|
|
160
198
|
ColorSchemeService,
|
|
161
199
|
WindowDimensionsService,
|
|
162
200
|
],
|
package/src/renderer/index.ts
CHANGED
|
@@ -28,6 +28,12 @@ import { descriptorFor } from '@symbiote-native/components';
|
|
|
28
28
|
import type { Renderer2, RendererFactory2, RendererType2 } from '@angular/core';
|
|
29
29
|
import { isAnchorHostComponent } from '../anchor-host-registry';
|
|
30
30
|
import {
|
|
31
|
+
countAngular,
|
|
32
|
+
noteAngularCreate,
|
|
33
|
+
noteAngularWrite,
|
|
34
|
+
} from '../diagnostics';
|
|
35
|
+
import {
|
|
36
|
+
flushScrollViewProjections,
|
|
31
37
|
getScrollViewProjection,
|
|
32
38
|
removeScrollViewProjectedChild,
|
|
33
39
|
} from '../components/scroll-view/projection';
|
|
@@ -68,7 +74,9 @@ function describeHost(node: IHostElement | null | undefined): string {
|
|
|
68
74
|
if (node === null || node === undefined) return 'null';
|
|
69
75
|
if (isSurface(node)) return 'surface';
|
|
70
76
|
const anchorId = anchorDebugIds.get(node);
|
|
71
|
-
return anchorId !== undefined
|
|
77
|
+
return anchorId !== undefined
|
|
78
|
+
? `${node.component}#${anchorId}`
|
|
79
|
+
: node.component;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
const PRIMITIVE_SELECTOR_ALIAS: Record<string, string> = {
|
|
@@ -86,7 +94,9 @@ const PRIMITIVE_SELECTOR_ALIAS: Record<string, string> = {
|
|
|
86
94
|
// but guard anyway for parity with the Vue adapter and to fail loudly on a bad template.
|
|
87
95
|
function assertTextPlacement(child: ISymbioteNode, parent: IHostElement): void {
|
|
88
96
|
if (isRawText(child) && (isSurface(parent) || !parent.isText)) {
|
|
89
|
-
throw new Error(
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Text string "${String(child.props.text)}" must be rendered inside a <Text>`,
|
|
99
|
+
);
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
|
|
@@ -111,10 +121,14 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
111
121
|
// any other string flows through as a raw Fabric name (events/processors derived from its
|
|
112
122
|
// ViewConfig). toPublicInstance grafts the imperative API (measure / setNativeProps /
|
|
113
123
|
// focus) onto the raw node in place, returning the SAME identity the commit mirror keys on.
|
|
124
|
+
countAngular('nodesCreated');
|
|
125
|
+
noteAngularCreate(name);
|
|
114
126
|
const engineName = PRIMITIVE_SELECTOR_ALIAS[name] ?? name;
|
|
115
127
|
if (isAnchorHostComponent(engineName)) {
|
|
116
128
|
const anchor = tagAnchorForDebug(createAnchor());
|
|
117
|
-
dlog(
|
|
129
|
+
dlog(
|
|
130
|
+
`angular createElement ${name} -> anchor host ${describeHost(anchor)}`,
|
|
131
|
+
);
|
|
118
132
|
return anchor;
|
|
119
133
|
}
|
|
120
134
|
|
|
@@ -128,12 +142,14 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
128
142
|
// Angular structural directives (*ngIf / @if / @for) need anchor nodes to track
|
|
129
143
|
// position. A real retained node the commit walk SKIPS — no native view. Twin of the
|
|
130
144
|
// Vue createComment path.
|
|
145
|
+
countAngular('nodesCreated');
|
|
131
146
|
const anchor = tagAnchorForDebug(createAnchor());
|
|
132
147
|
dlog(`Angular renderer createComment -> ${describeHost(anchor)}`);
|
|
133
148
|
return anchor;
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
createText(value: string): IHostNode {
|
|
152
|
+
countAngular('nodesCreated');
|
|
137
153
|
return createRawText(value);
|
|
138
154
|
}
|
|
139
155
|
|
|
@@ -142,9 +158,12 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
142
158
|
// (see parentNode below) — mirrors Angular's own `if (parentRNode !== null)` guard in
|
|
143
159
|
// addLViewToLContainer: skip silently now, the later projection pass places it correctly.
|
|
144
160
|
if (parent === null) return;
|
|
161
|
+
countAngular('nodesInserted');
|
|
145
162
|
assertTextPlacement(newChild, parent);
|
|
146
163
|
if (isSurface(parent)) {
|
|
147
|
-
dlog(
|
|
164
|
+
dlog(
|
|
165
|
+
`Angular renderer appendChild parent=surface child=${describeHost(newChild)}`,
|
|
166
|
+
);
|
|
148
167
|
parent.appendChild(newChild);
|
|
149
168
|
} else {
|
|
150
169
|
const projection = getScrollViewProjection(parent);
|
|
@@ -162,8 +181,13 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
162
181
|
this.surface.requestCommit();
|
|
163
182
|
}
|
|
164
183
|
|
|
165
|
-
insertBefore(
|
|
184
|
+
insertBefore(
|
|
185
|
+
parent: IHostElement | null,
|
|
186
|
+
newChild: IHostNode,
|
|
187
|
+
refChild: IHostNode | null,
|
|
188
|
+
): void {
|
|
166
189
|
if (parent === null) return; // see the appendChild guard above
|
|
190
|
+
countAngular('nodesInserted');
|
|
167
191
|
assertTextPlacement(newChild, parent);
|
|
168
192
|
if (isSurface(parent)) {
|
|
169
193
|
dlog(
|
|
@@ -177,10 +201,15 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
177
201
|
`Angular renderer insertBefore parent=${describeHost(parent)} child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'} projection=${projection !== undefined}`,
|
|
178
202
|
);
|
|
179
203
|
if (projection !== undefined) {
|
|
180
|
-
projection.insertProjectedChild(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
204
|
+
projection.insertProjectedChild(
|
|
205
|
+
parent,
|
|
206
|
+
newChild,
|
|
207
|
+
refChild,
|
|
208
|
+
(target, child, before) => {
|
|
209
|
+
if (before === undefined) appendChild(target, child);
|
|
210
|
+
else insertBefore(target, child, before);
|
|
211
|
+
},
|
|
212
|
+
);
|
|
184
213
|
} else if (refChild) {
|
|
185
214
|
insertBefore(parent, newChild, refChild);
|
|
186
215
|
} else {
|
|
@@ -194,10 +223,13 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
194
223
|
// Detach from the child's own retained parent (a top-level node lives in
|
|
195
224
|
// surface.children with no parent). Angular's `parent` arg is ignored in favor of the
|
|
196
225
|
// authoritative link, mirroring the Vue adapter's remove.
|
|
226
|
+
countAngular('nodesRemoved');
|
|
197
227
|
const angularParent = _parent !== null ? describeHost(_parent) : 'null';
|
|
198
|
-
const retainedParent =
|
|
199
|
-
|
|
200
|
-
|
|
228
|
+
const retainedParent =
|
|
229
|
+
oldChild.parent !== undefined ? describeHost(oldChild.parent) : 'none';
|
|
230
|
+
const wasProjected = removeScrollViewProjectedChild(
|
|
231
|
+
oldChild,
|
|
232
|
+
(parent, child) => removeChild(parent, child),
|
|
201
233
|
);
|
|
202
234
|
dlog(
|
|
203
235
|
`Angular renderer removeChild angularParent=${angularParent} retainedParent=${retainedParent} child=${describeHost(oldChild)} viaProjection=${wasProjected}`,
|
|
@@ -228,7 +260,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
228
260
|
}
|
|
229
261
|
|
|
230
262
|
nextSibling(node: IHostNode): IHostNode | null {
|
|
231
|
-
const siblings =
|
|
263
|
+
const siblings =
|
|
264
|
+
node.parent !== undefined ? node.parent.children : this.surface.children;
|
|
232
265
|
const index = siblings.indexOf(node);
|
|
233
266
|
return index >= 0 ? (siblings[index + 1] ?? null) : null;
|
|
234
267
|
}
|
|
@@ -243,12 +276,16 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
243
276
|
|
|
244
277
|
setAttribute(el: IHostElement, name: string, value: string): void {
|
|
245
278
|
if (isSurface(el)) return;
|
|
279
|
+
countAngular('rendererWrites');
|
|
280
|
+
noteAngularWrite(name);
|
|
246
281
|
routeProp(el, name, value);
|
|
247
282
|
this.surface.requestCommit();
|
|
248
283
|
}
|
|
249
284
|
|
|
250
285
|
removeAttribute(el: IHostElement, name: string): void {
|
|
251
286
|
if (isSurface(el)) return;
|
|
287
|
+
countAngular('rendererWrites');
|
|
288
|
+
noteAngularWrite(name);
|
|
252
289
|
routeProp(el, name, undefined);
|
|
253
290
|
this.surface.requestCommit();
|
|
254
291
|
}
|
|
@@ -264,6 +301,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
264
301
|
|
|
265
302
|
addClass(el: IHostElement, name: string): void {
|
|
266
303
|
if (isSurface(el)) return;
|
|
304
|
+
countAngular('rendererWrites');
|
|
305
|
+
noteAngularWrite('class');
|
|
267
306
|
const tokens = this.classTokens.get(el) ?? new Set<string>();
|
|
268
307
|
tokens.add(name);
|
|
269
308
|
this.classTokens.set(el, tokens);
|
|
@@ -275,6 +314,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
275
314
|
if (isSurface(el)) return;
|
|
276
315
|
const tokens = this.classTokens.get(el);
|
|
277
316
|
if (tokens === undefined) return;
|
|
317
|
+
countAngular('rendererWrites');
|
|
318
|
+
noteAngularWrite('class');
|
|
278
319
|
tokens.delete(name);
|
|
279
320
|
routeProp(el, 'class', tokens.size > 0 ? [...tokens].join(' ') : undefined);
|
|
280
321
|
this.surface.requestCommit();
|
|
@@ -287,6 +328,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
287
328
|
// merge writes, and spreading an array as a record would silently produce numeric-index keys.
|
|
288
329
|
setStyle(el: IHostElement, style: string, value: unknown): void {
|
|
289
330
|
if (isSurface(el)) return;
|
|
331
|
+
countAngular('rendererWrites');
|
|
332
|
+
noteAngularWrite(`style.${style}`);
|
|
290
333
|
const current = getExplicitStyle(el);
|
|
291
334
|
const base = isRecord(current) ? current : {};
|
|
292
335
|
routeProp(el, 'style', { ...base, [style]: value });
|
|
@@ -297,6 +340,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
297
340
|
if (isSurface(el)) return;
|
|
298
341
|
const current = getExplicitStyle(el);
|
|
299
342
|
if (!isRecord(current)) return;
|
|
343
|
+
countAngular('rendererWrites');
|
|
344
|
+
noteAngularWrite(`style.${style}`);
|
|
300
345
|
const { [style]: _removed, ...rest } = current;
|
|
301
346
|
routeProp(el, 'style', rest);
|
|
302
347
|
this.surface.requestCommit();
|
|
@@ -306,11 +351,15 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
306
351
|
// ViewConfig (identical to React/Vue), so the whole flat-bag prop layer is shared.
|
|
307
352
|
setProperty(el: IHostElement, name: string, value: unknown): void {
|
|
308
353
|
if (isSurface(el)) return;
|
|
354
|
+
countAngular('rendererWrites');
|
|
355
|
+
noteAngularWrite(name);
|
|
309
356
|
routeProp(el, name, value);
|
|
310
357
|
this.surface.requestCommit();
|
|
311
358
|
}
|
|
312
359
|
|
|
313
360
|
setValue(node: IHostNode, value: string): void {
|
|
361
|
+
countAngular('rendererWrites');
|
|
362
|
+
noteAngularWrite('#text');
|
|
314
363
|
// A useful permanent seam: text mutations are low-frequency and the one place a stale
|
|
315
364
|
// `{{binding}}` (a change-detection gap) shows up as "the setValue never fired".
|
|
316
365
|
dlog(`Angular renderer setValue "${value}" on ${describeHost(node)}`);
|
|
@@ -341,7 +390,20 @@ export class SymbioteRendererFactory implements RendererFactory2 {
|
|
|
341
390
|
|
|
342
391
|
constructor(private readonly surface: SymbioteSurface) {}
|
|
343
392
|
|
|
344
|
-
createRenderer(
|
|
393
|
+
createRenderer(
|
|
394
|
+
_hostElement: unknown,
|
|
395
|
+
_type: RendererType2 | null,
|
|
396
|
+
): Renderer2 {
|
|
345
397
|
return (this.renderer ??= new SymbioteRenderer(this.surface));
|
|
346
398
|
}
|
|
399
|
+
|
|
400
|
+
// Not commit coalescing (requestCommit owns that) — this is the one moment where a ScrollView's
|
|
401
|
+
// two input channels are known to be consistent: Angular has finished writing every `@Input` AND
|
|
402
|
+
// the renderer has finished every projected insert/remove for this pass, while the surface's
|
|
403
|
+
// commit is still only queued. Sticky projection reconciles are batched to here for both reasons;
|
|
404
|
+
// see flushScrollViewProjections.
|
|
405
|
+
end(): void {
|
|
406
|
+
countAngular('cdPasses');
|
|
407
|
+
flushScrollViewProjections();
|
|
408
|
+
}
|
|
347
409
|
}
|
|
@@ -11,7 +11,9 @@ import { Appearance, type IColorSchemeName } from '@symbiote-native/engine';
|
|
|
11
11
|
*/
|
|
12
12
|
@Injectable({ providedIn: 'root' })
|
|
13
13
|
export class ColorSchemeService {
|
|
14
|
-
readonly colorScheme = signal<IColorSchemeName | null>(
|
|
14
|
+
readonly colorScheme = signal<IColorSchemeName | null>(
|
|
15
|
+
Appearance.getColorScheme(),
|
|
16
|
+
);
|
|
15
17
|
|
|
16
18
|
constructor() {
|
|
17
19
|
const destroyRef = inject(DestroyRef);
|