@symbiote-native/angular 0.6.2 → 0.8.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 (191) hide show
  1. package/README.md +3 -3
  2. package/bin/symbiote-angular-dev.cjs +20 -19
  3. package/build/angular/anchor-host-registry.js +2 -2
  4. package/build/angular/anchor-host-registry.js.map +1 -1
  5. package/build/angular/bootstrap.js +1 -1
  6. package/build/angular/bootstrap.js.map +1 -1
  7. package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
  8. package/build/angular/components/activity-indicator/index.android.js +12 -4
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
  11. package/build/angular/components/activity-indicator/index.ios.js +8 -3
  12. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  13. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  14. package/build/angular/components/activity-indicator/shared.js +4 -4
  15. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  16. package/build/angular/components/button.d.ts +4 -2
  17. package/build/angular/components/button.js +41 -22
  18. package/build/angular/components/button.js.map +1 -1
  19. package/build/angular/components/flat-list/index.d.ts +6 -4
  20. package/build/angular/components/flat-list/index.js +80 -50
  21. package/build/angular/components/flat-list/index.js.map +1 -1
  22. package/build/angular/components/image/index.android.d.ts +8 -3
  23. package/build/angular/components/image/index.android.js +42 -10
  24. package/build/angular/components/image/index.android.js.map +1 -1
  25. package/build/angular/components/image/index.ios.d.ts +8 -3
  26. package/build/angular/components/image/index.ios.js +42 -10
  27. package/build/angular/components/image/index.ios.js.map +1 -1
  28. package/build/angular/components/image/shared.d.ts +4 -1
  29. package/build/angular/components/image/shared.js +51 -8
  30. package/build/angular/components/image/shared.js.map +1 -1
  31. package/build/angular/components/image-background.d.ts +2 -1
  32. package/build/angular/components/image-background.js +26 -10
  33. package/build/angular/components/image-background.js.map +1 -1
  34. package/build/angular/components/input-accessory-view/index.d.ts +9 -4
  35. package/build/angular/components/input-accessory-view/index.js +49 -10
  36. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  37. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  38. package/build/angular/components/keyboard-avoiding-view/index.js +44 -12
  39. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  40. package/build/angular/components/modal/index.d.ts +11 -8
  41. package/build/angular/components/modal/index.js +51 -29
  42. package/build/angular/components/modal/index.js.map +1 -1
  43. package/build/angular/components/pressable/index.d.ts +12 -6
  44. package/build/angular/components/pressable/index.js +66 -13
  45. package/build/angular/components/pressable/index.js.map +1 -1
  46. package/build/angular/components/refresh-control/index.d.ts +8 -4
  47. package/build/angular/components/refresh-control/index.js +54 -16
  48. package/build/angular/components/refresh-control/index.js.map +1 -1
  49. package/build/angular/components/safe-area-view/index.d.ts +9 -4
  50. package/build/angular/components/safe-area-view/index.js +48 -9
  51. package/build/angular/components/safe-area-view/index.js.map +1 -1
  52. package/build/angular/components/scroll-view/index.android.d.ts +2 -3
  53. package/build/angular/components/scroll-view/index.android.js +88 -61
  54. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  55. package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
  56. package/build/angular/components/scroll-view/index.ios.js +24 -18
  57. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  58. package/build/angular/components/scroll-view/projection.d.ts +35 -0
  59. package/build/angular/components/scroll-view/projection.js +279 -53
  60. package/build/angular/components/scroll-view/projection.js.map +1 -1
  61. package/build/angular/components/scroll-view/shared.d.ts +14 -5
  62. package/build/angular/components/scroll-view/shared.js +225 -75
  63. package/build/angular/components/scroll-view/shared.js.map +1 -1
  64. package/build/angular/components/scroll-view/sticky-header.js +21 -22
  65. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  66. package/build/angular/components/section-list/index.d.ts +7 -1
  67. package/build/angular/components/section-list/index.js +91 -54
  68. package/build/angular/components/section-list/index.js.map +1 -1
  69. package/build/angular/components/switch/index.android.d.ts +7 -3
  70. package/build/angular/components/switch/index.android.js +52 -10
  71. package/build/angular/components/switch/index.android.js.map +1 -1
  72. package/build/angular/components/switch/index.ios.d.ts +7 -3
  73. package/build/angular/components/switch/index.ios.js +52 -10
  74. package/build/angular/components/switch/index.ios.js.map +1 -1
  75. package/build/angular/components/switch/shared.d.ts +13 -4
  76. package/build/angular/components/switch/shared.js +71 -7
  77. package/build/angular/components/switch/shared.js.map +1 -1
  78. package/build/angular/components/text-input.d.ts +11 -3
  79. package/build/angular/components/text-input.js +68 -8
  80. package/build/angular/components/text-input.js.map +1 -1
  81. package/build/angular/components/touchable/index.d.ts +40 -9
  82. package/build/angular/components/touchable/index.js +303 -67
  83. package/build/angular/components/touchable/index.js.map +1 -1
  84. package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
  85. package/build/angular/components/touchable-native-feedback/index.js +63 -27
  86. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  87. package/build/angular/components/virtualized-list/directives.js +22 -18
  88. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  89. package/build/angular/components/virtualized-list/index.d.ts +15 -4
  90. package/build/angular/components/virtualized-list/index.js +326 -94
  91. package/build/angular/components/virtualized-list/index.js.map +1 -1
  92. package/build/angular/components/virtualized-section-list/directives.js +12 -12
  93. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  94. package/build/angular/components/virtualized-section-list/index.d.ts +19 -1
  95. package/build/angular/components/virtualized-section-list/index.js +93 -22
  96. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  97. package/build/angular/components.d.ts +2 -2
  98. package/build/angular/components.js +5 -0
  99. package/build/angular/components.js.map +1 -1
  100. package/build/angular/create-portal/index.js +34 -36
  101. package/build/angular/create-portal/index.js.map +1 -1
  102. package/build/angular/create-tunnel/index.js +19 -22
  103. package/build/angular/create-tunnel/index.js.map +1 -1
  104. package/build/angular/descriptor-to-angular/index.js +6 -4
  105. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  106. package/build/angular/diagnostics.d.ts +33 -0
  107. package/build/angular/diagnostics.js +82 -0
  108. package/build/angular/diagnostics.js.map +1 -0
  109. package/build/angular/host-instance/index.js +1 -1
  110. package/build/angular/host-instance/index.js.map +1 -1
  111. package/build/angular/index.d.ts +9 -4
  112. package/build/angular/index.js +17 -3
  113. package/build/angular/index.js.map +1 -1
  114. package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
  115. package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
  116. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
  117. package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
  118. package/build/angular/modules/animated/create-animated-component.js +106 -55
  119. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  120. package/build/angular/modules/status-bar/index.js +4 -4
  121. package/build/angular/modules/status-bar/index.js.map +1 -1
  122. package/build/angular/primitives/index.d.ts +20 -3
  123. package/build/angular/primitives/index.js +84 -48
  124. package/build/angular/primitives/index.js.map +1 -1
  125. package/build/angular/primitives/shared.d.ts +48 -2
  126. package/build/angular/primitives/shared.js +158 -11
  127. package/build/angular/primitives/shared.js.map +1 -1
  128. package/build/angular/render/index.js +69 -38
  129. package/build/angular/render/index.js.map +1 -1
  130. package/build/angular/renderer/index.d.ts +1 -0
  131. package/build/angular/renderer/index.js +37 -2
  132. package/build/angular/renderer/index.js.map +1 -1
  133. package/build/angular/services/color-scheme.service.js +3 -3
  134. package/build/angular/services/color-scheme.service.js.map +1 -1
  135. package/build/angular/services/window-dimensions.service.js +4 -4
  136. package/build/angular/services/window-dimensions.service.js.map +1 -1
  137. package/package.json +10 -8
  138. package/src/anchor-host-registry.ts +2 -2
  139. package/src/bootstrap.ts +4 -1
  140. package/src/components/activity-indicator/index.android.ts +8 -1
  141. package/src/components/activity-indicator/index.ios.ts +4 -0
  142. package/src/components/activity-indicator/shared.ts +32 -13
  143. package/src/components/button.ts +46 -19
  144. package/src/components/flat-list/index.ts +122 -62
  145. package/src/components/image/index.android.ts +58 -7
  146. package/src/components/image/index.ios.ts +58 -7
  147. package/src/components/image/shared.ts +92 -21
  148. package/src/components/image-background.ts +29 -9
  149. package/src/components/input-accessory-view/index.ts +67 -12
  150. package/src/components/keyboard-avoiding-view/index.ts +61 -15
  151. package/src/components/modal/index.ts +72 -33
  152. package/src/components/pressable/index.ts +117 -28
  153. package/src/components/refresh-control/index.ts +74 -18
  154. package/src/components/safe-area-view/index.ts +64 -10
  155. package/src/components/scroll-view/index.android.ts +62 -47
  156. package/src/components/scroll-view/index.ios.ts +19 -17
  157. package/src/components/scroll-view/projection.ts +363 -64
  158. package/src/components/scroll-view/shared.ts +312 -90
  159. package/src/components/scroll-view/sticky-header.ts +36 -24
  160. package/src/components/section-list/index.ts +86 -53
  161. package/src/components/switch/index.android.ts +60 -8
  162. package/src/components/switch/index.ios.ts +60 -8
  163. package/src/components/switch/shared.ts +93 -6
  164. package/src/components/text-input.ts +95 -13
  165. package/src/components/touchable/index.ts +348 -64
  166. package/src/components/touchable-native-feedback/index.ts +94 -31
  167. package/src/components/virtualized-list/directives.ts +19 -5
  168. package/src/components/virtualized-list/index.ts +380 -85
  169. package/src/components/virtualized-section-list/directives.ts +6 -3
  170. package/src/components/virtualized-section-list/index.ts +146 -33
  171. package/src/components.ts +9 -1
  172. package/src/create-portal/index.ts +31 -31
  173. package/src/create-tunnel/index.ts +16 -17
  174. package/src/descriptor-to-angular/index.ts +29 -8
  175. package/src/diagnostics.ts +138 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +61 -3
  178. package/src/modules/animated/animated-leaf-binder.ts +28 -77
  179. package/src/modules/animated/create-animated-component.ts +115 -47
  180. package/src/modules/app-registry/index.ts +9 -9
  181. package/src/modules/status-bar/index.ts +13 -4
  182. package/src/primitives/index.ts +47 -2
  183. package/src/primitives/shared.ts +170 -10
  184. package/src/render/index.ts +77 -39
  185. package/src/renderer/index.ts +76 -14
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  189. package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
  190. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
  191. package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
@@ -1,5 +1,18 @@
1
- import { Directive, EventEmitter, Output } from '@angular/core';
2
- import { renderSwitch, resolveAccessibilityProps } from '@symbiote-native/components';
1
+ import {
2
+ ChangeDetectorRef,
3
+ Directive,
4
+ EventEmitter,
5
+ Output,
6
+ computed,
7
+ inject,
8
+ signal,
9
+ type OnChanges,
10
+ } from '@angular/core';
11
+ import type { ControlValueAccessor } from '@angular/forms';
12
+ import {
13
+ renderSwitch,
14
+ resolveAccessibilityProps,
15
+ } from '@symbiote-native/components';
3
16
  import type {
4
17
  ISwitchPlatform,
5
18
  ISwitchProps,
@@ -23,7 +36,10 @@ import {
23
36
  type IViewStyle,
24
37
  } from '@symbiote-native/engine';
25
38
 
26
- export type { ISwitchProps, ISwitchTrackColor } from '@symbiote-native/components';
39
+ export type {
40
+ ISwitchProps,
41
+ ISwitchTrackColor,
42
+ } from '@symbiote-native/components';
27
43
 
28
44
  export type ISwitchInputs = Pick<
29
45
  ISwitchProps,
@@ -78,7 +94,9 @@ function isSwitchEvent(event: unknown): event is ISymbioteEvent {
78
94
  // concrete subclasses are real @Components — ngc's AOT build (NG2007) requires it the moment the
79
95
  // base itself carries decorated members (@Output() here), mirroring ScrollViewBase.
80
96
  @Directive()
81
- export abstract class SwitchBase implements ISwitchInputs {
97
+ export abstract class SwitchBase
98
+ implements ISwitchInputs, ControlValueAccessor, OnChanges
99
+ {
82
100
  // The value/change lifecycle as real Angular events: `(valueChange)="onToggle($event)"`, not
83
101
  // `[onValueChange]="onToggle"` — mirrors Pressable's press/hover @Output() conversion. Unlike
84
102
  // Pressable's createPressHandlers, handleChange() below calls `.emit()` unconditionally: it needs
@@ -105,9 +123,46 @@ export abstract class SwitchBase implements ISwitchInputs {
105
123
  'aria-checked': boolean | 'mixed' | undefined;
106
124
 
107
125
  protected abstract readonly platform: ISwitchHostPlatform;
126
+ // NOT a hostProps dependency - only snapBackIfNeeded reads it, and the bag's `value` comes from
127
+ // the @Input, not from this reducer state. So it stays a plain field; if a future change makes
128
+ // the bag read it, it must become a signal first (see inputsRevision's note).
108
129
  private switchState: ISwitchState = createInitialSwitchState();
130
+ // Only setDisabledState()/writeValue() need this: @angular/forms mutates `value`/`disabled` from
131
+ // OUTSIDE Angular's own binding path, which doesn't itself schedule a tick under zoneless CD.
132
+ private readonly changeDetector = inject(ChangeDetectorRef);
133
+
134
+ // Bridges the non-reactive @Input fields into the reactive graph so hostProps below can be a
135
+ // memoized computed(). Signal inputs would make this unnecessary, but `input()` is visible only
136
+ // to the AOT compiler and this package's unit suite runs on JIT - see the
137
+ // `angular-adapter-change-detection` skill, §6. `signal()`/`computed()` are plain runtime APIs
138
+ // and need no compiler support, which is what makes this bridge possible at all.
139
+ //
140
+ // Bumped from ngOnChanges (covers every @Input) AND from the two ControlValueAccessor writers
141
+ // below, which assign `value`/`disabled` from OUTSIDE Angular's binding path and so never reach
142
+ // ngOnChanges. ONLY a genuine dependency of the bag may bump it - widening it to "bump on
143
+ // everything" silently un-memoizes the bag and defeats the point. State that is not an @Input
144
+ // and is not one of those two writers needs its OWN signal instead (the platform components'
145
+ // anchor style is exactly that case).
146
+ private readonly inputsRevision = signal(0);
147
+
148
+ ngOnChanges(): void {
149
+ this.inputsRevision.update(revision => revision + 1);
150
+ }
151
+
152
+ // MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, so
153
+ // `[symbioteHostProps]`'s reference check failed every time and every key was re-pushed through
154
+ // Renderer2 -> routeProp - pure waste when nothing changed. computed() returns the SAME object
155
+ // until a tracked dependency actually changes, and the input setter then skips the whole spread.
156
+ //
157
+ // A computed FIELD cannot be overridden by a subclass accessor the way a getter can, so the
158
+ // platform components override buildHostProps() below and this stays the single memoization
159
+ // point for every platform.
160
+ readonly hostProps = computed<IHostProps>(() => {
161
+ this.inputsRevision();
162
+ return this.buildHostProps();
163
+ });
109
164
 
110
- get hostProps(): IHostProps {
165
+ protected buildHostProps(): IHostProps {
111
166
  const props = resolveAccessibilityProps(this.inputProps());
112
167
  const {
113
168
  value,
@@ -144,7 +199,10 @@ export abstract class SwitchBase implements ISwitchInputs {
144
199
  if (next === undefined) return;
145
200
 
146
201
  this.valueChange.emit(next);
147
- this.switchState = switchReducer(this.switchState, { type: 'native-reported', value: next });
202
+ this.switchState = switchReducer(this.switchState, {
203
+ type: 'native-reported',
204
+ value: next,
205
+ });
148
206
 
149
207
  const hostNode = readSwitchNode(node);
150
208
  if (hostNode === null) return;
@@ -168,6 +226,35 @@ export abstract class SwitchBase implements ISwitchInputs {
168
226
  });
169
227
  }
170
228
 
229
+ // ControlValueAccessor — `value` is a plain field feeding `hostProps` on every CD check, no
230
+ // ngOnChanges/stale-safe commit dance to guard (unlike TextInput, a boolean toggle has no
231
+ // keystroke to race), so writeValue()/setDisabledState() can set it straight.
232
+ writeValue(value: boolean | null): void {
233
+ this.value = value ?? false;
234
+ // `value` feeds hostProps but this write bypasses Angular's binding path, so ngOnChanges never
235
+ // fires for it - without the bump the memoized bag would keep reporting the old value and the
236
+ // switch would stop reflecting a form-driven change.
237
+ this.inputsRevision.update(revision => revision + 1);
238
+ this.changeDetector.markForCheck();
239
+ }
240
+
241
+ registerOnChange(fn: (value: boolean) => void): void {
242
+ this.valueChange.subscribe(fn);
243
+ }
244
+
245
+ registerOnTouched(fn: () => void): void {
246
+ // Switch has no blur concept (it's a discrete toggle, not a text field) — `change` is the
247
+ // closest touched signal, same convention Angular's own checkbox CVAs use.
248
+ this.change.subscribe(() => fn());
249
+ }
250
+
251
+ setDisabledState(isDisabled: boolean): void {
252
+ this.disabled = isDisabled;
253
+ // Same reason as writeValue: an @angular/forms write that never reaches ngOnChanges.
254
+ this.inputsRevision.update(revision => revision + 1);
255
+ this.changeDetector.markForCheck();
256
+ }
257
+
171
258
  private inputProps(): Partial<ISwitchProps> {
172
259
  const props: Partial<ISwitchProps> = {
173
260
  value: this.value,
@@ -28,9 +28,11 @@
28
28
  import {
29
29
  CUSTOM_ELEMENTS_SCHEMA,
30
30
  ChangeDetectionStrategy,
31
+ ChangeDetectorRef,
31
32
  Component,
32
33
  ElementRef,
33
34
  EventEmitter,
35
+ forwardRef,
34
36
  inject,
35
37
  Input,
36
38
  Output,
@@ -40,6 +42,7 @@ import {
40
42
  type OnDestroy,
41
43
  type SimpleChanges,
42
44
  } from '@angular/core';
45
+ import { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms';
43
46
  import {
44
47
  foldText,
45
48
  eventCountFromChange,
@@ -79,15 +82,23 @@ import {
79
82
  anchorHostStyle,
80
83
  MultilineTextInputHost,
81
84
  SymbioteHostPropsDirective,
85
+ SymbioteStyleInputDirective,
82
86
  TextInputHost,
83
87
  } from '../primitives';
84
88
 
85
- export type { ITextInputHandle, ITextInputSelection, IInputMode, IEnterKeyHint, ISubmitBehavior };
89
+ export type {
90
+ ITextInputHandle,
91
+ ITextInputSelection,
92
+ IInputMode,
93
+ IEnterKeyHint,
94
+ ISubmitBehavior,
95
+ };
86
96
 
87
97
  // Mirrors React's ITextInputProps minus nothing (TextInput has no children) — declared
88
98
  // per-adapter over the shared accessibility base because Angular's input surface aliases the
89
99
  // aria-* keys to camelCase @Inputs, unlike the plain agnostic fields shared across adapters.
90
- export interface IAngularTextInputProps extends IAccessibilityProps, IAriaProps {
100
+ export interface IAngularTextInputProps
101
+ extends IAccessibilityProps, IAriaProps {
91
102
  value?: string;
92
103
  defaultValue?: string;
93
104
  placeholder?: string;
@@ -154,9 +165,19 @@ export type IAngularTextInputInputs = Omit<
154
165
  @Component({
155
166
  selector: 'TextInput',
156
167
  standalone: true,
168
+ hostDirectives: [
169
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
170
+ ],
157
171
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
158
172
  imports: [SymbioteHostPropsDirective, TextInputHost, MultilineTextInputHost],
159
173
  changeDetection: ChangeDetectionStrategy.OnPush,
174
+ providers: [
175
+ {
176
+ provide: NG_VALUE_ACCESSOR,
177
+ useExisting: forwardRef(() => TextInput),
178
+ multi: true,
179
+ },
180
+ ],
160
181
  template: `
161
182
  @if (isMultiline) {
162
183
  <symbiote-text-input-multiline
@@ -196,7 +217,13 @@ export type IAngularTextInputInputs = Omit<
196
217
  `,
197
218
  })
198
219
  export class TextInput
199
- implements IAngularTextInputInputs, ITextInputHandle, OnChanges, AfterViewInit, OnDestroy
220
+ implements
221
+ IAngularTextInputInputs,
222
+ ITextInputHandle,
223
+ OnChanges,
224
+ AfterViewInit,
225
+ OnDestroy,
226
+ ControlValueAccessor
200
227
  {
201
228
  @Input() value?: string;
202
229
  @Input() defaultValue?: string;
@@ -263,8 +290,10 @@ export class TextInput
263
290
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
264
291
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
265
292
  @Input() accessibilityLabelledBy?: string | string[];
266
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
267
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
293
+ @Input()
294
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
295
+ @Input()
296
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
268
297
  @Input() screenReaderFocusable?: boolean;
269
298
  @Input() accessibilityViewIsModal?: boolean;
270
299
  @Input() accessibilityElementsHidden?: boolean;
@@ -291,6 +320,9 @@ export class TextInput
291
320
  // onto (see anchorHostStyle's doc comment) — NOT `hostRef` above, which targets the real inner
292
321
  // symbiote-text-input(-multiline) one level down.
293
322
  private readonly elementRef = inject(ElementRef);
323
+ // Only setDisabledState() needs this: it mutates `editable` from OUTSIDE Angular's own binding
324
+ // path (@angular/forms calling in directly), which doesn't itself schedule a tick under zoneless CD.
325
+ private readonly changeDetector = inject(ChangeDetectorRef);
294
326
 
295
327
  // The count native last acknowledged; the controlled write / clear / setSelection echo it so
296
328
  // native's eventLag lands on 0 and the write applies. A plain field (read live by the imperative
@@ -304,6 +336,9 @@ export class TextInput
304
336
  // synchronous focus getter (RN's TextInputState holds the same).
305
337
  private focused = false;
306
338
  private cancelAutoFocus?: () => void;
339
+ // Mirrors SimpleChange.firstChange for the writeValue() path below (@angular/forms' own initial
340
+ // write, distinct from ngOnChanges' firstChange, which never fires for a forms-driven write).
341
+ private isFirstFormWrite = true;
307
342
 
308
343
  get isMultiline(): boolean {
309
344
  return this.multiline === true;
@@ -323,14 +358,44 @@ export class TextInput
323
358
  ngOnChanges(changes: SimpleChanges): void {
324
359
  const valueChange = changes['value'];
325
360
  if (valueChange === undefined) return;
326
- // The first value is the seed, not a divergence: record it and do not command.
327
361
  if (valueChange.firstChange) {
362
+ // The initial value already reached native via createNode's hostProps — seed
363
+ // lastNativeText instead of re-commanding it (see commitControlledValue's own comment on
364
+ // why a redundant re-push isn't just wasteful — it can race a keystroke).
328
365
  this.lastNativeText = foldText(this.value, this.defaultValue);
329
366
  return;
330
367
  }
331
368
  this.commitControlledValue();
332
369
  }
333
370
 
371
+ // ControlValueAccessor — @angular/forms calls these straight into this component, bypassing
372
+ // template bindings/ngOnChanges entirely. `isFirstFormWrite` mirrors SimpleChange.firstChange:
373
+ // the initial value already reached native via createNode's hostProps, so it must seed
374
+ // lastNativeText, not re-command (see commitControlledValue's own comment on why a redundant
375
+ // re-push isn't just wasteful — it can race a keystroke).
376
+ writeValue(value: string | null): void {
377
+ this.value = value ?? undefined;
378
+ if (this.isFirstFormWrite) {
379
+ this.isFirstFormWrite = false;
380
+ this.lastNativeText = foldText(this.value, this.defaultValue);
381
+ return;
382
+ }
383
+ this.commitControlledValue();
384
+ }
385
+
386
+ registerOnChange(fn: (value: string) => void): void {
387
+ this.valueChange.subscribe(fn);
388
+ }
389
+
390
+ registerOnTouched(fn: () => void): void {
391
+ this.blurEvent.subscribe(() => fn());
392
+ }
393
+
394
+ setDisabledState(isDisabled: boolean): void {
395
+ this.editable = !isDisabled;
396
+ this.changeDetector.markForCheck();
397
+ }
398
+
334
399
  // autoFocus is driven in JS, not as a native prop: once the node is committed, command `focus`
335
400
  // down once (RN does the same via TextInputState.focusInput). Under Angular's async-batched
336
401
  // commit the node has no Fabric tag at ngAfterViewInit, so whenCommitted defers the command to
@@ -340,7 +405,9 @@ export class TextInput
340
405
  const node = this.hostNode;
341
406
  if (node === undefined) return;
342
407
  dlog('TextInput autoFocus -> focus command');
343
- this.cancelAutoFocus = whenCommitted(node, () => dispatchViewCommand(node, 'focus', []));
408
+ this.cancelAutoFocus = whenCommitted(node, () =>
409
+ dispatchViewCommand(node, 'focus', []),
410
+ );
344
411
  }
345
412
 
346
413
  ngOnDestroy(): void {
@@ -408,7 +475,12 @@ export class TextInput
408
475
  clear(): void {
409
476
  const node = this.hostNode;
410
477
  if (node === undefined) return;
411
- dispatchViewCommand(node, 'setTextAndSelection', [this.mostRecentEventCount, '', 0, 0]);
478
+ dispatchViewCommand(node, 'setTextAndSelection', [
479
+ this.mostRecentEventCount,
480
+ '',
481
+ 0,
482
+ 0,
483
+ ]);
412
484
  this.lastNativeText = '';
413
485
  }
414
486
 
@@ -438,12 +510,20 @@ export class TextInput
438
510
  const value = this.value;
439
511
  if (!shouldCommandText(this.lastNativeText, value)) return;
440
512
  const selStart = this.selection?.start ?? SELECTION_NONE;
441
- const selEnd = this.selection?.end ?? this.selection?.start ?? SELECTION_NONE;
513
+ const selEnd =
514
+ this.selection?.end ?? this.selection?.start ?? SELECTION_NONE;
442
515
  const count = this.mostRecentEventCount;
443
- dlog(`TextInput setTextAndSelection count=${count} text=${JSON.stringify(value)}`);
516
+ dlog(
517
+ `TextInput setTextAndSelection count=${count} text=${JSON.stringify(value)}`,
518
+ );
444
519
  this.lastNativeText = value;
445
520
  whenCommitted(node, () =>
446
- dispatchViewCommand(node, 'setTextAndSelection', [count, value, selStart, selEnd]),
521
+ dispatchViewCommand(node, 'setTextAndSelection', [
522
+ count,
523
+ value,
524
+ selStart,
525
+ selEnd,
526
+ ]),
447
527
  );
448
528
  }
449
529
 
@@ -508,8 +588,10 @@ export class TextInput
508
588
  accessibilityElementsHidden: this.accessibilityElementsHidden,
509
589
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
510
590
  accessibilityLanguage: this.accessibilityLanguage,
511
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
512
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
591
+ accessibilityRespondsToUserInteraction:
592
+ this.accessibilityRespondsToUserInteraction,
593
+ accessibilityShowsLargeContentViewer:
594
+ this.accessibilityShowsLargeContentViewer,
513
595
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
514
596
  role: this.role,
515
597
  'aria-label': this.ariaLabel,