@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
package/src/elements.ts CHANGED
@@ -58,7 +58,6 @@ import {
58
58
  } from './change-detection-flush';
59
59
  import { SymbioteCallbackHost } from './callback-host';
60
60
  import { withholdFromRuntimeMatching } from './runtime-matching';
61
- import { SymbioteStyleHost } from './style-host';
62
61
  import type {
63
62
  IActivityIndicatorProps,
64
63
  IImageProps,
@@ -190,44 +189,21 @@ export abstract class SymbioteElement implements OnChanges {
190
189
  @Input() shouldRasterizeIOS?: IElementProps['shouldRasterizeIOS'];
191
190
  @Input()
192
191
  needsOffscreenAlphaCompositing?: IElementProps['needsOffscreenAlphaCompositing'];
193
- // Declared, and the reason is the one `element-props.ts` used to give for NOT declaring it —
194
- // reversed by measurement. `[style]` on an element with no directive input reaches Angular's own
195
- // CSS styling engine (`ɵɵstyleMap`), which cannot represent an RN StyleProp: an ARRAY decomposes
196
- // into numeric-index keys and a FUNCTION — the press-state callback this ecosystem writes dies
197
- // in `toStylingKeyValueArray` as "ASSERTION ERROR: Unsupported styling type: function", taking
198
- // the whole enclosing template update with it. Device-reported 2026-09-11: every `ActionButton`
199
- // in examples/angular painted as an empty bordered box, because the throw aborted the update
200
- // before its `<text>` child was ever reached.
201
- //
202
- // A declared input CLAIMS the binding at compile time, so it never reaches the styling engine —
203
- // the same mechanism the primitive host COMPONENTS have always relied on
204
- // (`primitives/shared.ts`), which is why `<Pressable [style]>` worked and its replacement tag did
205
- // not. `[style.borderWidth.px]` is a different instruction (`ɵɵstyleProp`) and is untouched: it
206
- // still reaches `Renderer2.setStyle`, which merges per key.
192
+ // `[style]` and `[class]` are declared for the TYPE CHECK only. Withheld from run-time matching, no
193
+ // input claims them, so Angular's own styling engine decomposes them into `setStyle`/`addClass`
194
+ // exactly as on a DOM element - which is why `style` is typed as the object/string that engine can
195
+ // hold. An RN style ARRAY or press-state FUNCTION throws in there ("indexOf is not a function",
196
+ // "Unsupported styling type: function"), so it is `[styleProp]`: an ordinary property binding the
197
+ // renderer routes to `style` whole. A claim would cost a directive instance per element.
207
198
  //
208
199
  // The press-state callback rides the BASE type rather than `PressableElement` alone: a subclass
209
200
  // cannot widen an inherited property, and only the pressables have a `pressed` to read — so on
210
- // any other tag the engine resolves it at `pressed: false` and the looseness is in the type, not
211
- // in what commits.
201
+ // any other tag the engine resolves it at `pressed: false`.
212
202
  @Input() style?: IElementProps['style'];
203
+ @Input() styleProp?: IElementProps['styleProp'];
213
204
 
214
- /**
215
- * Declared so Angular SHADOWS the `[class]` binding into it instead of decomposing the string.
216
- *
217
- * A styling binding goes to a directive input when the directive declares that exact public name
218
- * — `setShadowStylingInputFlags` sets `hasClassInput`/`hasStyleInput` off the input map, and
219
- * `checkStylingMap` then hands the whole value over and never calls the renderer per key
220
- * (`view/directives.ts`, `instructions/styling.ts`). `style` has been declared here all along and
221
- * is shadowed; `class` was not, so every `[class]` reached the renderer one TOKEN at a time — on
222
- * the element-directive path as much as the bare one. Measured at ~3.3 us per class binding
223
- * (`adapter-create-cost.itest.ts`, "prices the class channel"), on the channel every example app
224
- * uses for its static look.
225
- *
226
- * `[class.foo]` and `[ngClass]` are NOT shadowed and keep arriving as `addClass`, so a node can
227
- * now be told its classes both ways at once; the renderer unions the two sources
228
- * (`classStringFor`). Typed as a string rather than RN's `className`, because that is what
229
- * `ɵɵclassMap` hands over after it concatenates any static `class=` prefix.
230
- */
205
+ // `ɵɵclassMap` hands over a string after joining any static `class=` prefix; `[class.foo]` and
206
+ // `[ngClass]` arrive as `addClass`. The renderer unions both sources (`classStringFor`).
231
207
  @Input() class?: string;
232
208
 
233
209
  // The flat-bag spelling of the events below. `(press)` and `[onPress]` are both supported and
@@ -422,6 +398,7 @@ export class TextElement extends SymbioteElement {
422
398
  // initializer is what TS2612 asks for to accept a redeclaration as deliberate; `declare` would
423
399
  // be the other answer and cannot carry a decorator.
424
400
  @Input() override style?: ITextElementProps['style'] = undefined;
401
+ @Input() override styleProp?: ITextElementProps['styleProp'] = undefined;
425
402
  @Input() numberOfLines?: ITextElementProps['numberOfLines'];
426
403
  @Input() ellipsizeMode?: ITextElementProps['ellipsizeMode'];
427
404
  @Input() selectable?: ITextElementProps['selectable'];
@@ -927,39 +904,27 @@ export const SYMBIOTE_ELEMENTS = [
927
904
  // bind one and carries the `ChangeDetectorRef` their wrapper needs. It rides this list for the same
928
905
  // reason the accessors do, and `elements.test.ts` subtracts it from the tag-coverage check by name.
929
906
  SymbioteCallbackHost,
930
- // The other attribute-matched one: it claims `[style]` and `[class]` so an RN style ARRAY never
931
- // reaches Angular's styling engine, which cannot represent one and throws. See `style-host.ts`.
932
- SymbioteStyleHost,
933
907
  ] as const;
934
908
 
935
909
  // THE DIRECTIVES THAT GO ON MATCHING, and the list is the exceptions rather than the rule.
936
910
  //
937
- // A tag directive here is, with four exceptions, nothing but `@Input()` declarations over one
938
- // inherited `ngOnChanges` that forwards each of them to the renderer which is the call
939
- // `ɵɵproperty` makes directly on an element nothing claimed. So the instance buys a compile-time
940
- // check at ~8.5-9.4 us of run time per element, and `./runtime-matching` keeps the check while
941
- // dropping the instance.
942
- //
943
- // These four cannot go, because they DO something when they are built:
911
+ // EVERY TAG DIRECTIVE IS WITHHELD. Each is `@Input()` declarations over one inherited `ngOnChanges`
912
+ // forwarding to the renderer (the call `ɵɵproperty` makes directly on an unclaimed element), so the
913
+ // instance bought a compile-time check at ~8 us of run time per element.
944
914
  //
945
- // text-input, text-input-multiline, switch `ValueChangeElement` listens for the engine's value
946
- // event and registers a view flush, which is what stops
947
- // a controlled value being undone inside one microtask
948
- // refresh-control `ReadBackElement`, the same flush for the same reason
915
+ // The read-back tags (text-input, text-input-multiline, switch, refresh-control) need a synchronous
916
+ // view flush; the renderer marks them at creation and the flush finds their view lazily
917
+ // (`change-detection-flush.ts`). `[(value)]` reaches the renderer's `listen('valueChange')` bridge.
949
918
  //
950
- // The two form accessors and `SymbioteCallbackHost` are absent from both lists deliberately: they
951
- // match on an ATTRIBUTE rather than a tag, so they already land only where they are needed.
919
+ // `[style]`/`[class]` stay unclaimed as on a DOM element (see `SymbioteElement.style`). What still
920
+ // matches is selected on an ATTRIBUTE an app writes: the two form accessors and
921
+ // `SymbioteCallbackHost`.
952
922
  withholdFromRuntimeMatching(
953
923
  SYMBIOTE_ELEMENTS.filter(
954
924
  directive =>
955
- directive !== TextInputElement &&
956
- directive !== MultilineTextInputElement &&
957
- directive !== SwitchElement &&
958
- directive !== RefreshControlElement &&
959
925
  directive !== TextInputValueAccessor &&
960
926
  directive !== SwitchValueAccessor &&
961
- directive !== SymbioteCallbackHost &&
962
- directive !== SymbioteStyleHost,
927
+ directive !== SymbioteCallbackHost,
963
928
  ),
964
929
  );
965
930
 
package/src/index.ts CHANGED
@@ -9,8 +9,8 @@
9
9
  // `@Component`s on the `view` and `text` SELECTORS — a second mechanism on the same tags
10
10
  // `SYMBIOTE_ELEMENTS` covers, kept for one stated reason their own header gives: "Declaring `style`
11
11
  // as a real Angular input prevents Angular's CSS style engine from decomposing RN `StyleProp`
12
- // arrays". `SymbioteStyleHost` does exactly that now, for every tag, at a directive's cost rather
13
- // than a component's.
12
+ // arrays". A tag takes an array or press-state callback as `[styleProp]`, a plain property binding
13
+ // with no instance behind it.
14
14
  //
15
15
  // An app loses nothing it cannot spell better: `@ViewChild('ref')` on a template reference returns
16
16
  // Angular's own `ElementRef<IHostInstance>`, whose `nativeElement` IS the engine host node — the same
@@ -146,10 +146,9 @@ export { setImageSourceResolver } from './components';
146
146
  //
147
147
  // They were removed on 2026-09-18 and put back the same hour. The reason to remove them is real: a
148
148
  // withheld tag directive (`./runtime-matching`) is a compile-time declaration and nothing else, so
149
- // what makes `<view [style]="[a, b]">` work at RUN time is `SymbioteStyleHost`, and what makes
150
- // `<view [onPress]="fn">` work at all is `SymbioteCallbackHost` claiming the name before
151
- // `setDomProperty` throws NG0306. Both ride this array. A narrow `imports: [ViewElement]` type-checks
152
- // and then fails on a device the first time the app writes an array style.
149
+ // what makes `<view [onPress]="fn">` work at all is `SymbioteCallbackHost` claiming the name before
150
+ // `setDomProperty` throws NG0306. It rides this array, so a narrow `imports: [ViewElement]`
151
+ // type-checks and then fails on a device the first time the app binds an `on*` prop.
153
152
  //
154
153
  // WHAT PUTS THEM BACK is NG3004: `Unable to import directive ViewElement — the symbol is not
155
154
  // exported from index.d.ts`. ngtsc resolves every directive reachable through an imported array to an
@@ -198,7 +197,6 @@ export {
198
197
  CALLBACK_ATTRIBUTE_SELECTOR,
199
198
  SymbioteCallbackHost,
200
199
  } from './callback-host';
201
- export { STYLE_HOST_SELECTOR, SymbioteStyleHost } from './style-host';
202
200
  export type {
203
201
  IElementProps,
204
202
  IStickyHeaderElementProps,
@@ -298,7 +298,11 @@ export class SymbioteHostPropsDirective {
298
298
  // Only keys the node actually carries, so the vanished-key sweep below walks a handful rather
299
299
  // than the bag's whole declared shape.
300
300
  const next: Record<string, unknown> = {};
301
- for (const [key, value] of Object.entries(props)) {
301
+ // `Object.keys` rather than `Object.entries`: `entries` allocates a two-element array per key
302
+ // before the loop starts, and this walk runs once per element instance. Measured on `-O` Hermes
303
+ // (`object-iteration-cost.itest.ts`), 0.19 us per node over a four-key bag.
304
+ for (const key of Object.keys(props)) {
305
+ const value = props[key];
302
306
  if (value !== undefined) next[key] = value;
303
307
  if (Object.is(pushed[key], value)) continue;
304
308
  this.renderer.setProperty(
@@ -81,9 +81,23 @@ import {
81
81
  createCallbackWrapper,
82
82
  flushViewFor,
83
83
  isWrappableCallback,
84
+ markReadBackNode,
84
85
  type ICallbackWrapper,
85
86
  } from '../change-detection-flush';
86
87
 
88
+ // The tags whose behavior reads the app's answer back inside the event's own turn - the three
89
+ // behaviors `../change-detection-flush` names, plus the multiline spelling of the input. Marked at
90
+ // creation so a flush finds their view lazily instead of a directive instance per element.
91
+ const READ_BACK_TAGS: ReadonlySet<string> = new Set([
92
+ 'text-input',
93
+ 'text-input-multiline',
94
+ 'switch',
95
+ 'refresh-control',
96
+ ]);
97
+
98
+ // The property binding an RN StyleProp travels as; see `SymbioteElement.style`.
99
+ const STYLE_PROP_BINDING = 'styleProp';
100
+
87
101
  // The app callbacks an engine behavior READS BACK inside the same microtask turn, as an Angular
88
102
  // `(event)` binding — `valueChange` is handled in `listen` on its own, since it also needs the field
89
103
  // unwrapped. Zoneless change detection is a macrotask, so without a flush the behavior reads the
@@ -253,6 +267,13 @@ export class SymbioteRenderer implements Renderer2 {
253
267
  // free of a second guard.
254
268
  private pendingStyleNode: ISymbioteNode | undefined;
255
269
  private pendingStyle: Record<string, unknown> = {};
270
+ // A node with no standing style is matched against a published style key by key, as Angular hands
271
+ // the keys over, and builds nothing while they match. Rows sharing a style then skip the
272
+ // accumulator and the shallow compare's two key arrays (~320 B a `view` on create). The first
273
+ // mismatch materializes the accumulator from the keys matched so far.
274
+ private matchCandidate: Record<string, unknown> | undefined;
275
+ private matchCandidateSize = 0;
276
+ private readonly matchedKeys: string[] = [];
256
277
  private pendingClassNode: ISymbioteNode | undefined;
257
278
  private readonly releaseBeforeFlush: () => void;
258
279
 
@@ -303,10 +324,20 @@ export class SymbioteRenderer implements Renderer2 {
303
324
  private flushStyling(): void {
304
325
  const styled = this.pendingStyleNode;
305
326
  if (styled !== undefined) {
306
- const style = this.pendingStyle;
307
327
  this.pendingStyleNode = undefined;
308
- this.pendingStyle = {};
309
- routeProp(styled, 'style', this.canonicalStyle(style));
328
+ const candidate = this.matchCandidate;
329
+ if (
330
+ candidate !== undefined &&
331
+ this.matchedKeys.length === this.matchCandidateSize
332
+ ) {
333
+ this.matchCandidate = undefined;
334
+ routeProp(styled, 'style', candidate);
335
+ } else {
336
+ this.materializeMatch();
337
+ const style = this.pendingStyle;
338
+ this.pendingStyle = {};
339
+ routeProp(styled, 'style', this.canonicalStyle(style));
340
+ }
310
341
  }
311
342
  const classed = this.pendingClassNode;
312
343
  if (classed !== undefined) {
@@ -362,6 +393,8 @@ export class SymbioteRenderer implements Renderer2 {
362
393
  * this simply stops sharing — it never stops being correct.
363
394
  */
364
395
  private readonly publishedStyles: Record<string, unknown>[] = [];
396
+ // Key count of each entry above, index for index: how a key-by-key match knows it is complete.
397
+ private readonly publishedSizes: number[] = [];
365
398
 
366
399
  /** A published object equal to this one, or this one — which then becomes the published copy. */
367
400
  private canonicalStyle(
@@ -373,26 +406,84 @@ export class SymbioteRenderer implements Renderer2 {
373
406
  if (at > 0) {
374
407
  this.publishedStyles.splice(at, 1);
375
408
  this.publishedStyles.unshift(known);
409
+ this.publishedSizes.unshift(this.publishedSizes.splice(at, 1)[0]);
376
410
  }
377
411
  return known;
378
412
  }
379
413
  this.publishedStyles.unshift(style);
380
- if (this.publishedStyles.length > STYLE_CACHE) this.publishedStyles.pop();
414
+ this.publishedSizes.unshift(Object.keys(style).length);
415
+ if (this.publishedStyles.length > STYLE_CACHE) {
416
+ this.publishedStyles.pop();
417
+ this.publishedSizes.pop();
418
+ }
381
419
  return style;
382
420
  }
383
421
 
384
- /** The accumulator for this node's style run, opening one (and closing any other) if needed. */
385
- private openStyleRun(el: ISymbioteNode): Record<string, unknown> {
386
- if (this.pendingStyleNode === el) return this.pendingStyle;
422
+ /** Open this node's style run (closing any other); a no-op when it is already the open one. */
423
+ private openRun(el: ISymbioteNode): void {
424
+ if (this.pendingStyleNode === el) return;
387
425
  this.flushStyling();
426
+ this.pendingStyleNode = el;
388
427
  // Seeded from what is STANDING, because Angular sends only the keys that changed — an update
389
428
  // that moves one key must not drop the rest.
390
429
  const current = getExplicitStyle(el);
391
- this.pendingStyle = isRecord(current) ? { ...current } : {};
392
- this.pendingStyleNode = el;
430
+ if (isRecord(current)) {
431
+ this.pendingStyle = { ...current };
432
+ return;
433
+ }
434
+ this.isChoosingCandidate = true;
435
+ this.matchedKeys.length = 0;
436
+ }
437
+
438
+ // The FIRST key picks the candidate: a row alternates styles (row, cell, cell, input), so the
439
+ // front entry is usually the neighbour's.
440
+ private isChoosingCandidate = false;
441
+
442
+ private chooseCandidate(key: string, value: unknown): void {
443
+ this.isChoosingCandidate = false;
444
+ if (value === undefined) return;
445
+ for (let at = 0; at < this.publishedStyles.length; at += 1) {
446
+ const known = this.publishedStyles[at];
447
+ if (!Object.is(known[key], value)) continue;
448
+ this.matchCandidate = known;
449
+ this.matchCandidateSize = this.publishedSizes[at] ?? 0;
450
+ return;
451
+ }
452
+ }
453
+
454
+ /** Stop matching: write the keys matched so far into the accumulator the run goes on with. */
455
+ private materializeMatch(): void {
456
+ this.isChoosingCandidate = false;
457
+ const candidate = this.matchCandidate;
458
+ if (candidate === undefined) return;
459
+ this.matchCandidate = undefined;
460
+ for (const key of this.matchedKeys) this.pendingStyle[key] = candidate[key];
461
+ }
462
+
463
+ /** The accumulator for this node's style run, opening one (and closing any other) if needed. */
464
+ private openStyleRun(el: ISymbioteNode): Record<string, unknown> {
465
+ this.openRun(el);
466
+ this.materializeMatch();
393
467
  return this.pendingStyle;
394
468
  }
395
469
 
470
+ private writeStyle(el: ISymbioteNode, key: string, value: unknown): void {
471
+ this.openRun(el);
472
+ if (this.isChoosingCandidate) this.chooseCandidate(key, value);
473
+ const candidate = this.matchCandidate;
474
+ if (
475
+ candidate !== undefined &&
476
+ value !== undefined &&
477
+ Object.is(candidate[key], value) &&
478
+ !this.matchedKeys.includes(key)
479
+ ) {
480
+ this.matchedKeys.push(key);
481
+ return;
482
+ }
483
+ this.materializeMatch();
484
+ this.pendingStyle[key] = value;
485
+ }
486
+
396
487
  createElement(name: string): IHostNode {
397
488
  // `name` is the component's host tag — a symbiote intrinsic (`view`,
398
489
  // `text`, …), a public ergonomic alias (`View`, `Text`), or a raw Fabric view
@@ -442,6 +533,7 @@ export class SymbioteRenderer implements Renderer2 {
442
533
  if (isDebug()) {
443
534
  dlog(`angular createElement ${name} -> ${descriptor.component}`);
444
535
  }
536
+ if (READ_BACK_TAGS.has(engineName)) markReadBackNode(node);
445
537
  return toPublicInstance(node);
446
538
  }
447
539
 
@@ -653,7 +745,7 @@ export class SymbioteRenderer implements Renderer2 {
653
745
  if (isSurface(el)) return;
654
746
  countAngular('rendererWrites');
655
747
  noteAngularStyleWrite(style);
656
- this.openStyleRun(el)[style] = value;
748
+ this.writeStyle(el, style, value);
657
749
  this.surface.requestCommit();
658
750
  }
659
751
 
@@ -709,7 +801,11 @@ export class SymbioteRenderer implements Renderer2 {
709
801
  }
710
802
  }
711
803
  this.flushStyling();
712
- routeProp(el, name, this.wrapCallback(el, name, value));
804
+ // `[styleProp]` IS `style`, carried as a plain property so an RN array or press-state callback
805
+ // reaches the engine whole instead of Angular's styling engine. Binding it beside `[style]` on one
806
+ // element makes the later write win - they are one prop.
807
+ const propName = name === STYLE_PROP_BINDING ? 'style' : name;
808
+ routeProp(el, propName, this.wrapCallback(el, propName, value));
713
809
  this.surface.requestCommit();
714
810
  }
715
811
 
@@ -27,15 +27,13 @@
27
27
  // something marks the view. That moved to `SymbioteRenderer.setProperty`, which
28
28
  // every path reaches whether a directive claimed the binding or not.
29
29
  // the style shadow `[style]` and `[class]` are no longer claimed by an input, so `ɵɵstyleMap` and
30
- // `ɵɵclassMap` resolve them per KEY again ~7 000 renderer calls on the bench
31
- // row where the shadow made 4 000. That is a REGRESSION and it is accepted: the
32
- // bare bench arm already pays it and is still ~86 ms ahead of the matched one.
30
+ // `ɵɵclassMap` resolve them per KEY, as on a DOM element. An RN array or
31
+ // press-state callback travels as `[styleProp]` instead (`SymbioteElement.style`).
32
+ // the view flush the read-back tags' synchronous re-check finds the node's view lazily
33
+ // (`change-detection-flush.ts`) instead of injecting a `ChangeDetectorRef`.
33
34
  //
34
- // WHICH IS WHY THE LIST IS NOT "ALL OF THEM". A directive with real runtime behaviour must go on
35
- // matching, and here that is the `ValueChangeElement` chain (`text-input`, `switch` they listen for
36
- // the engine's value event and register a view flush) and `refresh-control`. `SymbioteCallbackHost`
37
- // and the two form accessors match on ATTRIBUTES rather than on a tag, so they land only where they
38
- // are needed and are never withheld.
35
+ // Every tag directive is withheld. `SymbioteCallbackHost` and the two form accessors match on
36
+ // ATTRIBUTES rather than on a tag, so they land only where they are needed and are never withheld.
39
37
 
40
38
  /**
41
39
  * Take these directives out of Angular's runtime matcher, leaving ngtsc's view of them untouched.
@@ -51,30 +49,8 @@ export function withholdFromRuntimeMatching(types: readonly unknown[]): void {
51
49
  if (typeof definition !== 'object' || definition === null) continue;
52
50
  const selectors: unknown = Reflect.get(definition, 'selectors');
53
51
  if (!Array.isArray(selectors)) continue;
54
- // Ivy holds a `CssSelectorList` — one array per alternative, whose first entry is the tag name
55
- // when the alternative names one. Recorded before the emptying, because afterwards there is
56
- // nothing to read and the guard below would have no subject.
57
- for (const alternative of selectors)
58
- if (Array.isArray(alternative)) {
59
- const tag: unknown = alternative[0];
60
- if (typeof tag === 'string' && tag.length > 0) withheldTags.add(tag);
61
- }
62
52
  // Emptied in place rather than replaced: the def holds this array, and reading `ɵdir` under JIT
63
53
  // is what compiles the directive, so the object in hand is the one the matcher will walk.
64
54
  selectors.length = 0;
65
55
  }
66
56
  }
67
-
68
- /**
69
- * Every TAG that lost its directive, for the guard that keeps `SymbioteStyleHost` covering them.
70
- *
71
- * A withheld tag whose style is claimed by nothing sends an RN style ARRAY into Angular's styling
72
- * engine, which throws inside change detection — a device failure with no compile-time tell. So the
73
- * set is recorded here at the moment it is emptied rather than maintained as a second list, and
74
- * `style-host-covers-withheld-tags.test.ts` reads it.
75
- */
76
- const withheldTags = new Set<string>();
77
-
78
- export function tagsWithheldFromRuntimeMatching(): ReadonlySet<string> {
79
- return withheldTags;
80
- }
@@ -1,11 +0,0 @@
1
- import { type OnChanges, type SimpleChanges } from '@angular/core';
2
- import * as i0 from "@angular/core";
3
- /** Exported for the guard, which derives the same set from the withheld directives' own selectors. */
4
- export declare const STYLE_HOST_SELECTOR: string;
5
- export declare class SymbioteStyleHost implements OnChanges {
6
- private readonly renderer;
7
- private readonly host;
8
- ngOnChanges(changes: SimpleChanges): void;
9
- static ɵfac: i0.ɵɵFactoryDeclaration<SymbioteStyleHost, never>;
10
- static ɵdir: i0.ɵɵDirectiveDeclaration<SymbioteStyleHost, "view,symbiote-view,text,symbiote-text,image,symbiote-image,image-background,pressable,symbiote-pressable,button,symbiote-button,touchable-opacity,touchable-highlight,touchable-native-feedback,touchable-without-feedback,scroll-view,horizontal-scroll-view,scroll-content,horizontal-scroll-content,activity-indicator,activity-indicator-spinner,safe-area-view,modal,symbiote-modal,sticky-header,input-accessory-view", never, { "style": { "alias": "style"; "required": false; }; "class": { "alias": "class"; "required": false; }; }, {}, never, never, true, never>;
11
- }
@@ -1,86 +0,0 @@
1
- // The directive that claims `[style]` and `[class]` on the elements that bind them.
2
- //
3
- // IT IS A CORRECTNESS REQUIREMENT BEFORE IT IS AN OPTIMISATION, which is what separates it from
4
- // `callback-host.ts`. An RN `StyleProp` is allowed to be an ARRAY, and Angular's own styling engine
5
- // cannot represent one: `ɵɵstyleMap` decomposes the value key by key, so `applyStyling` takes each
6
- // array MEMBER as a style key and throws inside change detection. Device-diagnosed 2026-09-02 on
7
- // ImageBackground, and pinned ever since by `renderer/style-input.test.ts`.
8
- //
9
- // A declared input is what keeps the value away from that engine — `checkStylingMap` hands the whole
10
- // value to the input instead of walking it (`render3/instructions/styling.ts:259-288`). So when the
11
- // tag directives were withheld from runtime matching, the claim had to come from somewhere, or an
12
- // array style would throw on every screen that writes one.
13
- //
14
- // AND IT IS ALSO THE FASTER PATH, which is why `class` rides the same selector. Unclaimed, a style
15
- // object reaches the renderer once per KEY — measured at ~7 000 renderer calls on the thousand-row
16
- // bench row against 4 000 when an input claims it.
17
- //
18
- // MATCHED ON THE TAG, and the first spelling of this directive matched on `[style],[class]` instead
19
- // — which does not work, for a reason worth keeping because it is invisible from the outside.
20
- //
21
- // `[style]` IS NOT A PROPERTY BINDING. The compiler routes it to `ɵɵstyleMap`, a styling instruction,
22
- // and the name never lands in the `AttributeMarker.Bindings` run that `findAttrIndexInNode` walks
23
- // (upstream `node_selector_matcher.ts:240-282`, where `Classes` and `Styles` markers are SKIPPED
24
- // outright). So a directive selected on `[style]` never matches an element that binds one, and the
25
- // value goes to the styling engine exactly as if no directive existed. Measured: the AOT fixture in
26
- // `style-input-aot.test.ts` imports `SYMBIOTE_ELEMENTS`, carries this directive, and still threw
27
- // `Unsupported styling type: function`. `[onPress]` and every other `on*` name DO work that way,
28
- // which is why `callback-host.ts` can be attribute-matched and this one cannot.
29
- //
30
- // SO IT IS A TAG DIRECTIVE, and it is instantiated per element like the ones it replaces — including
31
- // on elements that bind no style at all, because a tag selector is the only one that can match. What
32
- // is bought is its SHAPE: two inputs and two injections against the withheld classes' 279 and three.
33
- //
34
- // MEASURED, AND SMALLER THAN THE IDEA PROMISED. On the JavaScriptCore ladder, withholding a directive
35
- // outright is worth ~70-91 ms over ten thousand elements (`WITHHOLDING it`, 7.0-9.1 us each) — but
36
- // the adapter cannot take that, because this directive has to stay. What it reads against a bare tag
37
- // AFTER the change is 58.2 / 58.8 / 71.0 ms, against `a directive at all` at 50.2 / 55.9 / 68.3 in
38
- // the same runs. Those two are the same number: a directive costs what a directive costs, thin or
39
- // fat, and nearly all of it is matching and instantiating rather than inputs or injections.
40
- //
41
- // So the change is worth the FAT-TO-THIN difference and not the whole crossing — the ladder's own
42
- // `279 inputs, not 1` (~19.9 ms) plus `the ChangeDetectorRef` (~10-31 ms). Recorded at that size
43
- // rather than at the one the plan was aimed at.
44
- //
45
- // NO `ChangeDetectorRef` HERE. A style write is an ordinary prop write and marks nothing; the view
46
- // marking belongs to the callback path and lives with it.
47
- //
48
- // THE FOUR TAGS THAT KEEP THEIR OWN DIRECTIVE ARE ABSENT from the selector — `text-input`,
49
- // `text-input-multiline`, `switch` and `refresh-control`. Their fat directive still matches and
50
- // already declares `style`, and two directives claiming one input both receive it and both forward
51
- // it, which is the `unchanged` double write this adapter has measured before.
52
- import { Directive, ElementRef, Renderer2, inject, } from '@angular/core';
53
- import * as i0 from "@angular/core";
54
- /** Exported for the guard, which derives the same set from the withheld directives' own selectors. */
55
- export const STYLE_HOST_SELECTOR = 'view,symbiote-view,text,symbiote-text,image,symbiote-image,image-background,' +
56
- 'pressable,symbiote-pressable,button,symbiote-button,touchable-opacity,touchable-highlight,' +
57
- 'touchable-native-feedback,touchable-without-feedback,scroll-view,horizontal-scroll-view,' +
58
- 'scroll-content,horizontal-scroll-content,activity-indicator,activity-indicator-spinner,' +
59
- 'safe-area-view,modal,symbiote-modal,sticky-header,input-accessory-view';
60
- export class SymbioteStyleHost {
61
- constructor() {
62
- this.renderer = inject(Renderer2);
63
- this.host = inject(ElementRef);
64
- }
65
- ngOnChanges(changes) {
66
- for (const name of Object.keys(changes))
67
- this.renderer.setProperty(this.host.nativeElement, name, changes[name]?.currentValue);
68
- }
69
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SymbioteStyleHost, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
70
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.8", type: SymbioteStyleHost, isStandalone: true, selector: "view,symbiote-view,text,symbiote-text,image,symbiote-image,image-background,pressable,symbiote-pressable,button,symbiote-button,touchable-opacity,touchable-highlight,touchable-native-feedback,touchable-without-feedback,scroll-view,horizontal-scroll-view,scroll-content,horizontal-scroll-content,activity-indicator,activity-indicator-spinner,safe-area-view,modal,symbiote-modal,sticky-header,input-accessory-view", inputs: { style: "style", class: "class" }, usesOnChanges: true, ngImport: i0 }); }
71
- }
72
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SymbioteStyleHost, decorators: [{
73
- type: Directive,
74
- args: [{
75
- selector: 'view,symbiote-view,text,symbiote-text,image,symbiote-image,image-background,' +
76
- 'pressable,symbiote-pressable,button,symbiote-button,touchable-opacity,touchable-highlight,' +
77
- 'touchable-native-feedback,touchable-without-feedback,scroll-view,horizontal-scroll-view,' +
78
- 'scroll-content,horizontal-scroll-content,activity-indicator,activity-indicator-spinner,' +
79
- 'safe-area-view,modal,symbiote-modal,sticky-header,input-accessory-view',
80
- // Spelled out rather than referenced: ngtsc statically evaluates decorator metadata, and a
81
- // computed selector makes it report the class as not standalone — see `callback-host.ts`.
82
- inputs: ['style', 'class'],
83
- standalone: true,
84
- }]
85
- }] });
86
- //# sourceMappingURL=style-host.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"style-host.js","sourceRoot":"","sources":["../../src/style-host.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,EAAE;AACF,gGAAgG;AAChG,oGAAoG;AACpG,mGAAmG;AACnG,iGAAiG;AACjG,4EAA4E;AAC5E,EAAE;AACF,qGAAqG;AACrG,oGAAoG;AACpG,kGAAkG;AAClG,2DAA2D;AAC3D,EAAE;AACF,mGAAmG;AACnG,mGAAmG;AACnG,mDAAmD;AACnD,EAAE;AACF,oGAAoG;AACpG,8FAA8F;AAC9F,EAAE;AACF,sGAAsG;AACtG,kGAAkG;AAClG,iGAAiG;AACjG,mGAAmG;AACnG,oGAAoG;AACpG,iGAAiG;AACjG,iGAAiG;AACjG,gFAAgF;AAChF,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,EAAE;AACF,sGAAsG;AACtG,mGAAmG;AACnG,qGAAqG;AACrG,mGAAmG;AACnG,kGAAkG;AAClG,4FAA4F;AAC5F,EAAE;AACF,kGAAkG;AAClG,iGAAiG;AACjG,gDAAgD;AAChD,EAAE;AACF,mGAAmG;AACnG,0DAA0D;AAC1D,EAAE;AACF,2FAA2F;AAC3F,gGAAgG;AAChG,mGAAmG;AACnG,8EAA8E;AAE9E,OAAO,EACL,SAAS,EACT,UAAU,EACV,SAAS,EAGT,MAAM,GACP,MAAM,eAAe,CAAC;;AAEvB,sGAAsG;AACtG,MAAM,CAAC,MAAM,mBAAmB,GAC9B,8EAA8E;IAC9E,4FAA4F;IAC5F,0FAA0F;IAC1F,yFAAyF;IACzF,wEAAwE,CAAC;AAc3E,MAAM,OAAO,iBAAiB;IAZ9B;QAamB,aAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QAC7B,SAAI,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;KAU5C;IARC,WAAW,CAAC,OAAsB;QAChC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;YACrC,IAAI,CAAC,QAAQ,CAAC,WAAW,CACvB,IAAI,CAAC,IAAI,CAAC,aAAa,EACvB,IAAI,EACJ,OAAO,CAAC,IAAI,CAAC,EAAE,YAAY,CAC5B,CAAC;IACN,CAAC;8GAXU,iBAAiB;kGAAjB,iBAAiB;;2FAAjB,iBAAiB;kBAZ7B,SAAS;mBAAC;oBACT,QAAQ,EACN,8EAA8E;wBAC9E,4FAA4F;wBAC5F,0FAA0F;wBAC1F,yFAAyF;wBACzF,wEAAwE;oBAC1E,2FAA2F;oBAC3F,0FAA0F;oBAC1F,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;oBAC1B,UAAU,EAAE,IAAI;iBACjB"}
package/src/style-host.ts DELETED
@@ -1,94 +0,0 @@
1
- // The directive that claims `[style]` and `[class]` on the elements that bind them.
2
- //
3
- // IT IS A CORRECTNESS REQUIREMENT BEFORE IT IS AN OPTIMISATION, which is what separates it from
4
- // `callback-host.ts`. An RN `StyleProp` is allowed to be an ARRAY, and Angular's own styling engine
5
- // cannot represent one: `ɵɵstyleMap` decomposes the value key by key, so `applyStyling` takes each
6
- // array MEMBER as a style key and throws inside change detection. Device-diagnosed 2026-09-02 on
7
- // ImageBackground, and pinned ever since by `renderer/style-input.test.ts`.
8
- //
9
- // A declared input is what keeps the value away from that engine — `checkStylingMap` hands the whole
10
- // value to the input instead of walking it (`render3/instructions/styling.ts:259-288`). So when the
11
- // tag directives were withheld from runtime matching, the claim had to come from somewhere, or an
12
- // array style would throw on every screen that writes one.
13
- //
14
- // AND IT IS ALSO THE FASTER PATH, which is why `class` rides the same selector. Unclaimed, a style
15
- // object reaches the renderer once per KEY — measured at ~7 000 renderer calls on the thousand-row
16
- // bench row against 4 000 when an input claims it.
17
- //
18
- // MATCHED ON THE TAG, and the first spelling of this directive matched on `[style],[class]` instead
19
- // — which does not work, for a reason worth keeping because it is invisible from the outside.
20
- //
21
- // `[style]` IS NOT A PROPERTY BINDING. The compiler routes it to `ɵɵstyleMap`, a styling instruction,
22
- // and the name never lands in the `AttributeMarker.Bindings` run that `findAttrIndexInNode` walks
23
- // (upstream `node_selector_matcher.ts:240-282`, where `Classes` and `Styles` markers are SKIPPED
24
- // outright). So a directive selected on `[style]` never matches an element that binds one, and the
25
- // value goes to the styling engine exactly as if no directive existed. Measured: the AOT fixture in
26
- // `style-input-aot.test.ts` imports `SYMBIOTE_ELEMENTS`, carries this directive, and still threw
27
- // `Unsupported styling type: function`. `[onPress]` and every other `on*` name DO work that way,
28
- // which is why `callback-host.ts` can be attribute-matched and this one cannot.
29
- //
30
- // SO IT IS A TAG DIRECTIVE, and it is instantiated per element like the ones it replaces — including
31
- // on elements that bind no style at all, because a tag selector is the only one that can match. What
32
- // is bought is its SHAPE: two inputs and two injections against the withheld classes' 279 and three.
33
- //
34
- // MEASURED, AND SMALLER THAN THE IDEA PROMISED. On the JavaScriptCore ladder, withholding a directive
35
- // outright is worth ~70-91 ms over ten thousand elements (`WITHHOLDING it`, 7.0-9.1 us each) — but
36
- // the adapter cannot take that, because this directive has to stay. What it reads against a bare tag
37
- // AFTER the change is 58.2 / 58.8 / 71.0 ms, against `a directive at all` at 50.2 / 55.9 / 68.3 in
38
- // the same runs. Those two are the same number: a directive costs what a directive costs, thin or
39
- // fat, and nearly all of it is matching and instantiating rather than inputs or injections.
40
- //
41
- // So the change is worth the FAT-TO-THIN difference and not the whole crossing — the ladder's own
42
- // `279 inputs, not 1` (~19.9 ms) plus `the ChangeDetectorRef` (~10-31 ms). Recorded at that size
43
- // rather than at the one the plan was aimed at.
44
- //
45
- // NO `ChangeDetectorRef` HERE. A style write is an ordinary prop write and marks nothing; the view
46
- // marking belongs to the callback path and lives with it.
47
- //
48
- // THE FOUR TAGS THAT KEEP THEIR OWN DIRECTIVE ARE ABSENT from the selector — `text-input`,
49
- // `text-input-multiline`, `switch` and `refresh-control`. Their fat directive still matches and
50
- // already declares `style`, and two directives claiming one input both receive it and both forward
51
- // it, which is the `unchanged` double write this adapter has measured before.
52
-
53
- import {
54
- Directive,
55
- ElementRef,
56
- Renderer2,
57
- type OnChanges,
58
- type SimpleChanges,
59
- inject,
60
- } from '@angular/core';
61
-
62
- /** Exported for the guard, which derives the same set from the withheld directives' own selectors. */
63
- export const STYLE_HOST_SELECTOR =
64
- 'view,symbiote-view,text,symbiote-text,image,symbiote-image,image-background,' +
65
- 'pressable,symbiote-pressable,button,symbiote-button,touchable-opacity,touchable-highlight,' +
66
- 'touchable-native-feedback,touchable-without-feedback,scroll-view,horizontal-scroll-view,' +
67
- 'scroll-content,horizontal-scroll-content,activity-indicator,activity-indicator-spinner,' +
68
- 'safe-area-view,modal,symbiote-modal,sticky-header,input-accessory-view';
69
-
70
- @Directive({
71
- selector:
72
- 'view,symbiote-view,text,symbiote-text,image,symbiote-image,image-background,' +
73
- 'pressable,symbiote-pressable,button,symbiote-button,touchable-opacity,touchable-highlight,' +
74
- 'touchable-native-feedback,touchable-without-feedback,scroll-view,horizontal-scroll-view,' +
75
- 'scroll-content,horizontal-scroll-content,activity-indicator,activity-indicator-spinner,' +
76
- 'safe-area-view,modal,symbiote-modal,sticky-header,input-accessory-view',
77
- // Spelled out rather than referenced: ngtsc statically evaluates decorator metadata, and a
78
- // computed selector makes it report the class as not standalone — see `callback-host.ts`.
79
- inputs: ['style', 'class'],
80
- standalone: true,
81
- })
82
- export class SymbioteStyleHost implements OnChanges {
83
- private readonly renderer = inject(Renderer2);
84
- private readonly host = inject(ElementRef);
85
-
86
- ngOnChanges(changes: SimpleChanges): void {
87
- for (const name of Object.keys(changes))
88
- this.renderer.setProperty(
89
- this.host.nativeElement,
90
- name,
91
- changes[name]?.currentValue,
92
- );
93
- }
94
- }