@symbiote-native/angular 3.0.2 → 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 (55) 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/element-props.d.ts +16 -5
  26. package/build/angular/elements.d.ts +5 -21
  27. package/build/angular/elements.js +18 -26
  28. package/build/angular/elements.js.map +1 -1
  29. package/build/angular/index.d.ts +0 -1
  30. package/build/angular/index.js +5 -7
  31. package/build/angular/index.js.map +1 -1
  32. package/build/angular/renderer/index.d.ts +11 -0
  33. package/build/angular/renderer/index.js +97 -12
  34. package/build/angular/renderer/index.js.map +1 -1
  35. package/build/angular/runtime-matching.d.ts +0 -1
  36. package/build/angular/runtime-matching.js +6 -29
  37. package/build/angular/runtime-matching.js.map +1 -1
  38. package/package.json +4 -4
  39. package/src/callback-host.ts +10 -43
  40. package/src/change-detection-flush.ts +67 -14
  41. package/src/components/flat-list/index.ts +22 -54
  42. package/src/components/modal/index.ts +22 -16
  43. package/src/components/section-list/index.ts +4 -0
  44. package/src/components/virtualized-list/directives.ts +10 -8
  45. package/src/components/virtualized-list/index.ts +38 -13
  46. package/src/components/virtualized-section-list/index.ts +6 -0
  47. package/src/element-props.ts +22 -13
  48. package/src/elements.ts +21 -56
  49. package/src/index.ts +5 -7
  50. package/src/renderer/index.ts +107 -11
  51. package/src/runtime-matching.ts +6 -30
  52. package/build/angular/style-host.d.ts +0 -11
  53. package/build/angular/style-host.js +0 -86
  54. package/build/angular/style-host.js.map +0 -1
  55. package/src/style-host.ts +0 -94
@@ -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
- }