@symbiote-native/angular 3.0.1 → 3.1.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 (61) hide show
  1. package/build/angular/callback-host.d.ts +2 -5
  2. package/build/angular/callback-host.js +11 -36
  3. package/build/angular/callback-host.js.map +1 -1
  4. package/build/angular/change-detection-flush.d.ts +1 -0
  5. package/build/angular/change-detection-flush.js +58 -14
  6. package/build/angular/change-detection-flush.js.map +1 -1
  7. package/build/angular/components/flat-list/index.d.ts +4 -2
  8. package/build/angular/components/flat-list/index.js +28 -78
  9. package/build/angular/components/flat-list/index.js.map +1 -1
  10. package/build/angular/components/image-shared.d.ts +8 -2
  11. package/build/angular/components/modal/index.d.ts +1 -0
  12. package/build/angular/components/modal/index.js +23 -16
  13. package/build/angular/components/modal/index.js.map +1 -1
  14. package/build/angular/components/section-list/index.d.ts +3 -1
  15. package/build/angular/components/section-list/index.js +10 -2
  16. package/build/angular/components/section-list/index.js.map +1 -1
  17. package/build/angular/components/virtualized-list/directives.js +7 -4
  18. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  19. package/build/angular/components/virtualized-list/index.d.ts +10 -2
  20. package/build/angular/components/virtualized-list/index.js +45 -25
  21. package/build/angular/components/virtualized-list/index.js.map +1 -1
  22. package/build/angular/components/virtualized-section-list/index.d.ts +5 -1
  23. package/build/angular/components/virtualized-section-list/index.js +10 -2
  24. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  25. package/build/angular/descriptor-to-angular/index.js +6 -3
  26. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  27. package/build/angular/element-props.d.ts +16 -5
  28. package/build/angular/elements.d.ts +5 -21
  29. package/build/angular/elements.js +18 -26
  30. package/build/angular/elements.js.map +1 -1
  31. package/build/angular/index.d.ts +0 -1
  32. package/build/angular/index.js +5 -7
  33. package/build/angular/index.js.map +1 -1
  34. package/build/angular/primitives/shared.js +5 -1
  35. package/build/angular/primitives/shared.js.map +1 -1
  36. package/build/angular/renderer/index.d.ts +11 -0
  37. package/build/angular/renderer/index.js +97 -12
  38. package/build/angular/renderer/index.js.map +1 -1
  39. package/build/angular/runtime-matching.d.ts +0 -1
  40. package/build/angular/runtime-matching.js +6 -29
  41. package/build/angular/runtime-matching.js.map +1 -1
  42. package/package.json +6 -6
  43. package/src/callback-host.ts +10 -43
  44. package/src/change-detection-flush.ts +67 -14
  45. package/src/components/flat-list/index.ts +22 -54
  46. package/src/components/modal/index.ts +22 -16
  47. package/src/components/section-list/index.ts +4 -0
  48. package/src/components/virtualized-list/directives.ts +10 -8
  49. package/src/components/virtualized-list/index.ts +38 -13
  50. package/src/components/virtualized-section-list/index.ts +6 -0
  51. package/src/descriptor-to-angular/index.ts +6 -3
  52. package/src/element-props.ts +22 -13
  53. package/src/elements.ts +21 -56
  54. package/src/index.ts +5 -7
  55. package/src/primitives/shared.ts +5 -1
  56. package/src/renderer/index.ts +107 -11
  57. package/src/runtime-matching.ts +6 -30
  58. package/build/angular/style-host.d.ts +0 -11
  59. package/build/angular/style-host.js +0 -86
  60. package/build/angular/style-host.js.map +0 -1
  61. package/src/style-host.ts +0 -94
@@ -13,9 +13,10 @@
13
13
  // flat-list.test.ts). Fixed for both column modes:
14
14
  // * Single column (numColumns <= 1): FlatList captures the app's `<ng-template vListItem>` (and
15
15
  // vListHeader/vListFooter/vListEmpty/vListSeparator) with its OWN @ContentChild — a single,
16
- // direct projection hop, which always resolves then re-authors equivalent `<ng-template>`s on
17
- // `<VirtualizedList>`, forwarding the captured templateRef + context through
18
- // VListOutletDirective. Item/index/separators pass through 1:1 (no row wrapping).
16
+ // direct projection hop, which always resolves. The per-cell item and separator templates go
17
+ // straight in through VirtualizedList's `[itemTemplate]`/`[itemSeparatorTemplate]`, so a cell
18
+ // is ONE outlet deep (a re-stamp doubled every cell's embedded views); the once-per-list
19
+ // header/footer/empty slots are still re-stamped.
19
20
  // * Multi column (numColumns > 1): same re-stamp, but the app's vListItem is typed for ItemT
20
21
  // while the virtualized stream is rows (IRow<ItemT>), so a plain passthrough couldn't work
21
22
  // regardless. The row vListItem lays out the N columns side by side (each cell stamps the
@@ -52,6 +53,7 @@ import {
52
53
  expandRowViewability,
53
54
  firstItemOfRow,
54
55
  lastItemOfRow,
56
+ removeClippedSubviewsOrDefault,
55
57
  rowKeyExtractor,
56
58
  type IRow,
57
59
  type IScrollViewHandle,
@@ -61,6 +63,7 @@ import {
61
63
  type IVirtualizedListHandle,
62
64
  } from '@symbiote-native/components';
63
65
  import {
66
+ Platform,
64
67
  dlog,
65
68
  flattenStyle,
66
69
  resolveClassName,
@@ -111,15 +114,6 @@ const NOOP_SEPARATORS: ISeparators = {
111
114
  updateProps: (): void => undefined,
112
115
  };
113
116
 
114
- // Angular cannot preserve VListSeparatorDirective<ItemT>'s type parameter across this re-stamp
115
- // reuse (the directive is matched structurally in FlatList's own template with no explicit type
116
- // argument to pin ItemT to, so `let-leadingItem`/`let-trailingItem` arrive typed `unknown`) — even
117
- // though VirtualizedList's own buildSeparatorContext always supplies a real ItemT value. The
118
- // narrowest possible I/O-boundary cast for that gap; not a general-purpose unknown-to-T narrowing.
119
- function asItem<ItemT>(value: unknown): ItemT | undefined {
120
- return value as ItemT | undefined;
121
- }
122
-
123
117
  // Re-export the shared list types + the authoring directives so the app can import the cell/slot
124
118
  // directives alongside FlatList (mirrors how virtualized-list/index re-exports them).
125
119
  export type {
@@ -226,6 +220,8 @@ export type IFlatListInputs<ItemT> = Omit<
226
220
  [scrollEventThrottle]="scrollEventThrottle"
227
221
  [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
228
222
  [keyboardDismissMode]="keyboardDismissMode"
223
+ [removeClippedSubviews]="resolvedRemoveClippedSubviews"
224
+ [nestedScrollEnabled]="nestedScrollEnabled"
229
225
  [testID]="testID"
230
226
  [style]="resolvedStyle"
231
227
  [contentContainerStyle]="contentContainerStyle"
@@ -310,21 +306,14 @@ export type IFlatListInputs<ItemT> = Omit<
310
306
  [scrollEventThrottle]="scrollEventThrottle"
311
307
  [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
312
308
  [keyboardDismissMode]="keyboardDismissMode"
309
+ [removeClippedSubviews]="resolvedRemoveClippedSubviews"
310
+ [nestedScrollEnabled]="nestedScrollEnabled"
313
311
  [testID]="testID"
314
312
  [style]="resolvedStyle"
315
313
  [contentContainerStyle]="contentContainerStyle"
314
+ [itemTemplate]="itemDir?.templateRef"
315
+ [itemSeparatorTemplate]="separatorDir?.templateRef"
316
316
  >
317
- <ng-template
318
- vListItem
319
- let-item
320
- let-index="index"
321
- let-separators="separators"
322
- >
323
- <ng-container
324
- [vListOutlet]="itemDir?.templateRef"
325
- [vListOutletContext]="{ $implicit: item, index, separators }"
326
- ></ng-container>
327
- </ng-template>
328
317
  @if (headerDir !== undefined) {
329
318
  <ng-template vListHeader>
330
319
  <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
@@ -340,21 +329,6 @@ export type IFlatListInputs<ItemT> = Omit<
340
329
  <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
341
330
  </ng-template>
342
331
  }
343
- @if (separatorDir !== undefined) {
344
- <ng-template
345
- vListSeparator
346
- let-highlighted="highlighted"
347
- let-leadingItem="leadingItem"
348
- let-trailingItem="trailingItem"
349
- >
350
- <ng-container
351
- [vListOutlet]="separatorDir.templateRef"
352
- [vListOutletContext]="
353
- itemSeparatorContext(highlighted, leadingItem, trailingItem)
354
- "
355
- ></ng-container>
356
- </ng-template>
357
- }
358
332
  </VirtualizedList>
359
333
  }
360
334
  `,
@@ -426,9 +400,19 @@ export class FlatList<ItemT = unknown>
426
400
  @Input() scrollEventThrottle?: number;
427
401
  @Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
428
402
  @Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
403
+ @Input() removeClippedSubviews?: boolean;
404
+ @Input() nestedScrollEnabled?: boolean;
429
405
  @Input() style?: IStyleProp<IViewStyle>;
430
406
  @Input() contentContainerStyle?: IStyleProp<IViewStyle>;
431
407
 
408
+ // FlatList.js always sends it, defaulted per platform.
409
+ get resolvedRemoveClippedSubviews(): boolean {
410
+ return removeClippedSubviewsOrDefault(
411
+ this.removeClippedSubviews,
412
+ Platform.OS,
413
+ );
414
+ }
415
+
432
416
  // The app's cell + slot templates, captured for the multi-column re-stamp path. In the
433
417
  // single-column path they are ALSO projected through <ng-content> to the inner list and these
434
418
  // captures stay unused (the inner list's own @ContentChild resolves them across the projection).
@@ -583,22 +567,6 @@ export class FlatList<ItemT = unknown>
583
567
  };
584
568
  }
585
569
 
586
- // Single-column separator context: leadingItem/trailingItem are already real items (no row
587
- // wrapper to narrow through — see the asItem boundary comment above).
588
- itemSeparatorContext(
589
- highlighted: unknown,
590
- leadingItem: unknown,
591
- trailingItem: unknown,
592
- ): IVListSeparatorContext<ItemT> {
593
- const isHighlighted = highlighted === true;
594
- return {
595
- $implicit: isHighlighted,
596
- highlighted: isHighlighted,
597
- leadingItem: asItem<ItemT>(leadingItem),
598
- trailingItem: asItem<ItemT>(trailingItem),
599
- };
600
- }
601
-
602
570
  // ---- imperative handle (RN FlatList surface) — delegates to the inner VirtualizedList ----
603
571
 
604
572
  scrollToOffset(params: { offset: number; animated?: boolean }): void {
@@ -2,10 +2,8 @@
2
2
  // through the same childSet as the rest of the tree (no second JS surface). The style math (the
3
3
  // backdrop override, the container/host styles, the presentationStyle default), the visible gate,
4
4
  // and the iOS keep-alive reducer all live framework-agnostic in @symbiote-native/components and are shared
5
- // verbatim with React/Vue; here Angular supplies only the lifecycle: the keep-alive state + a
6
- // POST-render transition (ngOnChanges queues the reducer on a microtask so it runs AFTER the render
7
- // that used the OLD state, the Angular twin of React's useEffect / Vue's post-flush watch — one
8
- // keep-alive frame survives the visible→hidden transition), reusing renderModal's resolved props.
5
+ // verbatim with React/Vue; here Angular supplies only the lifecycle: the keep-alive state, armed on
6
+ // show (ngOnChanges) and dropped by the native dismiss on iOS, reusing renderModal's resolved props.
9
7
  // The user children nest UNDER the container View via <ng-content>.
10
8
 
11
9
  import {
@@ -27,7 +25,9 @@ import {
27
25
  } from '@angular/core';
28
26
  import {
29
27
  createInitialModalState,
28
+ isModalVisible,
30
29
  modalReducer,
30
+ modalVisibilityAction,
31
31
  renderModal,
32
32
  resolveAccessibilityProps,
33
33
  shouldRenderModal,
@@ -42,6 +42,7 @@ import {
42
42
  import {
43
43
  dlog,
44
44
  isSymbioteEvent,
45
+ Platform,
45
46
  type IStyleProp,
46
47
  type ISymbioteEvent,
47
48
  type IViewStyle,
@@ -112,7 +113,7 @@ export type IAngularModalInputs = Omit<
112
113
  <modal
113
114
  [symbioteHostProps]="hostProps()"
114
115
  (show)="show.emit()"
115
- (dismiss)="dismiss.emit()"
116
+ (dismiss)="handleDismiss()"
116
117
  (requestClose)="requestClose.emit()"
117
118
  (orientationChange)="emit(orientationChange, $event)"
118
119
  >
@@ -180,8 +181,8 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
180
181
  @Input('aria-valuenow') ariaValueNow?: number;
181
182
  @Input('aria-valuetext') ariaValueText?: string;
182
183
 
183
- // The iOS keep-alive (state/modal.ts): on visible→hidden the node renders one more frame
184
- // (isRendered still true) before unmounting, so the native onDismiss can arrive.
184
+ // The iOS keep-alive (state/modal.ts): after visible→hidden the node stays until the native
185
+ // onDismiss (`handleDismiss`) drops it.
185
186
  private state: IModalState = createInitialModalState(false);
186
187
 
187
188
  private readonly changeDetector = inject(ChangeDetectorRef);
@@ -192,7 +193,7 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
192
193
  private readonly elementRef = inject(ElementRef);
193
194
 
194
195
  ngOnInit(): void {
195
- this.state = createInitialModalState(this.visible === true);
196
+ this.state = createInitialModalState(isModalVisible(this.visible));
196
197
  }
197
198
 
198
199
  // Bridges the non-reactive @Input fields `hostProps` reads into the reactive graph, so it can
@@ -212,20 +213,25 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
212
213
  const visibleChange = changes.visible;
213
214
  // First change is reflected by the ngOnInit seed; only later toggles drive the keep-alive.
214
215
  if (visibleChange === undefined || visibleChange.firstChange) return;
215
- const isVisible = this.visible === true;
216
- // Queue on a microtask so the reducer runs AFTER this CD pass renders with the OLD state —
217
- // the keep-alive frame. A synchronous dispatch here would unmount in the same pass.
216
+ // Arms the iOS keep-alive on show; a hide is left to the native dismiss (state/modal.ts).
217
+ const action = modalVisibilityAction(isModalVisible(this.visible));
218
+ if (action === undefined) return;
218
219
  queueMicrotask(() => {
219
- this.state = modalReducer(
220
- this.state,
221
- isVisible ? { type: 'show' } : { type: 'hide' },
222
- );
220
+ this.state = modalReducer(this.state, action);
223
221
  this.changeDetector.markForCheck();
224
222
  });
225
223
  }
226
224
 
225
+ // Modal.js: onDismiss is iOS-only — it drops the keep-alive, then tells the app.
226
+ handleDismiss(): void {
227
+ if (Platform.OS !== 'ios') return;
228
+ this.state = modalReducer(this.state, { type: 'hide' });
229
+ this.changeDetector.markForCheck();
230
+ this.dismiss.emit();
231
+ }
232
+
227
233
  get shouldRender(): boolean {
228
- const render = shouldRenderModal(this.visible === true, this.state);
234
+ const render = shouldRenderModal(isModalVisible(this.visible), this.state);
229
235
  if (!render) dlog('Modal hidden -> no node committed');
230
236
  return render;
231
237
  }
@@ -177,6 +177,8 @@ export type ISectionListInputs<ItemT> = Omit<
177
177
  [scrollEventThrottle]="scrollEventThrottle"
178
178
  [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
179
179
  [keyboardDismissMode]="keyboardDismissMode"
180
+ [removeClippedSubviews]="removeClippedSubviews"
181
+ [nestedScrollEnabled]="nestedScrollEnabled"
180
182
  [style]="resolvedStyle"
181
183
  [contentContainerStyle]="contentContainerStyle"
182
184
  [testID]="testID"
@@ -343,6 +345,8 @@ export class SectionList<ItemT = unknown>
343
345
  @Input() scrollEventThrottle?: number;
344
346
  @Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
345
347
  @Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
348
+ @Input() removeClippedSubviews?: boolean;
349
+ @Input() nestedScrollEnabled?: boolean;
346
350
  @Input() style?: IStyleProp<IViewStyle>;
347
351
  @Input() contentContainerStyle?: IStyleProp<IViewStyle>;
348
352
  @Input() testID?: string;
@@ -35,7 +35,7 @@ import {
35
35
  type OnDestroy,
36
36
  type SimpleChanges,
37
37
  } from '@angular/core';
38
- import { dlog } from '@symbiote-native/engine';
38
+ import { dlog, isDebug } from '@symbiote-native/engine';
39
39
  import { countAngular } from '../../diagnostics';
40
40
  import type { ISeparators } from '@symbiote-native/components';
41
41
 
@@ -129,9 +129,10 @@ export class VListOutletDirective<C = unknown> implements OnChanges, OnDestroy {
129
129
 
130
130
  ngOnChanges(changes: SimpleChanges): void {
131
131
  if (changes['templateRef'] !== undefined) {
132
- dlog(
133
- `Angular VListOutlet#${this.instanceId} templateRef CHANGED (was=${changes['templateRef'].previousValue !== undefined} now=${this.templateRef !== undefined}) -> clear + recreate`,
134
- );
132
+ if (isDebug())
133
+ dlog(
134
+ `Angular VListOutlet#${this.instanceId} templateRef CHANGED (was=${changes['templateRef'].previousValue !== undefined} now=${this.templateRef !== undefined}) -> clear + recreate`,
135
+ );
135
136
  this.viewContainer.clear();
136
137
  countAngular('outletCreates');
137
138
  this.viewRef =
@@ -145,9 +146,10 @@ export class VListOutletDirective<C = unknown> implements OnChanges, OnDestroy {
145
146
  }
146
147
  if (this.viewRef !== null && this.context !== undefined) {
147
148
  countAngular('outletUpdates');
148
- dlog(
149
- `Angular VListOutlet#${this.instanceId} context updated, markForCheck`,
150
- );
149
+ if (isDebug())
150
+ dlog(
151
+ `Angular VListOutlet#${this.instanceId} context updated, markForCheck`,
152
+ );
151
153
  this.updateContext(this.viewRef.context, this.context);
152
154
  this.viewRef.markForCheck();
153
155
  }
@@ -155,7 +157,7 @@ export class VListOutletDirective<C = unknown> implements OnChanges, OnDestroy {
155
157
 
156
158
  ngOnDestroy(): void {
157
159
  countAngular('outletDestroys');
158
- dlog(`Angular VListOutlet#${this.instanceId} destroyed`);
160
+ if (isDebug()) dlog(`Angular VListOutlet#${this.instanceId} destroyed`);
159
161
  this.viewContainer.clear();
160
162
  }
161
163
 
@@ -34,6 +34,7 @@ import {
34
34
  EventEmitter,
35
35
  Input,
36
36
  Output,
37
+ TemplateRef,
37
38
  ViewChild,
38
39
  computed,
39
40
  inject,
@@ -183,6 +184,8 @@ export interface IVirtualizedListProps<ItemT>
183
184
  scrollEventThrottle?: number;
184
185
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
185
186
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
187
+ removeClippedSubviews?: boolean;
188
+ nestedScrollEnabled?: boolean;
186
189
  style?: IStyleProp<IViewStyle>;
187
190
  contentContainerStyle?: IStyleProp<IViewStyle>;
188
191
  }
@@ -267,7 +270,7 @@ interface IWindowCell<ItemT> {
267
270
  [style]="cellStyle"
268
271
  >
269
272
  <ng-container
270
- [vListOutlet]="itemDir?.templateRef"
273
+ [vListOutlet]="cellTemplate"
271
274
  [vListOutletContext]="forcedStickyCell.context"
272
275
  ></ng-container>
273
276
  </sticky-header>
@@ -282,12 +285,12 @@ interface IWindowCell<ItemT> {
282
285
  [style]="cellStyle"
283
286
  >
284
287
  <ng-container
285
- [vListOutlet]="itemDir?.templateRef"
288
+ [vListOutlet]="cellTemplate"
286
289
  [vListOutletContext]="cell.context"
287
290
  ></ng-container>
288
291
  @if (cell.separatorContext !== undefined) {
289
292
  <ng-container
290
- [vListOutlet]="separatorDir?.templateRef"
293
+ [vListOutlet]="separatorTemplate"
291
294
  [vListOutletContext]="cell.separatorContext"
292
295
  ></ng-container>
293
296
  }
@@ -298,12 +301,12 @@ interface IWindowCell<ItemT> {
298
301
  [style]="cellStyle"
299
302
  >
300
303
  <ng-container
301
- [vListOutlet]="itemDir?.templateRef"
304
+ [vListOutlet]="cellTemplate"
302
305
  [vListOutletContext]="cell.context"
303
306
  ></ng-container>
304
307
  @if (cell.separatorContext !== undefined) {
305
308
  <ng-container
306
- [vListOutlet]="separatorDir?.templateRef"
309
+ [vListOutlet]="separatorTemplate"
307
310
  [vListOutletContext]="cell.separatorContext"
308
311
  ></ng-container>
309
312
  }
@@ -351,7 +354,7 @@ interface IWindowCell<ItemT> {
351
354
  [style]="cellStyle"
352
355
  >
353
356
  <ng-container
354
- [vListOutlet]="itemDir?.templateRef"
357
+ [vListOutlet]="cellTemplate"
355
358
  [vListOutletContext]="forcedStickyCell.context"
356
359
  ></ng-container>
357
360
  </sticky-header>
@@ -373,12 +376,12 @@ interface IWindowCell<ItemT> {
373
376
  [style]="cellStyle"
374
377
  >
375
378
  <ng-container
376
- [vListOutlet]="itemDir?.templateRef"
379
+ [vListOutlet]="cellTemplate"
377
380
  [vListOutletContext]="cell.context"
378
381
  ></ng-container>
379
382
  @if (cell.separatorContext !== undefined) {
380
383
  <ng-container
381
- [vListOutlet]="separatorDir?.templateRef"
384
+ [vListOutlet]="separatorTemplate"
382
385
  [vListOutletContext]="cell.separatorContext"
383
386
  ></ng-container>
384
387
  }
@@ -389,12 +392,12 @@ interface IWindowCell<ItemT> {
389
392
  [style]="cellStyle"
390
393
  >
391
394
  <ng-container
392
- [vListOutlet]="itemDir?.templateRef"
395
+ [vListOutlet]="cellTemplate"
393
396
  [vListOutletContext]="cell.context"
394
397
  ></ng-container>
395
398
  @if (cell.separatorContext !== undefined) {
396
399
  <ng-container
397
- [vListOutlet]="separatorDir?.templateRef"
400
+ [vListOutlet]="separatorTemplate"
398
401
  [vListOutletContext]="cell.separatorContext"
399
402
  ></ng-container>
400
403
  }
@@ -500,6 +503,8 @@ export class VirtualizedList<ItemT = unknown>
500
503
  @Input() scrollEventThrottle?: number;
501
504
  @Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
502
505
  @Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
506
+ @Input() removeClippedSubviews?: boolean;
507
+ @Input() nestedScrollEnabled?: boolean;
503
508
  @Input() style?: IStyleProp<IViewStyle>;
504
509
  @Input() contentContainerStyle?: IStyleProp<IViewStyle>;
505
510
  @Input() testID?: string;
@@ -550,6 +555,20 @@ export class VirtualizedList<ItemT = unknown>
550
555
  @ContentChild(VListSeparatorDirective)
551
556
  separatorDir?: VListSeparatorDirective<ItemT>;
552
557
 
558
+ // A wrapping list (FlatList) hands its app's cell/separator templates straight in, so a cell is
559
+ // one outlet deep instead of a wrapper outlet around the app's own; each wins over projection.
560
+ @Input() itemTemplate?: TemplateRef<IVListItemContext<ItemT>>;
561
+ @Input() itemSeparatorTemplate?: TemplateRef<IVListSeparatorContext<ItemT>>;
562
+
563
+ get cellTemplate(): TemplateRef<IVListItemContext<ItemT>> | undefined {
564
+ return this.itemTemplate ?? this.itemDir?.templateRef;
565
+ }
566
+
567
+ get separatorTemplate():
568
+ TemplateRef<IVListSeparatorContext<ItemT>> | undefined {
569
+ return this.itemSeparatorTemplate ?? this.separatorDir?.templateRef;
570
+ }
571
+
553
572
  // The inner scroll TAG's own engine node — a template ref on a bare intrinsic hands back the
554
573
  // host node directly (`isSymbioteNode(elementRef.nativeElement)`, matching `anchorHostStyle`'s
555
574
  // own reader below), so there is no wrapper component left to read a handle off. Present from
@@ -739,6 +758,11 @@ export class VirtualizedList<ItemT = unknown>
739
758
  scrollEventThrottle: this.scrollEventThrottle,
740
759
  keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
741
760
  keyboardDismissMode: this.keyboardDismissMode,
761
+ // RN's VirtualizedList spreads its props onto the ScrollView, this one included.
762
+ removeClippedSubviews: this.removeClippedSubviews,
763
+ nestedScrollEnabled: this.nestedScrollEnabled,
764
+ // VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
765
+ isInvertedVirtualizedList: this.isInverted ? true : undefined,
742
766
  contentOffset: this.commandedOffset,
743
767
  // `stickyHeaderIndices` is deliberately NOT forwarded — it numbers the scroll view's PAINT
744
768
  // children, and a windowed list paints a header, a spacer and a slice, so the positions move
@@ -1041,7 +1065,7 @@ export class VirtualizedList<ItemT = unknown>
1041
1065
  this.headerDir !== undefined,
1042
1066
  this.footerDir !== undefined,
1043
1067
  this.emptyDir !== undefined,
1044
- this.separatorDir !== undefined,
1068
+ this.separatorTemplate !== undefined,
1045
1069
  ];
1046
1070
  const previous = this.lastRecompute;
1047
1071
  this.lastRecompute = recomputeInputs;
@@ -1076,7 +1100,7 @@ export class VirtualizedList<ItemT = unknown>
1076
1100
  const m = this.listState.metrics;
1077
1101
  this.itemCount = m.count;
1078
1102
  const hasHeader = this.headerDir !== undefined;
1079
- const hasSeparators = this.separatorDir !== undefined;
1103
+ const hasSeparators = this.separatorTemplate !== undefined;
1080
1104
  const stickySet =
1081
1105
  this.stickyHeaderIndices !== undefined
1082
1106
  ? new Set(this.stickyHeaderIndices)
@@ -1087,8 +1111,9 @@ export class VirtualizedList<ItemT = unknown>
1087
1111
  : this.contentContainerStyle;
1088
1112
  this.resolvedStyle = flattenStyle([
1089
1113
  anchorHostStyle(this.elementRef),
1114
+ // VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
1090
1115
  this.isInverted
1091
- ? [this.style, this.isHorizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE]
1116
+ ? [this.isHorizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE, this.style]
1092
1117
  : this.style,
1093
1118
  ]);
1094
1119
  this.cellStyle = this.isInverted
@@ -146,6 +146,8 @@ export interface IVirtualizedSectionListProps<ItemT>
146
146
  scrollEventThrottle?: number;
147
147
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
148
148
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
149
+ removeClippedSubviews?: boolean;
150
+ nestedScrollEnabled?: boolean;
149
151
  style?: IStyleProp<IViewStyle>;
150
152
  contentContainerStyle?: IStyleProp<IViewStyle>;
151
153
  }
@@ -213,6 +215,8 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
213
215
  [scrollEventThrottle]="scrollEventThrottle"
214
216
  [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
215
217
  [keyboardDismissMode]="keyboardDismissMode"
218
+ [removeClippedSubviews]="removeClippedSubviews"
219
+ [nestedScrollEnabled]="nestedScrollEnabled"
216
220
  [style]="resolvedStyle"
217
221
  [contentContainerStyle]="contentContainerStyle"
218
222
  [testID]="testID"
@@ -375,6 +379,8 @@ export class VirtualizedSectionList<ItemT = unknown>
375
379
  @Input() scrollEventThrottle?: number;
376
380
  @Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
377
381
  @Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
382
+ @Input() removeClippedSubviews?: boolean;
383
+ @Input() nestedScrollEnabled?: boolean;
378
384
  @Input() style?: IStyleProp<IViewStyle>;
379
385
  @Input() contentContainerStyle?: IStyleProp<IViewStyle>;
380
386
  @Input() testID?: string;
@@ -86,8 +86,10 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
86
86
  private createElement(descriptor: IDescriptor): IRenderedElement {
87
87
  const node = this.renderer.createElement(descriptor.type);
88
88
  const props = { ...descriptor.props };
89
- for (const [key, value] of Object.entries(props)) {
90
- this.renderer.setProperty(node, key, value);
89
+ // `Object.keys` rather than `Object.entries` — `entries` allocates a pair array per key up front,
90
+ // and this runs once per rendered descriptor element. See `object-iteration-cost.itest.ts`.
91
+ for (const key of Object.keys(props)) {
92
+ this.renderer.setProperty(node, key, props[key]);
91
93
  }
92
94
 
93
95
  const children = descriptor.children.map(child => this.createChild(child));
@@ -155,7 +157,8 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
155
157
  this.renderer.setProperty(rendered.node, key, undefined);
156
158
  }
157
159
  }
158
- for (const [key, value] of Object.entries(nextProps)) {
160
+ for (const key of Object.keys(nextProps)) {
161
+ const value = nextProps[key];
159
162
  if (!Object.is(prevProps[key], value)) {
160
163
  this.renderer.setProperty(rendered.node, key, value);
161
164
  }
@@ -8,13 +8,9 @@
8
8
  // differ, since a bare tag takes children from the template and a handle from `#ref`, so neither
9
9
  // `children` nor `ref` appears here.
10
10
  //
11
- // `style` IS declared, reversing what this comment said until 2026-09-11. Leaving it to Angular's
12
- // styling engine was not the conservative choice it reads as: an array decomposes into
13
- // numeric-index keys there and a press-state CALLBACK throws out of `toStylingKeyValueArray`,
14
- // aborting the enclosing template update — which is what an app writes, and what every deleted
15
- // wrapper component accepted. The claim that a directive can reclaim it "solely by executing a
16
- // linked AOT artifact" was answered rather than assumed: `elements.test.ts` compiles the binding
17
- // through real ngtsc and runs the LINKED output. See `SymbioteElement.style`.
11
+ // `style` and `styleProp` are TWO bindings. `[style]` is Angular's styling binding: an object or CSS
12
+ // string, as on a DOM element, unclaimed because a claim is a directive instance per element. An RN
13
+ // array or press-state callback throws in that engine, so it travels as `[styleProp]`.
18
14
  import type {
19
15
  IAccessibilityProps,
20
16
  IAriaProps,
@@ -31,6 +27,17 @@ import type {
31
27
 
32
28
  type IEventHandler = (event: ISymbioteEvent) => void;
33
29
 
30
+ /**
31
+ * What `[style]` carries on a tag: Angular's own styling binding, decomposed key by key into
32
+ * `Renderer2.setStyle` exactly as on a DOM element. A style OBJECT or a CSS string - never an array
33
+ * or a function, which that engine cannot represent. Those are `[styleProp]`.
34
+ */
35
+ export type IAngularStyleBinding<TStyle> = TStyle | string | null | undefined;
36
+
37
+ /** RN's full StyleProp, press-state callback included: the `[styleProp]` binding. */
38
+ export type IElementStyleProp =
39
+ IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
40
+
34
41
  export interface IElementProps
35
42
  extends IAccessibilityProps, IAriaProps, IResponderProps {
36
43
  /** RN's modern W3C-named alias for `nativeID`; the renderer folds it (`PROP_ALIASES`). */
@@ -43,13 +50,14 @@ export interface IElementProps
43
50
  renderToHardwareTextureAndroid?: boolean;
44
51
  shouldRasterizeIOS?: boolean;
45
52
  needsOffscreenAlphaCompositing?: boolean;
53
+ style?: IAngularStyleBinding<IViewStyle>;
46
54
  /**
47
- * The press-state callback is in the union because a subclass cannot widen an
48
- * inherited property and `<pressable>`/`<touchable-*>` are the tags that take one; on every other
49
- * tag the engine resolves it at `pressed: false`.
55
+ * An RN StyleProp as ONE property binding: arrays, falsy entries, and the press-state callback.
56
+ * The callback is in the union because a subclass cannot widen an inherited property and
57
+ * `<pressable>`/`<touchable-*>` are the tags that take one; on every other tag the engine resolves
58
+ * it at `pressed: false`.
50
59
  */
51
- style?:
52
- IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
60
+ styleProp?: IElementStyleProp;
53
61
  onPress?: IEventHandler;
54
62
  onPressIn?: IEventHandler;
55
63
  onPressOut?: IEventHandler;
@@ -63,7 +71,8 @@ export interface IElementProps
63
71
  /** Text's own surface on top of the shared one. `onTextLayout` is an event, so it is not here. */
64
72
  export interface ITextElementProps {
65
73
  /** Narrows the shared `style` so `fontSize` / `fontWeight` type-check on a `<text>`. */
66
- style?: IStyleProp<ITextStyle>;
74
+ style?: IAngularStyleBinding<ITextStyle>;
75
+ styleProp?: IStyleProp<ITextStyle>;
67
76
  numberOfLines?: number;
68
77
  ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
69
78
  selectable?: boolean;