@symbiote-native/angular 3.1.3 → 3.2.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/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/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/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/index.d.ts +5 -1
- package/build/angular/index.js +5 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/native-view-base.d.ts +19 -0
- package/build/angular/native-view-base.js +48 -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 +4 -0
- package/build/angular/services/index.js +4 -0
- package/build/angular/services/index.js.map +1 -1
- 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/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/finalization-registry.ts +18 -0
- package/src/index.ts +12 -1
- package/src/native-view-base.ts +51 -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 +4 -0
- package/src/services/permissions-service-base.ts +46 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// One synthesized `vListItem` template switches on the flattened entry kind and stamps the matching
|
|
2
|
+
// app template, since the inner list renders every cell through a single one
|
|
3
|
+
|
|
4
|
+
import { LIST_ACCESSIBILITY_FORWARD } from '../virtualized-list/list-a11y-forward';
|
|
5
|
+
|
|
6
|
+
export const VIRTUALIZED_SECTION_LIST_TEMPLATE = `
|
|
7
|
+
<VirtualizedList
|
|
8
|
+
[data]="flatEntries"
|
|
9
|
+
[getItem]="getEntry"
|
|
10
|
+
[getItemCount]="getEntryCount"
|
|
11
|
+
[keyExtractor]="entryKeyExtractor"
|
|
12
|
+
[getItemLayout]="entryItemLayout"
|
|
13
|
+
[stickyHeaderIndices]="stickyHeaderIndices"
|
|
14
|
+
[extraData]="extraData"
|
|
15
|
+
[inverted]="inverted"
|
|
16
|
+
[refreshing]="refreshing"
|
|
17
|
+
[progressViewOffset]="progressViewOffset"
|
|
18
|
+
[refreshRequested]="refreshRequested ?? refresh.observed"
|
|
19
|
+
(refresh)="resolvedOnRefresh?.()"
|
|
20
|
+
(endReached)="resolvedOnEndReached?.($event)"
|
|
21
|
+
[onEndReachedThreshold]="onEndReachedThreshold"
|
|
22
|
+
(startReached)="resolvedOnStartReached?.($event)"
|
|
23
|
+
[onStartReachedThreshold]="onStartReachedThreshold"
|
|
24
|
+
[initialNumToRender]="initialNumToRender"
|
|
25
|
+
[initialScrollIndex]="initialScrollIndex"
|
|
26
|
+
[maxToRenderPerBatch]="maxToRenderPerBatch"
|
|
27
|
+
[updateCellsBatchingPeriod]="updateCellsBatchingPeriod"
|
|
28
|
+
[windowSize]="windowSize"
|
|
29
|
+
[maintainVisibleContentPosition]="maintainVisibleContentPosition"
|
|
30
|
+
[onScroll]="onScroll"
|
|
31
|
+
[onScrollBeginDrag]="onScrollBeginDrag"
|
|
32
|
+
[onScrollEndDrag]="onScrollEndDrag"
|
|
33
|
+
[onMomentumScrollBegin]="onMomentumScrollBegin"
|
|
34
|
+
[onMomentumScrollEnd]="onMomentumScrollEnd"
|
|
35
|
+
[scrollEventThrottle]="scrollEventThrottle"
|
|
36
|
+
[keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
|
|
37
|
+
[keyboardDismissMode]="keyboardDismissMode"
|
|
38
|
+
[removeClippedSubviews]="removeClippedSubviews"
|
|
39
|
+
[nestedScrollEnabled]="nestedScrollEnabled"
|
|
40
|
+
[style]="resolvedStyle"
|
|
41
|
+
[contentContainerStyle]="contentContainerStyle"
|
|
42
|
+
[testID]="testID"
|
|
43
|
+
[nativeID]="nativeID"
|
|
44
|
+
${LIST_ACCESSIBILITY_FORWARD}
|
|
45
|
+
>
|
|
46
|
+
<!-- The single synthesized cell template: dispatch per flattened entry tag -->
|
|
47
|
+
<ng-template vListItem let-entry let-separators="separators">
|
|
48
|
+
@switch (entryKind(entry)) {
|
|
49
|
+
@case ('header') {
|
|
50
|
+
<ng-container
|
|
51
|
+
[vListOutlet]="sectionHeaderDir?.templateRef"
|
|
52
|
+
[vListOutletContext]="sectionContextOf(entry)"
|
|
53
|
+
></ng-container>
|
|
54
|
+
}
|
|
55
|
+
@case ('footer') {
|
|
56
|
+
<ng-container
|
|
57
|
+
[vListOutlet]="sectionFooterDir?.templateRef"
|
|
58
|
+
[vListOutletContext]="sectionContextOf(entry)"
|
|
59
|
+
></ng-container>
|
|
60
|
+
}
|
|
61
|
+
@case ('section-separator') {
|
|
62
|
+
<ng-container
|
|
63
|
+
[vListOutlet]="sectionSeparatorDir?.templateRef"
|
|
64
|
+
></ng-container>
|
|
65
|
+
}
|
|
66
|
+
@case ('item') {
|
|
67
|
+
<ng-container
|
|
68
|
+
[vListOutlet]="sectionItemDir?.templateRef"
|
|
69
|
+
[vListOutletContext]="itemContextOf(entry, separators)"
|
|
70
|
+
></ng-container>
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</ng-template>
|
|
74
|
+
|
|
75
|
+
<!-- List-level slots: forward the app's directives to the inner list -->
|
|
76
|
+
@if (listHeaderDir !== undefined) {
|
|
77
|
+
<ng-template vListHeader>
|
|
78
|
+
<ng-container [vListOutlet]="listHeaderDir.templateRef"></ng-container>
|
|
79
|
+
</ng-template>
|
|
80
|
+
}
|
|
81
|
+
@if (listFooterDir !== undefined) {
|
|
82
|
+
<ng-template vListFooter>
|
|
83
|
+
<ng-container [vListOutlet]="listFooterDir.templateRef"></ng-container>
|
|
84
|
+
</ng-template>
|
|
85
|
+
}
|
|
86
|
+
@if (listEmptyDir !== undefined) {
|
|
87
|
+
<ng-template vListEmpty>
|
|
88
|
+
<ng-container [vListOutlet]="listEmptyDir.templateRef"></ng-container>
|
|
89
|
+
</ng-template>
|
|
90
|
+
}
|
|
91
|
+
<!-- Item separator: unwrap each flattened entry back to its item -->
|
|
92
|
+
@if (itemSeparatorDir !== undefined) {
|
|
93
|
+
<ng-template
|
|
94
|
+
vListSeparator
|
|
95
|
+
let-highlighted
|
|
96
|
+
let-leadingItem="leadingItem"
|
|
97
|
+
let-trailingItem="trailingItem"
|
|
98
|
+
>
|
|
99
|
+
<ng-container
|
|
100
|
+
[vListOutlet]="itemSeparatorDir.templateRef"
|
|
101
|
+
[vListOutletContext]="
|
|
102
|
+
itemSeparatorContextOf(highlighted, leadingItem, trailingItem)
|
|
103
|
+
"
|
|
104
|
+
></ng-container>
|
|
105
|
+
</ng-template>
|
|
106
|
+
}
|
|
107
|
+
</VirtualizedList>
|
|
108
|
+
`;
|
|
@@ -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
|
+
}
|
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 {
|
|
@@ -224,7 +225,17 @@ export type {
|
|
|
224
225
|
ITaskCanceller,
|
|
225
226
|
ITaskCancelProvider,
|
|
226
227
|
} from './modules/app-registry';
|
|
227
|
-
export {
|
|
228
|
+
export {
|
|
229
|
+
ColorSchemeService,
|
|
230
|
+
WindowDimensionsService,
|
|
231
|
+
createResourceHook,
|
|
232
|
+
createEventValueHook,
|
|
233
|
+
connectWatchedSignal,
|
|
234
|
+
PermissionsServiceBase,
|
|
235
|
+
} from './services';
|
|
236
|
+
export { AccessibilityInputsBase } from './accessibility-inputs';
|
|
237
|
+
export { NativeViewBase } from './native-view-base';
|
|
238
|
+
export { anchorStyleProp } from './primitives/shared';
|
|
228
239
|
|
|
229
240
|
// Framework-agnostic runtime modules from @symbiote-native/engine. Every adapter re-exports them so
|
|
230
241
|
// app code names only @symbiote-native/angular.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Directive, ElementRef, inject, Input } from '@angular/core';
|
|
2
|
+
import { resolveAccessibilityProps } from '@symbiote-native/components';
|
|
3
|
+
import type { IDescriptor } from '@symbiote-native/components';
|
|
4
|
+
import type {
|
|
5
|
+
IStyleProp,
|
|
6
|
+
ISymbioteEvent,
|
|
7
|
+
IViewStyle,
|
|
8
|
+
} from '@symbiote-native/engine';
|
|
9
|
+
import { AccessibilityInputsBase } from './accessibility-inputs';
|
|
10
|
+
import { anchorStyleProp } from './primitives/shared';
|
|
11
|
+
|
|
12
|
+
// Shared shell of a component that paints one Expo native view: the common inputs, the anchor
|
|
13
|
+
// host `class="..."` resolves onto, and a descriptor that recomputes on every OnPush check
|
|
14
|
+
@Directive()
|
|
15
|
+
export abstract class NativeViewBase extends AccessibilityInputsBase {
|
|
16
|
+
@Input() style?: IStyleProp<IViewStyle>;
|
|
17
|
+
@Input() testID?: string;
|
|
18
|
+
@Input() nativeID?: string;
|
|
19
|
+
@Input() onLayout?: (event: ISymbioteEvent) => void;
|
|
20
|
+
@Input() onAccessibilityTap?: (event: ISymbioteEvent) => void;
|
|
21
|
+
@Input() onMagicTap?: (event: ISymbioteEvent) => void;
|
|
22
|
+
@Input() onAccessibilityEscape?: (event: ISymbioteEvent) => void;
|
|
23
|
+
|
|
24
|
+
// This component's own `ElementRef`, the anchor a `class="..."` at the use site lands on
|
|
25
|
+
private readonly elementRef = inject(ElementRef);
|
|
26
|
+
|
|
27
|
+
// Names of the component's own inputs that go to the native view, read off `this`
|
|
28
|
+
protected abstract readonly propNames: readonly string[];
|
|
29
|
+
|
|
30
|
+
// The core render fn, `null` when the view has nothing to paint on this platform
|
|
31
|
+
protected abstract renderView(props: object): IDescriptor | null;
|
|
32
|
+
|
|
33
|
+
get descriptor(): IDescriptor | null {
|
|
34
|
+
return this.renderView(
|
|
35
|
+
resolveAccessibilityProps({
|
|
36
|
+
...this.accessibilityInputProps(),
|
|
37
|
+
...Object.fromEntries(
|
|
38
|
+
this.propNames.map(name => [name, Reflect.get(this, name)]),
|
|
39
|
+
),
|
|
40
|
+
// Ambient class style first, so an explicit `style` input wins
|
|
41
|
+
style: [anchorStyleProp<IViewStyle>(this.elementRef), this.style],
|
|
42
|
+
testID: this.testID,
|
|
43
|
+
nativeID: this.nativeID,
|
|
44
|
+
onLayout: this.onLayout,
|
|
45
|
+
onAccessibilityTap: this.onAccessibilityTap,
|
|
46
|
+
onMagicTap: this.onMagicTap,
|
|
47
|
+
onAccessibilityEscape: this.onAccessibilityEscape,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
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,6 @@
|
|
|
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 { connectWatchedSignal } from './connect-watched-signal';
|
|
6
|
+
export { PermissionsServiceBase } from './permissions-service-base';
|
|
@@ -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
|
+
}
|