@symbiote-native/angular 3.1.3 → 3.3.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.
Files changed (165) hide show
  1. package/babel-register-composed.cjs +1 -0
  2. package/build/angular/accessibility-events.d.ts +12 -0
  3. package/build/angular/accessibility-events.js +28 -0
  4. package/build/angular/accessibility-events.js.map +1 -0
  5. package/build/angular/accessibility-inputs.d.ts +40 -0
  6. package/build/angular/accessibility-inputs.js +130 -0
  7. package/build/angular/accessibility-inputs.js.map +1 -0
  8. package/build/angular/bootstrap.d.ts +3 -1
  9. package/build/angular/bootstrap.js +4 -1
  10. package/build/angular/bootstrap.js.map +1 -1
  11. package/build/angular/components/checkbox-props.d.ts +2 -0
  12. package/build/angular/components/checkbox-props.js +2 -0
  13. package/build/angular/components/checkbox-props.js.map +1 -0
  14. package/build/angular/components/flat-list/flat-list-template.d.ts +1 -0
  15. package/build/angular/components/flat-list/flat-list-template.js +158 -0
  16. package/build/angular/components/flat-list/flat-list-template.js.map +1 -0
  17. package/build/angular/components/flat-list/index.d.ts +6 -61
  18. package/build/angular/components/flat-list/index.js +26 -463
  19. package/build/angular/components/flat-list/index.js.map +1 -1
  20. package/build/angular/components/keyboard-avoiding-view/index.d.ts +4 -40
  21. package/build/angular/components/keyboard-avoiding-view/index.js +7 -131
  22. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  23. package/build/angular/components/modal/index.d.ts +5 -41
  24. package/build/angular/components/modal/index.js +8 -133
  25. package/build/angular/components/modal/index.js.map +1 -1
  26. package/build/angular/components/section-list/index.d.ts +4 -51
  27. package/build/angular/components/section-list/index.js +18 -335
  28. package/build/angular/components/section-list/index.js.map +1 -1
  29. package/build/angular/components/virtualized-list/cell-registry.d.ts +35 -0
  30. package/build/angular/components/virtualized-list/cell-registry.js +133 -0
  31. package/build/angular/components/virtualized-list/cell-registry.js.map +1 -0
  32. package/build/angular/components/virtualized-list/index.d.ts +18 -253
  33. package/build/angular/components/virtualized-list/index.js +112 -1178
  34. package/build/angular/components/virtualized-list/index.js.map +1 -1
  35. package/build/angular/components/virtualized-list/list-a11y-forward.d.ts +1 -0
  36. package/build/angular/components/virtualized-list/list-a11y-forward.js +42 -0
  37. package/build/angular/components/virtualized-list/list-a11y-forward.js.map +1 -0
  38. package/build/angular/components/virtualized-list/list-bags.d.ts +32 -0
  39. package/build/angular/components/virtualized-list/list-bags.js +120 -0
  40. package/build/angular/components/virtualized-list/list-bags.js.map +1 -0
  41. package/build/angular/components/virtualized-list/list-effects.d.ts +31 -0
  42. package/build/angular/components/virtualized-list/list-effects.js +83 -0
  43. package/build/angular/components/virtualized-list/list-effects.js.map +1 -0
  44. package/build/angular/components/virtualized-list/list-events.d.ts +17 -0
  45. package/build/angular/components/virtualized-list/list-events.js +34 -0
  46. package/build/angular/components/virtualized-list/list-events.js.map +1 -0
  47. package/build/angular/components/virtualized-list/list-handle.d.ts +35 -0
  48. package/build/angular/components/virtualized-list/list-handle.js +61 -0
  49. package/build/angular/components/virtualized-list/list-handle.js.map +1 -0
  50. package/build/angular/components/virtualized-list/list-inputs.d.ts +78 -0
  51. package/build/angular/components/virtualized-list/list-inputs.js +150 -0
  52. package/build/angular/components/virtualized-list/list-inputs.js.map +1 -0
  53. package/build/angular/components/virtualized-list/list-props.d.ts +58 -0
  54. package/build/angular/components/virtualized-list/list-props.js +3 -0
  55. package/build/angular/components/virtualized-list/list-props.js.map +1 -0
  56. package/build/angular/components/virtualized-list/list-styles.d.ts +23 -0
  57. package/build/angular/components/virtualized-list/list-styles.js +26 -0
  58. package/build/angular/components/virtualized-list/list-styles.js.map +1 -0
  59. package/build/angular/components/virtualized-list/list-template.d.ts +1 -0
  60. package/build/angular/components/virtualized-list/list-template.js +176 -0
  61. package/build/angular/components/virtualized-list/list-template.js.map +1 -0
  62. package/build/angular/components/virtualized-section-list/index.d.ts +13 -70
  63. package/build/angular/components/virtualized-section-list/index.js +22 -455
  64. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  65. package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -0
  66. package/build/angular/components/virtualized-section-list/section-list-template.js +107 -0
  67. package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -0
  68. package/build/angular/components.d.ts +1 -0
  69. package/build/angular/components.js.map +1 -1
  70. package/build/angular/descriptor-to-angular/descriptor-host.d.ts +8 -0
  71. package/build/angular/descriptor-to-angular/descriptor-host.js +38 -0
  72. package/build/angular/descriptor-to-angular/descriptor-host.js.map +1 -0
  73. package/build/angular/descriptor-to-angular/index.d.ts +1 -0
  74. package/build/angular/descriptor-to-angular/index.js +4 -0
  75. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  76. package/build/angular/element-base.d.ts +153 -0
  77. package/build/angular/element-base.js +272 -0
  78. package/build/angular/element-base.js.map +1 -0
  79. package/build/angular/elements-controls.d.ts +165 -0
  80. package/build/angular/elements-controls.js +382 -0
  81. package/build/angular/elements-controls.js.map +1 -0
  82. package/build/angular/elements-layout.d.ts +122 -0
  83. package/build/angular/elements-layout.js +242 -0
  84. package/build/angular/elements-layout.js.map +1 -0
  85. package/build/angular/elements-press.d.ts +96 -0
  86. package/build/angular/elements-press.js +227 -0
  87. package/build/angular/elements-press.js.map +1 -0
  88. package/build/angular/elements.d.ts +6 -516
  89. package/build/angular/elements.js +8 -1091
  90. package/build/angular/elements.js.map +1 -1
  91. package/build/angular/finalization-registry.d.ts +4 -0
  92. package/build/angular/finalization-registry.js +17 -0
  93. package/build/angular/finalization-registry.js.map +1 -0
  94. package/build/angular/host-instance/index.d.ts +2 -0
  95. package/build/angular/host-instance/index.js +4 -4
  96. package/build/angular/host-instance/index.js.map +1 -1
  97. package/build/angular/index.d.ts +9 -4
  98. package/build/angular/index.js +8 -3
  99. package/build/angular/index.js.map +1 -1
  100. package/build/angular/native-view-base.d.ts +27 -0
  101. package/build/angular/native-view-base.js +69 -0
  102. package/build/angular/native-view-base.js.map +1 -0
  103. package/build/angular/render/index.d.ts +2 -1
  104. package/build/angular/render/index.js +46 -73
  105. package/build/angular/render/index.js.map +1 -1
  106. package/build/angular/services/connect-watched-signal.d.ts +2 -0
  107. package/build/angular/services/connect-watched-signal.js +9 -0
  108. package/build/angular/services/connect-watched-signal.js.map +1 -0
  109. package/build/angular/services/create-event-value-hook.d.ts +3 -0
  110. package/build/angular/services/create-event-value-hook.js +17 -0
  111. package/build/angular/services/create-event-value-hook.js.map +1 -0
  112. package/build/angular/services/create-resource-hook.d.ts +7 -0
  113. package/build/angular/services/create-resource-hook.js +20 -0
  114. package/build/angular/services/create-resource-hook.js.map +1 -0
  115. package/build/angular/services/index.d.ts +5 -0
  116. package/build/angular/services/index.js +5 -0
  117. package/build/angular/services/index.js.map +1 -1
  118. package/build/angular/services/inject-event.d.ts +6 -0
  119. package/build/angular/services/inject-event.js +24 -0
  120. package/build/angular/services/inject-event.js.map +1 -0
  121. package/build/angular/services/permissions-service-base.d.ts +14 -0
  122. package/build/angular/services/permissions-service-base.js +38 -0
  123. package/build/angular/services/permissions-service-base.js.map +1 -0
  124. package/package.json +6 -6
  125. package/src/accessibility-events.ts +13 -0
  126. package/src/accessibility-inputs.ts +89 -0
  127. package/src/bootstrap.ts +6 -2
  128. package/src/components/checkbox-props.ts +12 -0
  129. package/src/components/flat-list/flat-list-template.ts +159 -0
  130. package/src/components/flat-list/index.ts +30 -307
  131. package/src/components/keyboard-avoiding-view/index.ts +4 -78
  132. package/src/components/modal/index.ts +7 -80
  133. package/src/components/section-list/index.ts +21 -172
  134. package/src/components/virtualized-list/cell-registry.ts +199 -0
  135. package/src/components/virtualized-list/index.ts +145 -1181
  136. package/src/components/virtualized-list/list-a11y-forward.ts +42 -0
  137. package/src/components/virtualized-list/list-bags.ts +142 -0
  138. package/src/components/virtualized-list/list-effects.ts +116 -0
  139. package/src/components/virtualized-list/list-events.ts +31 -0
  140. package/src/components/virtualized-list/list-handle.ts +92 -0
  141. package/src/components/virtualized-list/list-inputs.ts +149 -0
  142. package/src/components/virtualized-list/list-props.ts +81 -0
  143. package/src/components/virtualized-list/list-styles.ts +62 -0
  144. package/src/components/virtualized-list/list-template.ts +175 -0
  145. package/src/components/virtualized-section-list/index.ts +40 -351
  146. package/src/components/virtualized-section-list/section-list-template.ts +108 -0
  147. package/src/components.ts +1 -0
  148. package/src/descriptor-to-angular/descriptor-host.ts +27 -0
  149. package/src/descriptor-to-angular/index.ts +5 -0
  150. package/src/element-base.ts +272 -0
  151. package/src/elements-controls.ts +282 -0
  152. package/src/elements-layout.ts +141 -0
  153. package/src/elements-press.ts +145 -0
  154. package/src/elements.ts +73 -788
  155. package/src/finalization-registry.ts +18 -0
  156. package/src/host-instance/index.ts +4 -4
  157. package/src/index.ts +18 -2
  158. package/src/native-view-base.ts +86 -0
  159. package/src/render/index.ts +76 -77
  160. package/src/services/connect-watched-signal.ts +15 -0
  161. package/src/services/create-event-value-hook.ts +24 -0
  162. package/src/services/create-resource-hook.ts +32 -0
  163. package/src/services/index.ts +5 -0
  164. package/src/services/inject-event.ts +52 -0
  165. package/src/services/permissions-service-base.ts +46 -0
@@ -0,0 +1,18 @@
1
+ // Hermes has no `FinalizationRegistry`, and `@angular/core` >=22.2 builds one at module load
2
+ // Cleanup callbacks never fire, which only costs Angular's signal debug bookkeeping
3
+
4
+ class InertFinalizationRegistry {
5
+ register(): void {}
6
+
7
+ unregister(): boolean {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ if (typeof globalThis.FinalizationRegistry === 'undefined') {
13
+ Object.defineProperty(globalThis, 'FinalizationRegistry', {
14
+ value: InertFinalizationRegistry,
15
+ configurable: true,
16
+ writable: true,
17
+ });
18
+ }
@@ -21,14 +21,14 @@ import {
21
21
 
22
22
  export type { IHostInstance } from '@symbiote-native/engine';
23
23
 
24
- function resolveHostNode(candidate: unknown): ISymbioteNode | null {
24
+ export function hostNodeOf(candidate: unknown): ISymbioteNode | null {
25
25
  if (candidate === null || candidate === undefined) return null;
26
26
  if (isSymbioteNode(candidate)) return candidate;
27
27
  if (candidate instanceof ElementRef)
28
- return resolveHostNode(candidate.nativeElement);
28
+ return hostNodeOf(candidate.nativeElement);
29
29
  const maybeHost = candidate as { nativeElement?: unknown };
30
30
  if (typeof maybeHost.nativeElement !== 'undefined') {
31
- return resolveHostNode(maybeHost.nativeElement);
31
+ return hostNodeOf(maybeHost.nativeElement);
32
32
  }
33
33
  return null;
34
34
  }
@@ -37,6 +37,6 @@ export function findNodeHandle(componentOrHandle: unknown): number | null {
37
37
  if (componentOrHandle === null || componentOrHandle === undefined)
38
38
  return null;
39
39
  if (typeof componentOrHandle === 'number') return componentOrHandle;
40
- const node = resolveHostNode(componentOrHandle);
40
+ const node = hostNodeOf(componentOrHandle);
41
41
  return node ? (getNativeTag(node) ?? null) : null;
42
42
  }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  // template dependencies to importable names (NG3004, the same rule that put the element directives
18
18
  // back below). Retiring them means retiring them from those nine components first.
19
19
 
20
+ import './finalization-registry';
20
21
  import './register';
21
22
 
22
23
  export {
@@ -88,6 +89,7 @@ export type {
88
89
  IAngularTouchableOpacityProps,
89
90
  IAngularTouchableWithoutFeedbackProps,
90
91
  IButtonProps,
92
+ ICheckboxProps,
91
93
  ICellLayout,
92
94
  IFlatListHandle,
93
95
  IFlatListProps,
@@ -143,6 +145,7 @@ export {
143
145
  ActivityIndicatorElement,
144
146
  ActivityIndicatorSpinnerElement,
145
147
  ButtonElement,
148
+ CheckboxElement,
146
149
  HorizontalScrollContentElement,
147
150
  HorizontalScrollViewElement,
148
151
  ImageBackgroundElement,
@@ -186,6 +189,7 @@ export { mount, unmount } from './render';
186
189
  // @symbiote-native/slider) can render a shared @symbiote-native/components/@symbiote-native/slider Descriptor tree
187
190
  // without hand-writing its own Renderer2 walker.
188
191
  export { DescriptorOutlet } from './descriptor-to-angular';
192
+ export { DescriptorHost } from './descriptor-to-angular/descriptor-host';
189
193
  // createPortal (same-surface only — see the file header) and createTunnel (cross-surface,
190
194
  // see its file header) are the Angular twins of the React/Vue portal/tunnel primitives.
191
195
  // Angular can't synthesize components at runtime (no JIT under Metro/Hermes), so both are
@@ -205,7 +209,7 @@ export { SymbioteRenderer, SymbioteRendererFactory } from './renderer';
205
209
  // import straight from that subpath so app-screen registration never routes through this cyclic
206
210
  // barrel.
207
211
  export { registerComposedComponent } from './anchor-host-registry';
208
- export { findNodeHandle } from './host-instance';
212
+ export { findNodeHandle, hostNodeOf } from './host-instance';
209
213
  export type { IHostInstance } from './host-instance';
210
214
  export { StatusBar } from './modules/status-bar';
211
215
  export type { IStatusBarProps, IStatusBarStyle } from './modules/status-bar';
@@ -224,7 +228,19 @@ export type {
224
228
  ITaskCanceller,
225
229
  ITaskCancelProvider,
226
230
  } from './modules/app-registry';
227
- export { ColorSchemeService, WindowDimensionsService } from './services';
231
+ export {
232
+ ColorSchemeService,
233
+ WindowDimensionsService,
234
+ createResourceHook,
235
+ createEventValueHook,
236
+ injectEvent,
237
+ injectEventListener,
238
+ connectWatchedSignal,
239
+ PermissionsServiceBase,
240
+ } from './services';
241
+ export { AccessibilityInputsBase } from './accessibility-inputs';
242
+ export { NativeViewBase } from './native-view-base';
243
+ export { anchorStyleProp } from './primitives/shared';
228
244
 
229
245
  // Framework-agnostic runtime modules from @symbiote-native/engine. Every adapter re-exports them so
230
246
  // app code names only @symbiote-native/angular.
@@ -0,0 +1,86 @@
1
+ import {
2
+ ChangeDetectorRef,
3
+ Directive,
4
+ ElementRef,
5
+ inject,
6
+ Input,
7
+ ViewChild,
8
+ } from '@angular/core';
9
+ import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
10
+ import { resolveAccessibilityProps } from '@symbiote-native/components';
11
+ import type { IDescriptor } from '@symbiote-native/components';
12
+ import type {
13
+ IStyleProp,
14
+ ISymbioteEvent,
15
+ ISymbioteNode,
16
+ IViewStyle,
17
+ } from '@symbiote-native/engine';
18
+ import { AccessibilityInputsBase } from './accessibility-inputs';
19
+ import { DescriptorOutlet } from './descriptor-to-angular';
20
+ import { hostNodeOf } from './host-instance';
21
+ import { anchorStyleProp } from './primitives/shared';
22
+
23
+ // Shared shell of a component that paints one Expo native view: the common inputs, the anchor
24
+ // host `class="..."` resolves onto, and a descriptor that recomputes on every OnPush check
25
+ @Directive()
26
+ export abstract class NativeViewBase
27
+ extends AccessibilityInputsBase
28
+ implements OnChanges, OnDestroy
29
+ {
30
+ @Input() style?: IStyleProp<IViewStyle>;
31
+ @Input() testID?: string;
32
+ @Input() nativeID?: string;
33
+ @Input() onLayout?: (event: ISymbioteEvent) => void;
34
+ @Input() onAccessibilityTap?: (event: ISymbioteEvent) => void;
35
+ @Input() onMagicTap?: (event: ISymbioteEvent) => void;
36
+ @Input() onAccessibilityEscape?: (event: ISymbioteEvent) => void;
37
+
38
+ // This component's own `ElementRef`, the anchor a `class="..."` at the use site lands on
39
+ private readonly elementRef = inject(ElementRef);
40
+ protected readonly changeDetector = inject(ChangeDetectorRef);
41
+
42
+ @ViewChild(DescriptorOutlet) private readonly nativeOutlet?: DescriptorOutlet;
43
+
44
+ // The committed host node of the native view, for a component that calls its view functions
45
+ protected hostNode(): ISymbioteNode | null {
46
+ return hostNodeOf(this.nativeOutlet?.rootNode);
47
+ }
48
+
49
+ // Hook for a component that holds more than its native node, runs once when it is destroyed
50
+ protected disposeView(): void {}
51
+
52
+ // A `[style]` binding reaches the `style` input without dirtying an OnPush view, a plain
53
+ // `[prop]` does. A subclass with its own `ngOnChanges` calls this one first
54
+ ngOnChanges(changes: SimpleChanges): void {
55
+ if ('style' in changes) this.changeDetector.markForCheck();
56
+ }
57
+
58
+ ngOnDestroy(): void {
59
+ this.disposeView();
60
+ }
61
+
62
+ // Names of the component's own inputs that go to the native view, read off `this`
63
+ protected abstract readonly propNames: readonly string[];
64
+
65
+ // The core render fn, `null` when the view has nothing to paint on this platform
66
+ protected abstract renderView(props: object): IDescriptor | null;
67
+
68
+ get descriptor(): IDescriptor | null {
69
+ return this.renderView(
70
+ resolveAccessibilityProps({
71
+ ...this.accessibilityInputProps(),
72
+ ...Object.fromEntries(
73
+ this.propNames.map(name => [name, Reflect.get(this, name)]),
74
+ ),
75
+ // Ambient class style first, so an explicit `style` input wins
76
+ style: [anchorStyleProp<IViewStyle>(this.elementRef), this.style],
77
+ testID: this.testID,
78
+ nativeID: this.nativeID,
79
+ onLayout: this.onLayout,
80
+ onAccessibilityTap: this.onAccessibilityTap,
81
+ onMagicTap: this.onMagicTap,
82
+ onAccessibilityEscape: this.onAccessibilityEscape,
83
+ }),
84
+ );
85
+ }
86
+ }
@@ -29,6 +29,8 @@ import {
29
29
  ɵprovideZonelessChangeDetectionInternal as provideZonelessChangeDetectionInternal,
30
30
  type ComponentRef,
31
31
  type EnvironmentInjector,
32
+ type EnvironmentProviders,
33
+ type Provider,
32
34
  type Type,
33
35
  } from '@angular/core';
34
36
  import { SymbioteRendererFactory } from '../renderer';
@@ -119,6 +121,15 @@ export interface IMountOptions {
119
121
  wrapperComponent?: Type<unknown>;
120
122
  }
121
123
 
124
+ // App-level providers (`provideHttpClient()` and the like), set once at bootstrap
125
+ let rootProviders: (Provider | EnvironmentProviders)[] = [];
126
+
127
+ export function setRootProviders(
128
+ providers: (Provider | EnvironmentProviders)[],
129
+ ): void {
130
+ rootProviders = providers;
131
+ }
132
+
122
133
  function applyInputs(
123
134
  cmpRef: ComponentRef<unknown>,
124
135
  initialProps: object | undefined,
@@ -189,32 +200,17 @@ function settleAngularDevMode(): void {
189
200
  enableProdMode();
190
201
  }
191
202
 
192
- export function mount(
193
- rootTag: IRootTag,
194
- rootComponent: Type<unknown>,
195
- options?: IMountOptions,
196
- ): SymbioteSurface {
197
- settleAngularDevMode();
198
- // A re-mount on a live rootTag starts clean; otherwise the stale app double-drives the surface.
199
- teardown(rootTag);
200
-
201
- const surface = createSurface(rootTag);
202
- const rendererFactory = new SymbioteRendererFactory(surface);
203
- const injector = createEnvironmentInjector(
203
+ // Kept apart from `mount` so the provider list reads as one block
204
+ function createRootInjector(
205
+ surface: SymbioteSurface,
206
+ rendererFactory: SymbioteRendererFactory,
207
+ ): EnvironmentInjector {
208
+ return createEnvironmentInjector(
204
209
  [
205
- {
206
- provide: RendererFactory2,
207
- useValue: rendererFactory,
208
- },
209
- // `getElementById` is not decoration: `resource()` resolves TransferState, whose root
210
- // factory runs `retrieveTransferredState(doc, appId)` ->
211
- // `doc.getElementById(appId + '-state')` (core/src/transfer_state.ts:156) to pick up
212
- // server-rendered state. A stub without it makes that a TypeError, and because the throw
213
- // happens while the component is being constructed, the ENTIRE screen renders nothing —
214
- // white body under a native header that navigation drew anyway, no error surfaced. The
215
- // `optional: true` on the injector lookup does not help: the token resolves, its factory
216
- // is what throws. Returning null is the honest client answer — there is no server here, so
217
- // `script?.tagName` short-circuits and the caller gets `{}`.
210
+ ...rootProviders,
211
+ { provide: RendererFactory2, useValue: rendererFactory },
212
+ // `resource()` resolves TransferState, which calls `doc.getElementById`
213
+ // a stub without it throws mid-construction and the whole screen renders nothing
218
214
  {
219
215
  provide: DOCUMENT,
220
216
  useValue: {
@@ -223,72 +219,75 @@ export function mount(
223
219
  getElementById: (): null => null,
224
220
  },
225
221
  },
226
- // createEnvironmentInjector with a null parent scopes this injector to {'environment'}
227
- // only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
228
- // included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
229
- // a null parent always dead-ends in NullInjector). platform-browser solves this the same
230
- // way for real DOM apps via BROWSER_MODULE_PROVIDERS: hand { provide: INJECTOR_SCOPE,
231
- // useValue: 'root' } to the app-level providers, which R3Injector's constructor reads to
232
- // self-tag this.scopes with 'root'. Same trick here, no PlatformRef, no DOM.
222
+ // A null-parent injector is not root-scoped, so `providedIn: 'root'` tokens never resolve
233
223
  { provide: INJECTOR_SCOPE, useValue: 'root' },
234
- // Supplies the real ChangeDetectionSchedulerImpl (microtask-batched via
235
- // ApplicationRef.afterTick) + NoopNgZone + ZONELESS_ENABLED: true — the exact bundle
236
- // internalCreateApplication() uses. Replaces the old unconditional
237
- // `rootView.detectChanges(); cmpView.detectChanges()` (force-ran both root views on every
238
- // tick) with Angular's own tick(), which only enters a view something actually marked
239
- // dirty. This does NOT stop the root's own template from re-running on a plain press or
240
- // `markForCheck()` anywhere in the tree — `markViewDirty` unconditionally sets RefreshView
241
- // on every ancestor up to the root; that's fundamental Angular zoneless behavior, not
242
- // something this swap changes. A genuine child `@Component` boundary still protects a
243
- // sibling branch from an unrelated press.
224
+ // Real microtask-batched scheduler plus a noop zone, so `appRef.tick()` drives the views
244
225
  ...provideZonelessChangeDetectionInternal(),
245
- // Angular's INTERNAL_APPLICATION_ERROR_HANDLER reports a tick() exception via
246
- // `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
247
- // default, but our from-scratch environment injector never did, so the lookup itself threw
248
- // NG0201 and replaced the real error with "No provider found for ErrorHandler" — any async
249
- // tick() exception crashed hard, uncaught, instead of being reported. The token still has to
250
- // be provided for exactly that reason; what it resolves to is now SymbioteErrorHandler
251
- // (see above), which reports through the engine instead of Angular's `console.error('ERROR',
252
- // e)` and, like the default, returns rather than rethrows so the app keeps running.
226
+ // Angular's `tick()` looks up `ErrorHandler`, and a from-scratch injector has none (NG0201)
253
227
  { provide: ErrorHandler, useClass: SymbioteErrorHandler },
254
228
  ColorSchemeService,
255
229
  WindowDimensionsService,
256
230
  ],
257
231
  rootInjectorParent(),
258
232
  );
233
+ }
259
234
 
260
- // hostElement = the surface: the component's template content commits straight into the
261
- // surface with no wrapper view, the engine wrapping surface.children in its synthetic flex
262
- // root — the Angular equivalent of Vue's `app.mount(surface)`.
263
- let cmpRef: ComponentRef<unknown>;
264
- let rootRef: ComponentRef<unknown> | undefined;
265
- if (options?.wrapperComponent === undefined) {
266
- cmpRef = createComponent<unknown>(rootComponent, {
267
- environmentInjector: injector,
268
- hostElement: asAngularHost(surface),
269
- });
270
- applyInputs(cmpRef, options?.initialProps);
271
- } else {
272
- // The root gets its own host node from the renderer (createComponent without a
273
- // hostElement) but is not attached to the surface directly; it is handed to the
274
- // wrapper as projectable content instead, so only the wrapper needs a real host.
275
- const rootHost = asAngularHost(createDetachedViewHost());
276
- rootRef = createComponent<unknown>(rootComponent, {
277
- environmentInjector: injector,
278
- hostElement: rootHost,
279
- });
280
- applyInputs(rootRef, options.initialProps);
281
- cmpRef = createComponent<unknown>(options.wrapperComponent, {
235
+ interface IRoots {
236
+ cmpRef: ComponentRef<unknown>;
237
+ rootRef: ComponentRef<unknown> | undefined;
238
+ }
239
+
240
+ function createRoots(
241
+ injector: EnvironmentInjector,
242
+ surface: SymbioteSurface,
243
+ rootComponent: Type<unknown>,
244
+ options: IMountOptions | undefined,
245
+ ): IRoots {
246
+ const { wrapperComponent, initialProps } = options ?? {};
247
+ // `hostElement` is the surface, content commits straight into it with no wrapper view
248
+ if (wrapperComponent === undefined) {
249
+ const cmpRef = createComponent<unknown>(rootComponent, {
282
250
  environmentInjector: injector,
283
251
  hostElement: asAngularHost(surface),
284
- projectableNodes: [[rootHost]],
285
252
  });
253
+ applyInputs(cmpRef, initialProps);
254
+ return { cmpRef, rootRef: undefined };
286
255
  }
256
+ // The root gets its own host and reaches the wrapper as projected content
257
+ const rootHost = asAngularHost(createDetachedViewHost());
258
+ const rootRef = createComponent<unknown>(rootComponent, {
259
+ environmentInjector: injector,
260
+ hostElement: rootHost,
261
+ });
262
+ applyInputs(rootRef, initialProps);
263
+ const cmpRef = createComponent<unknown>(wrapperComponent, {
264
+ environmentInjector: injector,
265
+ hostElement: asAngularHost(surface),
266
+ projectableNodes: [[rootHost]],
267
+ });
268
+ return { cmpRef, rootRef };
269
+ }
270
+
271
+ export function mount(
272
+ rootTag: IRootTag,
273
+ rootComponent: Type<unknown>,
274
+ options?: IMountOptions,
275
+ ): SymbioteSurface {
276
+ settleAngularDevMode();
277
+ // A re-mount on a live rootTag starts clean; otherwise the stale app double-drives the surface.
278
+ teardown(rootTag);
279
+
280
+ const surface = createSurface(rootTag);
281
+ const rendererFactory = new SymbioteRendererFactory(surface);
282
+ const injector = createRootInjector(surface, rendererFactory);
283
+ const { cmpRef, rootRef } = createRoots(
284
+ injector,
285
+ surface,
286
+ rootComponent,
287
+ options,
288
+ );
287
289
 
288
- // Attach both root views to ApplicationRef so its own tick() (via the real
289
- // ChangeDetectionSchedulerImpl provided above) drives them from here on — no manual
290
- // ChangeDetectorRef juggling needed. markForCheck anywhere in the tree now notifies
291
- // the real scheduler, which batches a microtask and calls appRef.tick() itself.
290
+ // `tick()` then drives both root views through the real scheduler
292
291
  const appRef = injector.get(ApplicationRef);
293
292
  appRef.attachView(cmpRef.hostView);
294
293
  if (rootRef !== undefined) {
@@ -0,0 +1,15 @@
1
+ // A signal fed by a `watch` subscription that lives as long as the injector's owner
2
+
3
+ import { effect, signal, type Injector, type Signal } from '@angular/core';
4
+
5
+ export function connectWatchedSignal<T>(
6
+ injector: Injector,
7
+ initial: T,
8
+ watch: (set: (value: T) => void) => () => void,
9
+ ): Signal<T> {
10
+ const value = signal(initial);
11
+
12
+ effect(onCleanup => onCleanup(watch(next => value.set(next))), { injector });
13
+
14
+ return value.asReadonly();
15
+ }
@@ -0,0 +1,24 @@
1
+ // Every `useXStatus`-style `injectX` (`injectAudioPlayerStatus`, ...) binds this to its own
2
+ // event name + initial-value getter instead of repeating the signal/effect wiring
3
+
4
+ import { effect, signal, type Signal } from '@angular/core';
5
+ import type { IEventValueSource } from '@symbiote-native/engine';
6
+
7
+ export function createEventValueHook<
8
+ TSource extends IEventValueSource<TValue, TEvent>,
9
+ TValue,
10
+ TEvent extends string = string,
11
+ >(event: TEvent, getValue: (source: TSource) => TValue) {
12
+ return function injectValue(getSource: () => TSource): Signal<TValue> {
13
+ const value = signal(getValue(getSource()));
14
+
15
+ effect(onCleanup => {
16
+ const source = getSource();
17
+ value.set(getValue(source));
18
+ const subscription = source.addListener(event, next => value.set(next));
19
+ onCleanup(() => subscription.remove());
20
+ });
21
+
22
+ return value.asReadonly();
23
+ };
24
+ }
@@ -0,0 +1,32 @@
1
+ // Every `useReleasingSharedObject`-style `injectX` (`injectAudioPlayer`, `injectImageManipulator`,
2
+ // ...) binds this to its own `createXController()` factory instead of repeating the wiring
3
+
4
+ import { DestroyRef, effect, inject, signal, type Signal } from '@angular/core';
5
+
6
+ export type IResourceHookController<TArgs extends unknown[], TResource> = {
7
+ resolve: (...args: TArgs) => TResource;
8
+ flushDispose: () => void;
9
+ dispose: () => void;
10
+ };
11
+
12
+ export function createResourceHook<TArgs extends unknown[], TResource>(
13
+ createController: () => IResourceHookController<TArgs, TResource>,
14
+ ) {
15
+ return function injectResource(getArgs: () => TArgs): Signal<TResource> {
16
+ const controller = createController();
17
+ const resource = signal(controller.resolve(...getArgs()));
18
+
19
+ effect(() => {
20
+ resource.set(controller.resolve(...getArgs()));
21
+ });
22
+
23
+ effect(() => {
24
+ resource();
25
+ controller.flushDispose();
26
+ });
27
+
28
+ inject(DestroyRef).onDestroy(() => controller.dispose());
29
+
30
+ return resource.asReadonly();
31
+ };
32
+ }
@@ -1,2 +1,7 @@
1
1
  export { ColorSchemeService } from './color-scheme.service';
2
2
  export { WindowDimensionsService } from './window-dimensions.service';
3
+ export { createResourceHook } from './create-resource-hook';
4
+ export { createEventValueHook } from './create-event-value-hook';
5
+ export { injectEvent, injectEventListener } from './inject-event';
6
+ export { connectWatchedSignal } from './connect-watched-signal';
7
+ export { PermissionsServiceBase } from './permissions-service-base';
@@ -0,0 +1,52 @@
1
+ // Angular twins of `useEvent` and `useEventListener` of the `expo` package, for any emitter
2
+
3
+ import { effect, signal, untracked } from '@angular/core';
4
+ import type { Signal } from '@angular/core';
5
+ import { bindEventListener } from '@symbiote-native/engine';
6
+ import type {
7
+ IEventEmitterOf,
8
+ IEventName,
9
+ IEventPayload,
10
+ IEventsMap,
11
+ } from '@symbiote-native/engine';
12
+
13
+ /** Calls the listener for each event, the latest one, and stops with the injection context */
14
+ export function injectEventListener<
15
+ TEventsMap extends IEventsMap,
16
+ TName extends IEventName<TEventsMap>,
17
+ >(
18
+ emitter: () => IEventEmitterOf<TEventsMap>,
19
+ eventName: () => TName,
20
+ listener: () => TEventsMap[TName],
21
+ ): void {
22
+ effect(onCleanup => {
23
+ const source = emitter();
24
+ const name = eventName();
25
+ // The listener is read at each event, a new one is not a new subscription
26
+ onCleanup(bindEventListener(source, name, () => untracked(listener)));
27
+ });
28
+ }
29
+
30
+ /** The payload of the last event, `initialValue` (or `null`) until the first one */
31
+ export function injectEvent<
32
+ TEventsMap extends IEventsMap,
33
+ TName extends IEventName<TEventsMap>,
34
+ >(
35
+ emitter: () => IEventEmitterOf<TEventsMap>,
36
+ eventName: () => TName,
37
+ initialValue: IEventPayload<TEventsMap, TName> | null = null,
38
+ ): Signal<IEventPayload<TEventsMap, TName> | null> {
39
+ const event = signal<IEventPayload<TEventsMap, TName> | null>(initialValue);
40
+ effect(onCleanup => {
41
+ const source = emitter();
42
+ const name = eventName();
43
+ onCleanup(
44
+ bindEventListener(
45
+ source,
46
+ name,
47
+ () => (next: IEventPayload<TEventsMap, TName>) => event.set(next),
48
+ ),
49
+ );
50
+ });
51
+ return event.asReadonly();
52
+ }
@@ -0,0 +1,46 @@
1
+ import { signal, type Signal } from '@angular/core';
2
+
3
+ // Общая часть сервисов разрешений всех Expo-пакетов, наследник передаёт пару методов
4
+ // Сбой автозапроса попадает в `error`, а не в unhandled rejection: null-статус с непустым `error`
5
+ // значит "запрос упал", а не "ещё не было"
6
+ export abstract class PermissionsServiceBase<TPermission, TOptions = void> {
7
+ private readonly status = signal<TPermission | null>(null);
8
+ private readonly fetchError = signal<Error | null>(null);
9
+ private isAutoFetchStarted = false;
10
+
11
+ readonly error: Signal<Error | null> = this.fetchError.asReadonly();
12
+
13
+ protected constructor(
14
+ private readonly getMethod: (options?: TOptions) => Promise<TPermission>,
15
+ private readonly requestMethod: (
16
+ options?: TOptions,
17
+ ) => Promise<TPermission>,
18
+ ) {}
19
+
20
+ // Защёлка, а не проверка `status() === null`: упавший запрос оставляет null навсегда
21
+ connect(): Signal<TPermission | null> {
22
+ if (!this.isAutoFetchStarted) {
23
+ this.isAutoFetchStarted = true;
24
+ this.get().catch((cause: unknown) => {
25
+ this.fetchError.set(
26
+ cause instanceof Error ? cause : new Error(String(cause)),
27
+ );
28
+ });
29
+ }
30
+ return this.status.asReadonly();
31
+ }
32
+
33
+ async get(options?: TOptions): Promise<TPermission> {
34
+ return this.store(await this.getMethod(options));
35
+ }
36
+
37
+ async request(options?: TOptions): Promise<TPermission> {
38
+ return this.store(await this.requestMethod(options));
39
+ }
40
+
41
+ private store(response: TPermission): TPermission {
42
+ this.status.set(response);
43
+ this.fetchError.set(null);
44
+ return response;
45
+ }
46
+ }