@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.
- package/babel-register-composed.cjs +1 -0
- package/build/angular/accessibility-events.d.ts +12 -0
- package/build/angular/accessibility-events.js +28 -0
- package/build/angular/accessibility-events.js.map +1 -0
- package/build/angular/accessibility-inputs.d.ts +40 -0
- package/build/angular/accessibility-inputs.js +130 -0
- package/build/angular/accessibility-inputs.js.map +1 -0
- package/build/angular/bootstrap.d.ts +3 -1
- package/build/angular/bootstrap.js +4 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/checkbox-props.d.ts +2 -0
- package/build/angular/components/checkbox-props.js +2 -0
- package/build/angular/components/checkbox-props.js.map +1 -0
- package/build/angular/components/flat-list/flat-list-template.d.ts +1 -0
- package/build/angular/components/flat-list/flat-list-template.js +158 -0
- package/build/angular/components/flat-list/flat-list-template.js.map +1 -0
- package/build/angular/components/flat-list/index.d.ts +6 -61
- package/build/angular/components/flat-list/index.js +26 -463
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +4 -40
- package/build/angular/components/keyboard-avoiding-view/index.js +7 -131
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +5 -41
- package/build/angular/components/modal/index.js +8 -133
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +4 -51
- package/build/angular/components/section-list/index.js +18 -335
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/virtualized-list/cell-registry.d.ts +35 -0
- package/build/angular/components/virtualized-list/cell-registry.js +133 -0
- package/build/angular/components/virtualized-list/cell-registry.js.map +1 -0
- package/build/angular/components/virtualized-list/index.d.ts +18 -253
- package/build/angular/components/virtualized-list/index.js +112 -1178
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-list/list-a11y-forward.d.ts +1 -0
- package/build/angular/components/virtualized-list/list-a11y-forward.js +42 -0
- package/build/angular/components/virtualized-list/list-a11y-forward.js.map +1 -0
- package/build/angular/components/virtualized-list/list-bags.d.ts +32 -0
- package/build/angular/components/virtualized-list/list-bags.js +120 -0
- package/build/angular/components/virtualized-list/list-bags.js.map +1 -0
- package/build/angular/components/virtualized-list/list-effects.d.ts +31 -0
- package/build/angular/components/virtualized-list/list-effects.js +83 -0
- package/build/angular/components/virtualized-list/list-effects.js.map +1 -0
- package/build/angular/components/virtualized-list/list-events.d.ts +17 -0
- package/build/angular/components/virtualized-list/list-events.js +34 -0
- package/build/angular/components/virtualized-list/list-events.js.map +1 -0
- package/build/angular/components/virtualized-list/list-handle.d.ts +35 -0
- package/build/angular/components/virtualized-list/list-handle.js +61 -0
- package/build/angular/components/virtualized-list/list-handle.js.map +1 -0
- package/build/angular/components/virtualized-list/list-inputs.d.ts +78 -0
- package/build/angular/components/virtualized-list/list-inputs.js +150 -0
- package/build/angular/components/virtualized-list/list-inputs.js.map +1 -0
- package/build/angular/components/virtualized-list/list-props.d.ts +58 -0
- package/build/angular/components/virtualized-list/list-props.js +3 -0
- package/build/angular/components/virtualized-list/list-props.js.map +1 -0
- package/build/angular/components/virtualized-list/list-styles.d.ts +23 -0
- package/build/angular/components/virtualized-list/list-styles.js +26 -0
- package/build/angular/components/virtualized-list/list-styles.js.map +1 -0
- package/build/angular/components/virtualized-list/list-template.d.ts +1 -0
- package/build/angular/components/virtualized-list/list-template.js +176 -0
- package/build/angular/components/virtualized-list/list-template.js.map +1 -0
- package/build/angular/components/virtualized-section-list/index.d.ts +13 -70
- package/build/angular/components/virtualized-section-list/index.js +22 -455
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -0
- package/build/angular/components/virtualized-section-list/section-list-template.js +107 -0
- package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -0
- package/build/angular/components.d.ts +1 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/descriptor-to-angular/descriptor-host.d.ts +8 -0
- package/build/angular/descriptor-to-angular/descriptor-host.js +38 -0
- package/build/angular/descriptor-to-angular/descriptor-host.js.map +1 -0
- package/build/angular/descriptor-to-angular/index.d.ts +1 -0
- package/build/angular/descriptor-to-angular/index.js +4 -0
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/element-base.d.ts +153 -0
- package/build/angular/element-base.js +272 -0
- package/build/angular/element-base.js.map +1 -0
- package/build/angular/elements-controls.d.ts +165 -0
- package/build/angular/elements-controls.js +382 -0
- package/build/angular/elements-controls.js.map +1 -0
- package/build/angular/elements-layout.d.ts +122 -0
- package/build/angular/elements-layout.js +242 -0
- package/build/angular/elements-layout.js.map +1 -0
- package/build/angular/elements-press.d.ts +96 -0
- package/build/angular/elements-press.js +227 -0
- package/build/angular/elements-press.js.map +1 -0
- package/build/angular/elements.d.ts +6 -516
- package/build/angular/elements.js +8 -1091
- package/build/angular/elements.js.map +1 -1
- package/build/angular/finalization-registry.d.ts +4 -0
- package/build/angular/finalization-registry.js +17 -0
- package/build/angular/finalization-registry.js.map +1 -0
- package/build/angular/host-instance/index.d.ts +2 -0
- package/build/angular/host-instance/index.js +4 -4
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +9 -4
- package/build/angular/index.js +8 -3
- package/build/angular/index.js.map +1 -1
- package/build/angular/native-view-base.d.ts +27 -0
- package/build/angular/native-view-base.js +69 -0
- package/build/angular/native-view-base.js.map +1 -0
- package/build/angular/render/index.d.ts +2 -1
- package/build/angular/render/index.js +46 -73
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/services/connect-watched-signal.d.ts +2 -0
- package/build/angular/services/connect-watched-signal.js +9 -0
- package/build/angular/services/connect-watched-signal.js.map +1 -0
- package/build/angular/services/create-event-value-hook.d.ts +3 -0
- package/build/angular/services/create-event-value-hook.js +17 -0
- package/build/angular/services/create-event-value-hook.js.map +1 -0
- package/build/angular/services/create-resource-hook.d.ts +7 -0
- package/build/angular/services/create-resource-hook.js +20 -0
- package/build/angular/services/create-resource-hook.js.map +1 -0
- package/build/angular/services/index.d.ts +5 -0
- package/build/angular/services/index.js +5 -0
- package/build/angular/services/index.js.map +1 -1
- package/build/angular/services/inject-event.d.ts +6 -0
- package/build/angular/services/inject-event.js +24 -0
- package/build/angular/services/inject-event.js.map +1 -0
- package/build/angular/services/permissions-service-base.d.ts +14 -0
- package/build/angular/services/permissions-service-base.js +38 -0
- package/build/angular/services/permissions-service-base.js.map +1 -0
- package/package.json +6 -6
- package/src/accessibility-events.ts +13 -0
- package/src/accessibility-inputs.ts +89 -0
- package/src/bootstrap.ts +6 -2
- package/src/components/checkbox-props.ts +12 -0
- package/src/components/flat-list/flat-list-template.ts +159 -0
- package/src/components/flat-list/index.ts +30 -307
- package/src/components/keyboard-avoiding-view/index.ts +4 -78
- package/src/components/modal/index.ts +7 -80
- package/src/components/section-list/index.ts +21 -172
- package/src/components/virtualized-list/cell-registry.ts +199 -0
- package/src/components/virtualized-list/index.ts +145 -1181
- package/src/components/virtualized-list/list-a11y-forward.ts +42 -0
- package/src/components/virtualized-list/list-bags.ts +142 -0
- package/src/components/virtualized-list/list-effects.ts +116 -0
- package/src/components/virtualized-list/list-events.ts +31 -0
- package/src/components/virtualized-list/list-handle.ts +92 -0
- package/src/components/virtualized-list/list-inputs.ts +149 -0
- package/src/components/virtualized-list/list-props.ts +81 -0
- package/src/components/virtualized-list/list-styles.ts +62 -0
- package/src/components/virtualized-list/list-template.ts +175 -0
- package/src/components/virtualized-section-list/index.ts +40 -351
- package/src/components/virtualized-section-list/section-list-template.ts +108 -0
- package/src/components.ts +1 -0
- package/src/descriptor-to-angular/descriptor-host.ts +27 -0
- package/src/descriptor-to-angular/index.ts +5 -0
- package/src/element-base.ts +272 -0
- package/src/elements-controls.ts +282 -0
- package/src/elements-layout.ts +141 -0
- package/src/elements-press.ts +145 -0
- package/src/elements.ts +73 -788
- package/src/finalization-registry.ts +18 -0
- package/src/host-instance/index.ts +4 -4
- package/src/index.ts +18 -2
- package/src/native-view-base.ts +86 -0
- package/src/render/index.ts +76 -77
- package/src/services/connect-watched-signal.ts +15 -0
- package/src/services/create-event-value-hook.ts +24 -0
- package/src/services/create-resource-hook.ts +32 -0
- package/src/services/index.ts +5 -0
- package/src/services/inject-event.ts +52 -0
- 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
|
|
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
|
|
28
|
+
return hostNodeOf(candidate.nativeElement);
|
|
29
29
|
const maybeHost = candidate as { nativeElement?: unknown };
|
|
30
30
|
if (typeof maybeHost.nativeElement !== 'undefined') {
|
|
31
|
-
return
|
|
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 =
|
|
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 {
|
|
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
|
+
}
|
package/src/render/index.ts
CHANGED
|
@@ -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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
):
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
//
|
|
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
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -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
|
+
}
|