@symbiote-native/angular 0.7.0 → 1.0.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/babel-lower-host-primitives.cjs +636 -0
- package/babel-register-composed.cjs +21 -3
- package/build/angular/anchor-host-registry.js +3 -1
- 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.js +7 -2
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.js +3 -1
- 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 +1 -1
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.d.ts +3 -0
- package/build/angular/components/button.js +22 -5
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +1 -1
- package/build/angular/components/flat-list/index.js +47 -14
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.js +4 -10
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.js +4 -10
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -4
- package/build/angular/components/image/shared.js +51 -33
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +3 -0
- package/build/angular/components/image-background.js +32 -9
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +2 -1
- package/build/angular/components/input-accessory-view/index.js +18 -16
- 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 +56 -19
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +1 -0
- package/build/angular/components/modal/index.js +21 -10
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +10 -5
- package/build/angular/components/pressable/index.js +61 -16
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +2 -1
- package/build/angular/components/refresh-control/index.js +13 -9
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +3 -1
- package/build/angular/components/safe-area-view/index.js +22 -20
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.js +44 -14
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.js +13 -5
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +212 -47
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +2 -0
- package/build/angular/components/scroll-view/shared.js +48 -11
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +7 -3
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +9 -1
- package/build/angular/components/section-list/index.js +72 -18
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.js +23 -9
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.js +23 -9
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +1 -1
- package/build/angular/components/switch/shared.js +5 -2
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +8 -2
- package/build/angular/components/text-input.js +79 -34
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +36 -7
- package/build/angular/components/touchable/index.js +280 -72
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
- package/build/angular/components/touchable-native-feedback/index.js +19 -17
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +4 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +8 -2
- package/build/angular/components/virtualized-list/index.js +207 -61
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +21 -1
- package/build/angular/components/virtualized-section-list/index.js +100 -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 +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.d.ts +11 -2
- package/build/angular/create-portal/index.js +42 -12
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +3 -1
- 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/gate-demand.d.ts +37 -0
- package/build/angular/gate-demand.js +84 -0
- package/build/angular/gate-demand.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 +6 -2
- package/build/angular/index.js +16 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
- package/build/angular/modules/animated/create-animated-component.js +41 -18
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/app-registry/index.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +1 -1
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +31 -2
- package/build/angular/primitives/index.js +81 -3
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +9 -1
- package/build/angular/primitives/shared.js +49 -3
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/register.d.ts +1 -0
- package/build/angular/register.js +35 -0
- package/build/angular/register.js.map +1 -0
- package/build/angular/render/index.js +51 -6
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +157 -21
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +1 -1
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/build/angular/state-style.d.ts +9 -0
- package/build/angular/state-style.js +39 -0
- package/build/angular/state-style.js.map +1 -0
- package/metro-transformer.cjs +69 -0
- package/package.json +27 -8
- package/src/anchor-host-registry.ts +3 -1
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +7 -2
- package/src/components/activity-indicator/index.ios.ts +3 -1
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +36 -6
- package/src/components/flat-list/index.ts +91 -29
- package/src/components/image/index.android.ts +9 -6
- package/src/components/image/index.ios.ts +9 -6
- package/src/components/image/shared.ts +110 -45
- package/src/components/image-background.ts +47 -10
- package/src/components/input-accessory-view/index.ts +41 -18
- package/src/components/keyboard-avoiding-view/index.ts +76 -22
- package/src/components/modal/index.ts +39 -12
- package/src/components/pressable/index.ts +121 -31
- package/src/components/refresh-control/index.ts +36 -13
- package/src/components/safe-area-view/index.ts +44 -19
- package/src/components/scroll-view/index.android.ts +31 -9
- package/src/components/scroll-view/index.ios.ts +17 -4
- package/src/components/scroll-view/projection.ts +290 -63
- package/src/components/scroll-view/shared.ts +124 -30
- package/src/components/scroll-view/sticky-header.ts +25 -8
- package/src/components/section-list/index.ts +79 -19
- package/src/components/switch/index.android.ts +18 -7
- package/src/components/switch/index.ios.ts +18 -7
- package/src/components/switch/shared.ts +15 -4
- package/src/components/text-input.ts +119 -31
- package/src/components/touchable/index.ts +339 -72
- package/src/components/touchable-native-feedback/index.ts +50 -18
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +292 -72
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +164 -34
- package/src/components.ts +6 -1
- package/src/create-portal/index.ts +47 -11
- package/src/create-tunnel/index.ts +3 -1
- package/src/descriptor-to-angular/index.ts +29 -8
- package/src/diagnostics.ts +138 -0
- package/src/gate-demand.ts +109 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +33 -2
- package/src/modules/animated/create-animated-component.ts +67 -19
- package/src/modules/app-registry/index.ts +3 -1
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +73 -2
- package/src/primitives/shared.ts +70 -9
- package/src/register.ts +43 -0
- package/src/render/index.ts +59 -7
- package/src/renderer/index.ts +209 -37
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
- package/src/state-style.ts +39 -0
package/src/primitives/index.ts
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
// component imports them and renders them in its template. Declaring `style` as a real
|
|
4
4
|
// Angular input prevents Angular's CSS style engine from decomposing RN `StyleProp` arrays.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Component,
|
|
8
|
+
Input,
|
|
9
|
+
type OnInit,
|
|
10
|
+
type SimpleChanges,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import {
|
|
13
|
+
resolveTextProps,
|
|
14
|
+
type IEllipsizeMode,
|
|
15
|
+
} from '@symbiote-native/components';
|
|
7
16
|
import { SymbiotePrimitiveHost } from './shared';
|
|
8
17
|
|
|
9
18
|
export {
|
|
@@ -21,12 +30,47 @@ export {
|
|
|
21
30
|
})
|
|
22
31
|
export class ViewHost extends SymbiotePrimitiveHost {}
|
|
23
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Text carries the two defaults RN's Text.js applies unconditionally (`ellipsizeMode ?? 'tail'`,
|
|
35
|
+
* `allowFontScaling !== false`, Text.js:289 and :291). Without them native falls back to `clip` and a
|
|
36
|
+
* clamped Text cuts mid-word with no ellipsis — device-observed on the other adapters 2026-08-19.
|
|
37
|
+
*
|
|
38
|
+
* They are declared as real `@Input()`s, unlike the pass-through props the base's comment
|
|
39
|
+
* describes, for one reason: the default can only be applied by code that can SEE whether the
|
|
40
|
+
* caller supplied a value. A host-binding pass-through is invisible here, so blindly writing
|
|
41
|
+
* 'tail' would silently overwrite an explicit `ellipsizeMode="clip"`.
|
|
42
|
+
*/
|
|
24
43
|
@Component({
|
|
25
44
|
selector: 'symbiote-text, Text',
|
|
26
45
|
standalone: true,
|
|
27
46
|
template: '<ng-content></ng-content>',
|
|
28
47
|
})
|
|
29
|
-
export class TextHost extends SymbiotePrimitiveHost {
|
|
48
|
+
export class TextHost extends SymbiotePrimitiveHost implements OnInit {
|
|
49
|
+
@Input() ellipsizeMode?: IEllipsizeMode;
|
|
50
|
+
@Input() allowFontScaling?: boolean;
|
|
51
|
+
|
|
52
|
+
// ngOnChanges fires before ngOnInit when a binding exists, and not at all when none does —
|
|
53
|
+
// so both hooks are needed to guarantee the defaults land exactly once per settled value.
|
|
54
|
+
ngOnInit(): void {
|
|
55
|
+
this.applyTextDefaults();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override ngOnChanges(changes: SimpleChanges): void {
|
|
59
|
+
super.ngOnChanges(changes);
|
|
60
|
+
if ('ellipsizeMode' in changes || 'allowFontScaling' in changes) {
|
|
61
|
+
this.applyTextDefaults();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private applyTextDefaults(): void {
|
|
66
|
+
const resolved = resolveTextProps({
|
|
67
|
+
ellipsizeMode: this.ellipsizeMode,
|
|
68
|
+
allowFontScaling: this.allowFontScaling,
|
|
69
|
+
});
|
|
70
|
+
this.setHostProp('ellipsizeMode', resolved.ellipsizeMode);
|
|
71
|
+
this.setHostProp('allowFontScaling', resolved.allowFontScaling);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
30
74
|
|
|
31
75
|
@Component({
|
|
32
76
|
selector: 'symbiote-image',
|
|
@@ -63,6 +107,9 @@ export class HorizontalScrollView extends SymbiotePrimitiveHost {}
|
|
|
63
107
|
})
|
|
64
108
|
export class HorizontalScrollContentView extends SymbiotePrimitiveHost {}
|
|
65
109
|
|
|
110
|
+
// The pair the LOWERED path commits — declared so the tag alphabet stays complete, though no
|
|
111
|
+
// Angular template renders them: this adapter has no lowering transform. The `-managed` pair below
|
|
112
|
+
// is what its own TextInput renders.
|
|
66
113
|
@Component({
|
|
67
114
|
selector: 'symbiote-text-input',
|
|
68
115
|
standalone: true,
|
|
@@ -77,6 +124,20 @@ export class TextInputHost extends SymbiotePrimitiveHost {}
|
|
|
77
124
|
})
|
|
78
125
|
export class MultilineTextInputHost extends SymbiotePrimitiveHost {}
|
|
79
126
|
|
|
127
|
+
@Component({
|
|
128
|
+
selector: 'symbiote-text-input-managed',
|
|
129
|
+
standalone: true,
|
|
130
|
+
template: '<ng-content></ng-content>',
|
|
131
|
+
})
|
|
132
|
+
export class ManagedTextInputHost extends SymbiotePrimitiveHost {}
|
|
133
|
+
|
|
134
|
+
@Component({
|
|
135
|
+
selector: 'symbiote-text-input-multiline-managed',
|
|
136
|
+
standalone: true,
|
|
137
|
+
template: '<ng-content></ng-content>',
|
|
138
|
+
})
|
|
139
|
+
export class ManagedMultilineTextInputHost extends SymbiotePrimitiveHost {}
|
|
140
|
+
|
|
80
141
|
@Component({
|
|
81
142
|
selector: 'symbiote-switch',
|
|
82
143
|
standalone: true,
|
|
@@ -84,6 +145,16 @@ export class MultilineTextInputHost extends SymbiotePrimitiveHost {}
|
|
|
84
145
|
})
|
|
85
146
|
export class SwitchHost extends SymbiotePrimitiveHost {}
|
|
86
147
|
|
|
148
|
+
// The component path's spelling — same native Switch/AndroidSwitch, a tag the engine's Switch
|
|
149
|
+
// behavior does not carry. See `component-names/shared.ts` for why the wrapper may not share the
|
|
150
|
+
// lowered tag; mirrors `ManagedTextInputHost` above.
|
|
151
|
+
@Component({
|
|
152
|
+
selector: 'symbiote-switch-managed',
|
|
153
|
+
standalone: true,
|
|
154
|
+
template: '<ng-content></ng-content>',
|
|
155
|
+
})
|
|
156
|
+
export class ManagedSwitchHost extends SymbiotePrimitiveHost {}
|
|
157
|
+
|
|
87
158
|
@Component({
|
|
88
159
|
selector: 'symbiote-activity-indicator',
|
|
89
160
|
standalone: true,
|
package/src/primitives/shared.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Renderer2,
|
|
10
10
|
type SimpleChanges,
|
|
11
11
|
} from '@angular/core';
|
|
12
|
+
import { countAngular } from '../diagnostics';
|
|
12
13
|
import {
|
|
13
14
|
flattenStyle,
|
|
14
15
|
isSymbioteNode,
|
|
@@ -61,7 +62,9 @@ function isStyleValue<T>(value: unknown): value is IStyleProp<T> {
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
export function anchorStyleProp<T>(
|
|
65
|
+
export function anchorStyleProp<T>(
|
|
66
|
+
elementRef: ElementRef<unknown>,
|
|
67
|
+
): IStyleProp<T> | undefined {
|
|
65
68
|
const value = anchorHostStyle(elementRef);
|
|
66
69
|
return isStyleValue<T>(value) ? value : undefined;
|
|
67
70
|
}
|
|
@@ -70,7 +73,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
70
73
|
return typeof value === 'object' && value !== null;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function shallowStyleEqual(
|
|
76
|
+
function shallowStyleEqual(
|
|
77
|
+
a: Record<string, unknown>,
|
|
78
|
+
b: Record<string, unknown>,
|
|
79
|
+
): boolean {
|
|
74
80
|
const aKeys = Object.keys(a);
|
|
75
81
|
if (aKeys.length !== Object.keys(b).length) return false;
|
|
76
82
|
return aKeys.every(key => a[key] === b[key]);
|
|
@@ -97,7 +103,9 @@ export function stableAnchorStyle(
|
|
|
97
103
|
previous: unknown,
|
|
98
104
|
): Record<string, unknown> {
|
|
99
105
|
const next = flattenStyle([anchorHostStyle(elementRef), explicitStyle]);
|
|
100
|
-
return isRecord(previous) && shallowStyleEqual(previous, next)
|
|
106
|
+
return isRecord(previous) && shallowStyleEqual(previous, next)
|
|
107
|
+
? previous
|
|
108
|
+
: next;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
const ON_PREFIX = /^on[A-Z]/;
|
|
@@ -166,9 +174,11 @@ export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
|
|
|
166
174
|
// check re-dirties the view every tick and free-runs CD, the hazard `stableAnchorStyle` above
|
|
167
175
|
// prevents one level up.
|
|
168
176
|
ngDoCheck(): void {
|
|
177
|
+
countAngular('styleChecks');
|
|
169
178
|
const current = anchorHostStyle(this.elementRef);
|
|
170
179
|
if (isAnchorStyleUnchanged(this.lastAnchorStyle, current)) return;
|
|
171
180
|
this.lastAnchorStyle = current;
|
|
181
|
+
countAngular('styleMarks');
|
|
172
182
|
this.cdr.markForCheck();
|
|
173
183
|
}
|
|
174
184
|
}
|
|
@@ -178,7 +188,9 @@ export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
|
|
|
178
188
|
// above into a CD loop.
|
|
179
189
|
function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
|
|
180
190
|
if (Object.is(previous, next)) return true;
|
|
181
|
-
return
|
|
191
|
+
return (
|
|
192
|
+
isRecord(previous) && isRecord(next) && shallowStyleEqual(previous, next)
|
|
193
|
+
);
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
/**
|
|
@@ -194,10 +206,17 @@ function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
|
|
|
194
206
|
* custom renderer's `setProperty`, so the engine's `routeProp` handles them as usual.
|
|
195
207
|
*/
|
|
196
208
|
@Directive()
|
|
197
|
-
export abstract class SymbiotePrimitiveHost implements OnChanges {
|
|
209
|
+
export abstract class SymbiotePrimitiveHost implements DoCheck, OnChanges {
|
|
198
210
|
private readonly renderer = inject(Renderer2);
|
|
199
211
|
private readonly elementRef = inject(ElementRef);
|
|
200
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
|
+
|
|
201
220
|
@Input() style?: IStyleProp<unknown>;
|
|
202
221
|
|
|
203
222
|
/**
|
|
@@ -210,8 +229,18 @@ export abstract class SymbiotePrimitiveHost implements OnChanges {
|
|
|
210
229
|
|
|
211
230
|
ngOnChanges(changes: SimpleChanges): void {
|
|
212
231
|
if (!('style' in changes)) return;
|
|
213
|
-
const value =
|
|
214
|
-
|
|
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);
|
|
215
244
|
}
|
|
216
245
|
}
|
|
217
246
|
|
|
@@ -246,16 +275,48 @@ export class SymbioteHostPropsDirective {
|
|
|
246
275
|
// Reference stability is the point, not just the allocation: a fresh function on every push
|
|
247
276
|
// defeats every downstream identity check on the bag, so nothing upstream could ever conclude
|
|
248
277
|
// "this bag is unchanged" while a callback prop was in it.
|
|
249
|
-
private readonly wrappers = new WeakMap<
|
|
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> = {};
|
|
250
286
|
|
|
251
287
|
get node(): unknown {
|
|
252
288
|
return this.elementRef.nativeElement;
|
|
253
289
|
}
|
|
254
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.
|
|
255
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> = {};
|
|
256
302
|
for (const [key, value] of Object.entries(props)) {
|
|
257
|
-
|
|
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);
|
|
258
318
|
}
|
|
319
|
+
this.pushed = next;
|
|
259
320
|
}
|
|
260
321
|
|
|
261
322
|
// A flat-bag `onX` callback (responder negotiation, onLongPress, …) is invoked by the engine's
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Side-effect ONLY. Exports nothing, and must never be re-exported from the barrel.
|
|
2
|
+
//
|
|
3
|
+
// A lowered `<symbiote-pressable>`/`<symbiote-text-input>` is a bare custom element with no
|
|
4
|
+
// component behind it, so the press machine and the controlled-input machine have to live on the
|
|
5
|
+
// engine node itself — reached by `registerPressableBehavior()`/`registerTextInputBehavior()`
|
|
6
|
+
// having run before the first node of that tag is created.
|
|
7
|
+
//
|
|
8
|
+
// Metro turns on `inlineRequires` for PRODUCTION only, moving a `require()` down to the first place
|
|
9
|
+
// its binding is used as a VALUE; a barrel's `export { X } from './x'` compiles to a lazy getter,
|
|
10
|
+
// so a module reached only that way never evaluates unless something names it as a value — silent
|
|
11
|
+
// in Release builds alone, invisible to tsc, to vitest, and to grepping the bundle (see CLAUDE.md,
|
|
12
|
+
// "Never make correctness depend on a module's load-time side effect"). A bare
|
|
13
|
+
// `import './register';` placed NEXT TO a re-export of the same specifier does not help either —
|
|
14
|
+
// Babel merges the two into one dependency and the merged one stays lazy. This is the shape that
|
|
15
|
+
// survives both traps, matching `adapters/{vue,svelte,solid}/src/register.ts` and
|
|
16
|
+
// `packages/slider/src/{react,vue,svelte,angular}/index.ts`.
|
|
17
|
+
import {
|
|
18
|
+
registerImageBehavior,
|
|
19
|
+
registerInputAccessoryViewBehavior,
|
|
20
|
+
registerPressableBehavior,
|
|
21
|
+
registerSwitchBehavior,
|
|
22
|
+
registerTextInputBehavior,
|
|
23
|
+
} from '@symbiote-native/components';
|
|
24
|
+
|
|
25
|
+
registerPressableBehavior();
|
|
26
|
+
// Only the LOWERED tags carry this — the wrapper renders `symbiote-text-input-managed` /
|
|
27
|
+
// `symbiote-text-input-multiline-managed` and keeps running its own lifecycle. One owner per node;
|
|
28
|
+
// see `component-names/shared.ts`.
|
|
29
|
+
registerTextInputBehavior();
|
|
30
|
+
// Same reason as TextInput: the wrapper renders `symbiote-switch-managed` and runs its own
|
|
31
|
+
// lastNativeReport/snap-back lifecycle, so the engine's copy attaches only to the bare tag.
|
|
32
|
+
registerSwitchBehavior();
|
|
33
|
+
|
|
34
|
+
// Image owns no runtime — its behavior is a prop FOLD and nothing else, and it is registered on the
|
|
35
|
+
// same `symbiote-image` the wrapper already emits rather than on a `-managed` twin. That is safe
|
|
36
|
+
// only because the mapping is idempotent, which `core/components/src/behaviors/image.test.ts`
|
|
37
|
+
// asserts rather than assumes; a wrapper-built node simply folds a second time and nothing moves.
|
|
38
|
+
registerImageBehavior();
|
|
39
|
+
|
|
40
|
+
// Fold-only, and it shares the wrapper's tag for the same reason Image does: the mapping has no
|
|
41
|
+
// aliasing at all, so a wrapper-built node folding a second time moves nothing.
|
|
42
|
+
// `core/components/src/behaviors/input-accessory-view.test.ts` asserts that rather than assuming it.
|
|
43
|
+
registerInputAccessoryViewBehavior();
|
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,
|
|
@@ -61,8 +62,38 @@ function asAngularHost(hostNode: SymbioteSurface | ISymbioteNode): Element {
|
|
|
61
62
|
// that isn't one of our primitives), so the root needs an explicit host too, one we then hand
|
|
62
63
|
// to the wrapper as projectable content.
|
|
63
64
|
function createDetachedViewHost(): ISymbioteNode {
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// 'symbiote-view', not 'View': the public name is not a Fabric view name, and until
|
|
66
|
+
// makeDescriptorFor learned to reject one it fell through to a view literally named `View`.
|
|
67
|
+
const descriptor = descriptorFor('symbiote-view');
|
|
68
|
+
return toPublicInstance(
|
|
69
|
+
createEngineElement(descriptor.component, descriptor.isText),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Angular's whole unhandled-error funnel: INTERNAL_APPLICATION_ERROR_HANDLER resolves this token
|
|
74
|
+
// and hands it everything the framework caught on the app's behalf — a scheduled (async) tick that
|
|
75
|
+
// threw, a template listener that threw, a rejected pending task. Routed to the engine so an
|
|
76
|
+
// Angular app reaches the same native redbox as a React one instead of a console line nobody sees
|
|
77
|
+
// off a dev machine.
|
|
78
|
+
//
|
|
79
|
+
// There is deliberately no caught-vs-uncaught split here, unlike the React adapter's
|
|
80
|
+
// onUncaughtError/onCaughtError pair. Angular has no error boundary, and every path where the app
|
|
81
|
+
// DID handle something keeps clear of this seam on its own: a `@defer` block with an `@error`
|
|
82
|
+
// branch renders that branch (defer/rendering.ts calls handleUncaughtError only when there is
|
|
83
|
+
// none), `resource()` parks the failure in its own error signal, and an app-level try/catch or
|
|
84
|
+
// `catchError` never reaches Angular at all. So anything arriving here is by construction
|
|
85
|
+
// unclaimed, and the redbox is the right answer for all of it.
|
|
86
|
+
//
|
|
87
|
+
// Two things this must not do. It must not also call `super.handleError` — reportUncaughtError
|
|
88
|
+
// picks exactly one channel (host reporter or console) precisely because RN routes console.error
|
|
89
|
+
// into LogBox too, so the pair would double-report every error. And it must not throw: the
|
|
90
|
+
// scheduler invokes this from inside its own `catch` (zoneless_scheduling_impl.ts's tick), where a
|
|
91
|
+
// second throw escapes to the host and kills the "report and keep running" behaviour the provider
|
|
92
|
+
// exists for.
|
|
93
|
+
class SymbioteErrorHandler extends ErrorHandler {
|
|
94
|
+
override handleError(error: unknown): void {
|
|
95
|
+
reportUncaughtError(error, { origin: 'angular render' });
|
|
96
|
+
}
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
interface IMountedApp {
|
|
@@ -81,7 +112,10 @@ export interface IMountOptions {
|
|
|
81
112
|
wrapperComponent?: Type<unknown>;
|
|
82
113
|
}
|
|
83
114
|
|
|
84
|
-
function applyInputs(
|
|
115
|
+
function applyInputs(
|
|
116
|
+
cmpRef: ComponentRef<unknown>,
|
|
117
|
+
initialProps: object | undefined,
|
|
118
|
+
): void {
|
|
85
119
|
if (initialProps === undefined) return;
|
|
86
120
|
for (const [key, value] of Object.entries(initialProps)) {
|
|
87
121
|
cmpRef.setInput(key, value);
|
|
@@ -118,7 +152,23 @@ export function mount(
|
|
|
118
152
|
provide: RendererFactory2,
|
|
119
153
|
useValue: new SymbioteRendererFactory(surface),
|
|
120
154
|
},
|
|
121
|
-
|
|
155
|
+
// `getElementById` is not decoration: `resource()` resolves TransferState, whose root
|
|
156
|
+
// factory runs `retrieveTransferredState(doc, appId)` ->
|
|
157
|
+
// `doc.getElementById(appId + '-state')` (core/src/transfer_state.ts:156) to pick up
|
|
158
|
+
// server-rendered state. A stub without it makes that a TypeError, and because the throw
|
|
159
|
+
// happens while the component is being constructed, the ENTIRE screen renders nothing —
|
|
160
|
+
// white body under a native header that navigation drew anyway, no error surfaced. The
|
|
161
|
+
// `optional: true` on the injector lookup does not help: the token resolves, its factory
|
|
162
|
+
// is what throws. Returning null is the honest client answer — there is no server here, so
|
|
163
|
+
// `script?.tagName` short-circuits and the caller gets `{}`.
|
|
164
|
+
{
|
|
165
|
+
provide: DOCUMENT,
|
|
166
|
+
useValue: {
|
|
167
|
+
head: surface,
|
|
168
|
+
body: surface,
|
|
169
|
+
getElementById: (): null => null,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
122
172
|
// createEnvironmentInjector with a null parent scopes this injector to {'environment'}
|
|
123
173
|
// only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
|
|
124
174
|
// included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
|
|
@@ -142,9 +192,11 @@ export function mount(
|
|
|
142
192
|
// `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
|
|
143
193
|
// default, but our from-scratch environment injector never did, so the lookup itself threw
|
|
144
194
|
// NG0201 and replaced the real error with "No provider found for ErrorHandler" — any async
|
|
145
|
-
// tick() exception crashed hard, uncaught, instead of being reported.
|
|
146
|
-
//
|
|
147
|
-
|
|
195
|
+
// tick() exception crashed hard, uncaught, instead of being reported. The token still has to
|
|
196
|
+
// be provided for exactly that reason; what it resolves to is now SymbioteErrorHandler
|
|
197
|
+
// (see above), which reports through the engine instead of Angular's `console.error('ERROR',
|
|
198
|
+
// e)` and, like the default, returns rather than rethrows so the app keeps running.
|
|
199
|
+
{ provide: ErrorHandler, useClass: SymbioteErrorHandler },
|
|
148
200
|
ColorSchemeService,
|
|
149
201
|
WindowDimensionsService,
|
|
150
202
|
],
|