@symbiote-native/angular 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/symbiote-angular-dev.cjs +20 -19
- package/build/angular/anchor-host-registry.js +2 -2
- package/build/angular/anchor-host-registry.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.android.js +6 -3
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.ios.js +6 -3
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.js +3 -3
- package/build/angular/components/button.d.ts +4 -2
- package/build/angular/components/button.js +28 -17
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +5 -3
- package/build/angular/components/flat-list/index.js +35 -38
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.d.ts +8 -3
- package/build/angular/components/image/index.android.js +39 -9
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.d.ts +8 -3
- package/build/angular/components/image/index.ios.js +39 -9
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -1
- package/build/angular/components/image/shared.js +33 -2
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +2 -1
- package/build/angular/components/image-background.js +6 -4
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +9 -4
- package/build/angular/components/input-accessory-view/index.js +43 -9
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +2 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +6 -4
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +11 -8
- package/build/angular/components/modal/index.js +45 -28
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +11 -5
- package/build/angular/components/pressable/index.js +61 -12
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +8 -4
- package/build/angular/components/refresh-control/index.js +52 -16
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +9 -4
- package/build/angular/components/safe-area-view/index.js +46 -9
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.d.ts +2 -3
- package/build/angular/components/scroll-view/index.android.js +49 -52
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
- package/build/angular/components/scroll-view/index.ios.js +15 -17
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +3 -0
- package/build/angular/components/scroll-view/projection.js +68 -7
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +14 -5
- package/build/angular/components/scroll-view/shared.js +198 -69
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +14 -19
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +2 -1
- package/build/angular/components/section-list/index.js +31 -39
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.d.ts +7 -3
- package/build/angular/components/switch/index.android.js +38 -10
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.d.ts +7 -3
- package/build/angular/components/switch/index.ios.js +38 -10
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +12 -3
- package/build/angular/components/switch/shared.js +66 -5
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +10 -2
- package/build/angular/components/text-input.js +46 -6
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +17 -8
- package/build/angular/components/touchable/index.js +78 -19
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
- package/build/angular/components/touchable-native-feedback/index.js +61 -25
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +18 -18
- package/build/angular/components/virtualized-list/index.d.ts +12 -4
- package/build/angular/components/virtualized-list/index.js +163 -68
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +12 -12
- package/build/angular/components/virtualized-section-list/index.d.ts +2 -1
- package/build/angular/components/virtualized-section-list/index.js +6 -4
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +1 -1
- package/build/angular/components.js +5 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +29 -35
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +19 -22
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +3 -3
- package/build/angular/index.d.ts +4 -2
- package/build/angular/index.js +2 -2
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
- package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
- package/build/angular/modules/animated/create-animated-component.js +83 -50
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/app-registry/index.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +3 -3
- package/build/angular/primitives/index.d.ts +1 -1
- package/build/angular/primitives/index.js +46 -46
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +39 -1
- package/build/angular/primitives/shared.js +110 -9
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +23 -35
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js +3 -3
- package/build/angular/services/window-dimensions.service.js +3 -3
- package/package.json +9 -7
- package/src/anchor-host-registry.ts +2 -2
- package/src/components/activity-indicator/index.android.ts +2 -0
- package/src/components/activity-indicator/index.ios.ts +2 -0
- package/src/components/button.ts +28 -14
- package/src/components/flat-list/index.ts +34 -36
- package/src/components/image/index.android.ts +50 -6
- package/src/components/image/index.ios.ts +50 -6
- package/src/components/image/shared.ts +34 -2
- package/src/components/image-background.ts +2 -1
- package/src/components/input-accessory-view/index.ts +42 -4
- package/src/components/keyboard-avoiding-view/index.ts +7 -1
- package/src/components/modal/index.ts +51 -26
- package/src/components/pressable/index.ts +67 -8
- package/src/components/refresh-control/index.ts +54 -12
- package/src/components/safe-area-view/index.ts +50 -5
- package/src/components/scroll-view/index.android.ts +34 -41
- package/src/components/scroll-view/index.ios.ts +10 -21
- package/src/components/scroll-view/projection.ts +79 -7
- package/src/components/scroll-view/shared.ts +223 -68
- package/src/components/scroll-view/sticky-header.ts +11 -16
- package/src/components/section-list/index.ts +27 -36
- package/src/components/switch/index.android.ts +48 -7
- package/src/components/switch/index.ios.ts +48 -7
- package/src/components/switch/shared.ts +79 -3
- package/src/components/text-input.ts +52 -2
- package/src/components/touchable/index.ts +76 -16
- package/src/components/touchable-native-feedback/index.ts +62 -21
- package/src/components/virtualized-list/index.ts +137 -44
- package/src/components/virtualized-section-list/index.ts +2 -1
- package/src/components.ts +5 -0
- package/src/create-portal/index.ts +23 -29
- package/src/create-tunnel/index.ts +13 -16
- package/src/index.ts +30 -1
- package/src/modules/animated/animated-leaf-binder.ts +28 -77
- package/src/modules/animated/create-animated-component.ts +75 -36
- package/src/modules/app-registry/index.ts +6 -8
- package/src/primitives/index.ts +1 -0
- package/src/primitives/shared.ts +102 -3
- package/src/render/index.ts +23 -35
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
- package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
- package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
forwardRef,
|
|
6
|
+
inject,
|
|
7
|
+
signal,
|
|
8
|
+
type DoCheck,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
|
+
import {
|
|
12
|
+
anchorHostStyle,
|
|
13
|
+
SwitchHost,
|
|
14
|
+
SymbioteHostPropsDirective,
|
|
15
|
+
SymbioteStyleInputDirective,
|
|
16
|
+
} from '../../primitives';
|
|
3
17
|
import { SwitchBase } from './shared';
|
|
4
18
|
export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
5
19
|
|
|
6
20
|
@Component({
|
|
7
21
|
selector: 'Switch',
|
|
8
22
|
standalone: true,
|
|
23
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
9
24
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
10
25
|
imports: [SwitchHost, SymbioteHostPropsDirective],
|
|
26
|
+
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Switch), multi: true }],
|
|
11
27
|
inputs: [
|
|
12
28
|
'value',
|
|
13
29
|
'disabled',
|
|
@@ -32,12 +48,12 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
32
48
|
template: `
|
|
33
49
|
<symbiote-switch
|
|
34
50
|
#nativeSwitch="symbioteHost"
|
|
35
|
-
[symbioteHostProps]="hostProps"
|
|
51
|
+
[symbioteHostProps]="hostProps()"
|
|
36
52
|
(change)="handleChange($event, nativeSwitch)"
|
|
37
53
|
/>
|
|
38
54
|
`,
|
|
39
55
|
})
|
|
40
|
-
export class Switch extends SwitchBase {
|
|
56
|
+
export class Switch extends SwitchBase implements DoCheck {
|
|
41
57
|
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
42
58
|
// onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch> leaf one level down.
|
|
43
59
|
private readonly elementRef = inject(ElementRef);
|
|
@@ -51,11 +67,36 @@ export class Switch extends SwitchBase {
|
|
|
51
67
|
}),
|
|
52
68
|
};
|
|
53
69
|
|
|
70
|
+
// The anchor's class-derived style is written by the renderer's addClass/removeClass and never
|
|
71
|
+
// reaches Angular's input pipeline (a bare `class="x"` never appears in SimpleChanges — see
|
|
72
|
+
// anchorHostStyle's doc comment), so SwitchBase's inputsRevision cannot see it. Hence its own
|
|
73
|
+
// signal: a plain field read inside a computed() would be untracked and the bag would go stale
|
|
74
|
+
// the moment a `[class.x]`/`[ngClass]` toggled.
|
|
75
|
+
//
|
|
76
|
+
// ngDoCheck is the right cadence: render3's callHooks flushes a node's pre-order hooks at the
|
|
77
|
+
// next `ɵɵadvance` past it (or in refreshView's post-template flush), i.e. AFTER the declaring
|
|
78
|
+
// template's ɵɵclassProp wrote the class onto this element and BEFORE this component's own view
|
|
79
|
+
// refreshes — the exact window a class poll needs. callHooks also runs under
|
|
80
|
+
// setActiveConsumer(null), so the write registers no dependency on the caller's view (no NG0600).
|
|
81
|
+
// commitClassStyle allocates a fresh style array only when a token actually moved, so signal.set's
|
|
82
|
+
// own Object.is check makes an unchanged poll a no-op that dirties nothing.
|
|
83
|
+
//
|
|
84
|
+
// Known gap, shared with every other component polling this way (Pressable, SafeAreaView,
|
|
85
|
+
// TouchableNativeFeedback): `[ngClass]` applies its tokens from NgClass's OWN ngDoCheck, and a
|
|
86
|
+
// node's hooks flush in registration order with the component's first, so an [ngClass] flip lands
|
|
87
|
+
// one pass after this poll reads it. `class=` and `[class.x]` are written by the declaring
|
|
88
|
+
// template's styling instructions and so are always current here.
|
|
89
|
+
private readonly anchorStyle = signal<unknown>(undefined);
|
|
90
|
+
|
|
91
|
+
ngDoCheck(): void {
|
|
92
|
+
this.anchorStyle.set(anchorHostStyle(this.elementRef));
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
// The anchor's class-derived style goes first, then whatever SwitchBase already resolved
|
|
55
96
|
// (its own explicit `style` @Input plus the ios_backgroundColor fold), so an explicit [style]
|
|
56
97
|
// still beats the ambient class.
|
|
57
|
-
override
|
|
58
|
-
const props = super.
|
|
59
|
-
return { ...props, style: [
|
|
98
|
+
protected override buildHostProps(): Record<string, unknown> {
|
|
99
|
+
const props = super.buildHostProps();
|
|
100
|
+
return { ...props, style: [this.anchorStyle(), props['style']] };
|
|
60
101
|
}
|
|
61
102
|
}
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
forwardRef,
|
|
6
|
+
inject,
|
|
7
|
+
signal,
|
|
8
|
+
type DoCheck,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
|
+
import {
|
|
12
|
+
anchorHostStyle,
|
|
13
|
+
SwitchHost,
|
|
14
|
+
SymbioteHostPropsDirective,
|
|
15
|
+
SymbioteStyleInputDirective,
|
|
16
|
+
} from '../../primitives';
|
|
3
17
|
import { SwitchBase } from './shared';
|
|
4
18
|
export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
5
19
|
|
|
6
20
|
@Component({
|
|
7
21
|
selector: 'Switch',
|
|
8
22
|
standalone: true,
|
|
23
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
9
24
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
10
25
|
imports: [SwitchHost, SymbioteHostPropsDirective],
|
|
26
|
+
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Switch), multi: true }],
|
|
11
27
|
inputs: [
|
|
12
28
|
'value',
|
|
13
29
|
'disabled',
|
|
@@ -32,12 +48,12 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
32
48
|
template: `
|
|
33
49
|
<symbiote-switch
|
|
34
50
|
#nativeSwitch="symbioteHost"
|
|
35
|
-
[symbioteHostProps]="hostProps"
|
|
51
|
+
[symbioteHostProps]="hostProps()"
|
|
36
52
|
(change)="handleChange($event, nativeSwitch)"
|
|
37
53
|
/>
|
|
38
54
|
`,
|
|
39
55
|
})
|
|
40
|
-
export class Switch extends SwitchBase {
|
|
56
|
+
export class Switch extends SwitchBase implements DoCheck {
|
|
41
57
|
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
42
58
|
// onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch> leaf one level down.
|
|
43
59
|
private readonly elementRef = inject(ElementRef);
|
|
@@ -50,11 +66,36 @@ export class Switch extends SwitchBase {
|
|
|
50
66
|
}),
|
|
51
67
|
};
|
|
52
68
|
|
|
69
|
+
// The anchor's class-derived style is written by the renderer's addClass/removeClass and never
|
|
70
|
+
// reaches Angular's input pipeline (a bare `class="x"` never appears in SimpleChanges — see
|
|
71
|
+
// anchorHostStyle's doc comment), so SwitchBase's inputsRevision cannot see it. Hence its own
|
|
72
|
+
// signal: a plain field read inside a computed() would be untracked and the bag would go stale
|
|
73
|
+
// the moment a `[class.x]`/`[ngClass]` toggled.
|
|
74
|
+
//
|
|
75
|
+
// ngDoCheck is the right cadence: render3's callHooks flushes a node's pre-order hooks at the
|
|
76
|
+
// next `ɵɵadvance` past it (or in refreshView's post-template flush), i.e. AFTER the declaring
|
|
77
|
+
// template's ɵɵclassProp wrote the class onto this element and BEFORE this component's own view
|
|
78
|
+
// refreshes — the exact window a class poll needs. callHooks also runs under
|
|
79
|
+
// setActiveConsumer(null), so the write registers no dependency on the caller's view (no NG0600).
|
|
80
|
+
// commitClassStyle allocates a fresh style array only when a token actually moved, so signal.set's
|
|
81
|
+
// own Object.is check makes an unchanged poll a no-op that dirties nothing.
|
|
82
|
+
//
|
|
83
|
+
// Known gap, shared with every other component polling this way (Pressable, SafeAreaView,
|
|
84
|
+
// TouchableNativeFeedback): `[ngClass]` applies its tokens from NgClass's OWN ngDoCheck, and a
|
|
85
|
+
// node's hooks flush in registration order with the component's first, so an [ngClass] flip lands
|
|
86
|
+
// one pass after this poll reads it. `class=` and `[class.x]` are written by the declaring
|
|
87
|
+
// template's styling instructions and so are always current here.
|
|
88
|
+
private readonly anchorStyle = signal<unknown>(undefined);
|
|
89
|
+
|
|
90
|
+
ngDoCheck(): void {
|
|
91
|
+
this.anchorStyle.set(anchorHostStyle(this.elementRef));
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
// The anchor's class-derived style goes first, then whatever SwitchBase already resolved
|
|
54
95
|
// (its own explicit `style` @Input plus the ios_backgroundColor fold), so an explicit [style]
|
|
55
96
|
// still beats the ambient class.
|
|
56
|
-
override
|
|
57
|
-
const props = super.
|
|
58
|
-
return { ...props, style: [
|
|
97
|
+
protected override buildHostProps(): Record<string, unknown> {
|
|
98
|
+
const props = super.buildHostProps();
|
|
99
|
+
return { ...props, style: [this.anchorStyle(), props['style']] };
|
|
59
100
|
}
|
|
60
101
|
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
2
12
|
import { renderSwitch, resolveAccessibilityProps } from '@symbiote-native/components';
|
|
3
13
|
import type {
|
|
4
14
|
ISwitchPlatform,
|
|
@@ -78,7 +88,7 @@ function isSwitchEvent(event: unknown): event is ISymbioteEvent {
|
|
|
78
88
|
// concrete subclasses are real @Components — ngc's AOT build (NG2007) requires it the moment the
|
|
79
89
|
// base itself carries decorated members (@Output() here), mirroring ScrollViewBase.
|
|
80
90
|
@Directive()
|
|
81
|
-
export abstract class SwitchBase implements ISwitchInputs {
|
|
91
|
+
export abstract class SwitchBase implements ISwitchInputs, ControlValueAccessor, OnChanges {
|
|
82
92
|
// The value/change lifecycle as real Angular events: `(valueChange)="onToggle($event)"`, not
|
|
83
93
|
// `[onValueChange]="onToggle"` — mirrors Pressable's press/hover @Output() conversion. Unlike
|
|
84
94
|
// Pressable's createPressHandlers, handleChange() below calls `.emit()` unconditionally: it needs
|
|
@@ -105,9 +115,46 @@ export abstract class SwitchBase implements ISwitchInputs {
|
|
|
105
115
|
'aria-checked': boolean | 'mixed' | undefined;
|
|
106
116
|
|
|
107
117
|
protected abstract readonly platform: ISwitchHostPlatform;
|
|
118
|
+
// NOT a hostProps dependency - only snapBackIfNeeded reads it, and the bag's `value` comes from
|
|
119
|
+
// the @Input, not from this reducer state. So it stays a plain field; if a future change makes
|
|
120
|
+
// the bag read it, it must become a signal first (see inputsRevision's note).
|
|
108
121
|
private switchState: ISwitchState = createInitialSwitchState();
|
|
122
|
+
// Only setDisabledState()/writeValue() need this: @angular/forms mutates `value`/`disabled` from
|
|
123
|
+
// OUTSIDE Angular's own binding path, which doesn't itself schedule a tick under zoneless CD.
|
|
124
|
+
private readonly changeDetector = inject(ChangeDetectorRef);
|
|
125
|
+
|
|
126
|
+
// Bridges the non-reactive @Input fields into the reactive graph so hostProps below can be a
|
|
127
|
+
// memoized computed(). Signal inputs would make this unnecessary, but `input()` is visible only
|
|
128
|
+
// to the AOT compiler and this package's unit suite runs on JIT - see the
|
|
129
|
+
// `angular-adapter-change-detection` skill, §6. `signal()`/`computed()` are plain runtime APIs
|
|
130
|
+
// and need no compiler support, which is what makes this bridge possible at all.
|
|
131
|
+
//
|
|
132
|
+
// Bumped from ngOnChanges (covers every @Input) AND from the two ControlValueAccessor writers
|
|
133
|
+
// below, which assign `value`/`disabled` from OUTSIDE Angular's binding path and so never reach
|
|
134
|
+
// ngOnChanges. ONLY a genuine dependency of the bag may bump it - widening it to "bump on
|
|
135
|
+
// everything" silently un-memoizes the bag and defeats the point. State that is not an @Input
|
|
136
|
+
// and is not one of those two writers needs its OWN signal instead (the platform components'
|
|
137
|
+
// anchor style is exactly that case).
|
|
138
|
+
private readonly inputsRevision = signal(0);
|
|
139
|
+
|
|
140
|
+
ngOnChanges(): void {
|
|
141
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
142
|
+
}
|
|
109
143
|
|
|
110
|
-
|
|
144
|
+
// MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, so
|
|
145
|
+
// `[symbioteHostProps]`'s reference check failed every time and every key was re-pushed through
|
|
146
|
+
// Renderer2 -> routeProp - pure waste when nothing changed. computed() returns the SAME object
|
|
147
|
+
// until a tracked dependency actually changes, and the input setter then skips the whole spread.
|
|
148
|
+
//
|
|
149
|
+
// A computed FIELD cannot be overridden by a subclass accessor the way a getter can, so the
|
|
150
|
+
// platform components override buildHostProps() below and this stays the single memoization
|
|
151
|
+
// point for every platform.
|
|
152
|
+
readonly hostProps = computed<IHostProps>(() => {
|
|
153
|
+
this.inputsRevision();
|
|
154
|
+
return this.buildHostProps();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
protected buildHostProps(): IHostProps {
|
|
111
158
|
const props = resolveAccessibilityProps(this.inputProps());
|
|
112
159
|
const {
|
|
113
160
|
value,
|
|
@@ -168,6 +215,35 @@ export abstract class SwitchBase implements ISwitchInputs {
|
|
|
168
215
|
});
|
|
169
216
|
}
|
|
170
217
|
|
|
218
|
+
// ControlValueAccessor — `value` is a plain field feeding `hostProps` on every CD check, no
|
|
219
|
+
// ngOnChanges/stale-safe commit dance to guard (unlike TextInput, a boolean toggle has no
|
|
220
|
+
// keystroke to race), so writeValue()/setDisabledState() can set it straight.
|
|
221
|
+
writeValue(value: boolean | null): void {
|
|
222
|
+
this.value = value ?? false;
|
|
223
|
+
// `value` feeds hostProps but this write bypasses Angular's binding path, so ngOnChanges never
|
|
224
|
+
// fires for it - without the bump the memoized bag would keep reporting the old value and the
|
|
225
|
+
// switch would stop reflecting a form-driven change.
|
|
226
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
227
|
+
this.changeDetector.markForCheck();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
registerOnChange(fn: (value: boolean) => void): void {
|
|
231
|
+
this.valueChange.subscribe(fn);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
registerOnTouched(fn: () => void): void {
|
|
235
|
+
// Switch has no blur concept (it's a discrete toggle, not a text field) — `change` is the
|
|
236
|
+
// closest touched signal, same convention Angular's own checkbox CVAs use.
|
|
237
|
+
this.change.subscribe(() => fn());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
setDisabledState(isDisabled: boolean): void {
|
|
241
|
+
this.disabled = isDisabled;
|
|
242
|
+
// Same reason as writeValue: an @angular/forms write that never reaches ngOnChanges.
|
|
243
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
244
|
+
this.changeDetector.markForCheck();
|
|
245
|
+
}
|
|
246
|
+
|
|
171
247
|
private inputProps(): Partial<ISwitchProps> {
|
|
172
248
|
const props: Partial<ISwitchProps> = {
|
|
173
249
|
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,6 +82,7 @@ import {
|
|
|
79
82
|
anchorHostStyle,
|
|
80
83
|
MultilineTextInputHost,
|
|
81
84
|
SymbioteHostPropsDirective,
|
|
85
|
+
SymbioteStyleInputDirective,
|
|
82
86
|
TextInputHost,
|
|
83
87
|
} from '../primitives';
|
|
84
88
|
|
|
@@ -154,9 +158,13 @@ export type IAngularTextInputInputs = Omit<
|
|
|
154
158
|
@Component({
|
|
155
159
|
selector: 'TextInput',
|
|
156
160
|
standalone: true,
|
|
161
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
157
162
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
158
163
|
imports: [SymbioteHostPropsDirective, TextInputHost, MultilineTextInputHost],
|
|
159
164
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
165
|
+
providers: [
|
|
166
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInput), multi: true },
|
|
167
|
+
],
|
|
160
168
|
template: `
|
|
161
169
|
@if (isMultiline) {
|
|
162
170
|
<symbiote-text-input-multiline
|
|
@@ -196,7 +204,13 @@ export type IAngularTextInputInputs = Omit<
|
|
|
196
204
|
`,
|
|
197
205
|
})
|
|
198
206
|
export class TextInput
|
|
199
|
-
implements
|
|
207
|
+
implements
|
|
208
|
+
IAngularTextInputInputs,
|
|
209
|
+
ITextInputHandle,
|
|
210
|
+
OnChanges,
|
|
211
|
+
AfterViewInit,
|
|
212
|
+
OnDestroy,
|
|
213
|
+
ControlValueAccessor
|
|
200
214
|
{
|
|
201
215
|
@Input() value?: string;
|
|
202
216
|
@Input() defaultValue?: string;
|
|
@@ -291,6 +305,9 @@ export class TextInput
|
|
|
291
305
|
// onto (see anchorHostStyle's doc comment) — NOT `hostRef` above, which targets the real inner
|
|
292
306
|
// symbiote-text-input(-multiline) one level down.
|
|
293
307
|
private readonly elementRef = inject(ElementRef);
|
|
308
|
+
// Only setDisabledState() needs this: it mutates `editable` from OUTSIDE Angular's own binding
|
|
309
|
+
// path (@angular/forms calling in directly), which doesn't itself schedule a tick under zoneless CD.
|
|
310
|
+
private readonly changeDetector = inject(ChangeDetectorRef);
|
|
294
311
|
|
|
295
312
|
// The count native last acknowledged; the controlled write / clear / setSelection echo it so
|
|
296
313
|
// native's eventLag lands on 0 and the write applies. A plain field (read live by the imperative
|
|
@@ -304,6 +321,9 @@ export class TextInput
|
|
|
304
321
|
// synchronous focus getter (RN's TextInputState holds the same).
|
|
305
322
|
private focused = false;
|
|
306
323
|
private cancelAutoFocus?: () => void;
|
|
324
|
+
// Mirrors SimpleChange.firstChange for the writeValue() path below (@angular/forms' own initial
|
|
325
|
+
// write, distinct from ngOnChanges' firstChange, which never fires for a forms-driven write).
|
|
326
|
+
private isFirstFormWrite = true;
|
|
307
327
|
|
|
308
328
|
get isMultiline(): boolean {
|
|
309
329
|
return this.multiline === true;
|
|
@@ -323,14 +343,44 @@ export class TextInput
|
|
|
323
343
|
ngOnChanges(changes: SimpleChanges): void {
|
|
324
344
|
const valueChange = changes['value'];
|
|
325
345
|
if (valueChange === undefined) return;
|
|
326
|
-
// The first value is the seed, not a divergence: record it and do not command.
|
|
327
346
|
if (valueChange.firstChange) {
|
|
347
|
+
// The initial value already reached native via createNode's hostProps — seed
|
|
348
|
+
// lastNativeText instead of re-commanding it (see commitControlledValue's own comment on
|
|
349
|
+
// why a redundant re-push isn't just wasteful — it can race a keystroke).
|
|
328
350
|
this.lastNativeText = foldText(this.value, this.defaultValue);
|
|
329
351
|
return;
|
|
330
352
|
}
|
|
331
353
|
this.commitControlledValue();
|
|
332
354
|
}
|
|
333
355
|
|
|
356
|
+
// ControlValueAccessor — @angular/forms calls these straight into this component, bypassing
|
|
357
|
+
// template bindings/ngOnChanges entirely. `isFirstFormWrite` mirrors SimpleChange.firstChange:
|
|
358
|
+
// the initial value already reached native via createNode's hostProps, so it must seed
|
|
359
|
+
// lastNativeText, not re-command (see commitControlledValue's own comment on why a redundant
|
|
360
|
+
// re-push isn't just wasteful — it can race a keystroke).
|
|
361
|
+
writeValue(value: string | null): void {
|
|
362
|
+
this.value = value ?? undefined;
|
|
363
|
+
if (this.isFirstFormWrite) {
|
|
364
|
+
this.isFirstFormWrite = false;
|
|
365
|
+
this.lastNativeText = foldText(this.value, this.defaultValue);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this.commitControlledValue();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
registerOnChange(fn: (value: string) => void): void {
|
|
372
|
+
this.valueChange.subscribe(fn);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
registerOnTouched(fn: () => void): void {
|
|
376
|
+
this.blurEvent.subscribe(() => fn());
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
setDisabledState(isDisabled: boolean): void {
|
|
380
|
+
this.editable = !isDisabled;
|
|
381
|
+
this.changeDetector.markForCheck();
|
|
382
|
+
}
|
|
383
|
+
|
|
334
384
|
// autoFocus is driven in JS, not as a native prop: once the node is committed, command `focus`
|
|
335
385
|
// down once (RN does the same via TextInputState.focusInput). Under Angular's async-batched
|
|
336
386
|
// commit the node has no Fabric tag at ngAfterViewInit, so whenCommitted defers the command to
|
|
@@ -26,11 +26,15 @@ import {
|
|
|
26
26
|
CUSTOM_ELEMENTS_SCHEMA,
|
|
27
27
|
ChangeDetectionStrategy,
|
|
28
28
|
Component,
|
|
29
|
+
computed,
|
|
30
|
+
type DoCheck,
|
|
29
31
|
ElementRef,
|
|
30
32
|
EventEmitter,
|
|
31
33
|
inject,
|
|
32
34
|
Input,
|
|
35
|
+
type OnChanges,
|
|
33
36
|
Output,
|
|
37
|
+
signal,
|
|
34
38
|
} from '@angular/core';
|
|
35
39
|
import {
|
|
36
40
|
createTouchableFeedbackHandlers,
|
|
@@ -53,7 +57,7 @@ import {
|
|
|
53
57
|
type ITouchableFeedbackHandlers,
|
|
54
58
|
} from '@symbiote-native/components';
|
|
55
59
|
import { type ISymbioteEvent, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
|
|
56
|
-
import { anchorHostStyle, anchorStyleProp } from '../../primitives';
|
|
60
|
+
import { anchorHostStyle, anchorStyleProp, SymbioteStyleInputDirective } from '../../primitives';
|
|
57
61
|
import { Pressable, type IAngularPressableInputs } from '../pressable';
|
|
58
62
|
import { Animated, AnimatedView } from '../../modules/animated';
|
|
59
63
|
|
|
@@ -88,6 +92,7 @@ function scheduleTimeout(callback: () => void, ms: number): () => void {
|
|
|
88
92
|
@Component({
|
|
89
93
|
selector: 'TouchableOpacity',
|
|
90
94
|
standalone: true,
|
|
95
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
91
96
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
92
97
|
imports: [Pressable, AnimatedView],
|
|
93
98
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -163,7 +168,7 @@ function scheduleTimeout(callback: () => void, ms: number): () => void {
|
|
|
163
168
|
</Pressable>
|
|
164
169
|
`,
|
|
165
170
|
})
|
|
166
|
-
export class TouchableOpacity implements IAngularTouchableOpacityProps {
|
|
171
|
+
export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck {
|
|
167
172
|
@Output() readonly press = new EventEmitter<ISymbioteEvent>();
|
|
168
173
|
@Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
|
|
169
174
|
@Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
|
|
@@ -249,7 +254,21 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps {
|
|
|
249
254
|
// the commit layer flattens). `unknown` because the array mixes a plain style with an
|
|
250
255
|
// AnimatedNode, exactly the bag AnimatedView's `style` input accepts.
|
|
251
256
|
get animatedStyle(): unknown {
|
|
252
|
-
return [
|
|
257
|
+
return [this.anchorStyle(), this.style, { opacity: this.opacity }];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The anchor's class-derived style is written by the renderer's addClass/removeClass at the USE
|
|
261
|
+
// SITE - it never appears in SimpleChanges, and nothing about it dirties THIS component's view.
|
|
262
|
+
// A `class=` present at creation therefore worked, while one toggled later did not: the parent
|
|
263
|
+
// refreshed, the class landed on the anchor, and this component's own view was never refreshed,
|
|
264
|
+
// so whatever merged the anchor style in was never re-evaluated. Polling it into a signal from
|
|
265
|
+
// ngDoCheck fixes both halves: ngDoCheck runs during the PARENT's refresh even when this view is
|
|
266
|
+
// skipped, and the signal write is what then marks this view for refresh. `signal.set`'s own
|
|
267
|
+
// Object.is makes an unchanged poll a no-op, so there is no loop.
|
|
268
|
+
private readonly anchorStyle = signal<unknown>(undefined);
|
|
269
|
+
|
|
270
|
+
ngDoCheck(): void {
|
|
271
|
+
this.anchorStyle.set(anchorHostStyle(this.elementRef));
|
|
253
272
|
}
|
|
254
273
|
|
|
255
274
|
private setOpacityTo(toValue: number, duration: number): void {
|
|
@@ -304,11 +323,12 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps {
|
|
|
304
323
|
@Component({
|
|
305
324
|
selector: 'TouchableHighlight',
|
|
306
325
|
standalone: true,
|
|
326
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
307
327
|
imports: [Pressable],
|
|
308
328
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
309
329
|
template: `
|
|
310
330
|
<Pressable
|
|
311
|
-
[style]="pressedStyle"
|
|
331
|
+
[style]="pressedStyle()"
|
|
312
332
|
(press)="press.emit($event)"
|
|
313
333
|
(pressIn)="pressIn.emit($event)"
|
|
314
334
|
(pressOut)="pressOut.emit($event)"
|
|
@@ -377,7 +397,7 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps {
|
|
|
377
397
|
</Pressable>
|
|
378
398
|
`,
|
|
379
399
|
})
|
|
380
|
-
export class TouchableHighlight implements IAngularTouchableHighlightProps {
|
|
400
|
+
export class TouchableHighlight implements IAngularTouchableHighlightProps, OnChanges, DoCheck {
|
|
381
401
|
@Output() readonly press = new EventEmitter<ISymbioteEvent>();
|
|
382
402
|
@Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
|
|
383
403
|
@Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
|
|
@@ -458,20 +478,46 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps {
|
|
|
458
478
|
// [style] still beats the ambient class. When pressed, paint the underlay color + lower the
|
|
459
479
|
// child opacity (RN drives this with setState, not Animated, so we mirror that faithfully
|
|
460
480
|
// through Pressable's pressed flag, no tween) — the overlay always goes last so it wins.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
481
|
+
// A computed that RETURNS A NEW ARROW when its dependencies move, not one stable arrow forever.
|
|
482
|
+
// The stable-arrow form was silently frozen: Pressable's `style` @Input never reported a change,
|
|
483
|
+
// so Pressable's view never refreshed and never re-invoked the arrow - a class or [style]
|
|
484
|
+
// toggled after mount never reached the committed view. Reading the anchor signal from inside
|
|
485
|
+
// the arrow body instead would have worked by accident, by registering THIS component's signal
|
|
486
|
+
// on Pressable's consumer; returning a new reference keeps the dependency where it belongs, in
|
|
487
|
+
// an ordinary input change.
|
|
488
|
+
readonly pressedStyle = computed<(state: IPressState) => IStyleProp<IViewStyle>>(() => {
|
|
489
|
+
this.inputsRevision();
|
|
490
|
+
const base: IStyleProp<IViewStyle> = [this.anchorStyle(), this.style];
|
|
491
|
+
const underlayColor = this.underlayColor ?? DEFAULT_UNDERLAY_COLOR;
|
|
492
|
+
const childOpacity = this.activeOpacity ?? DEFAULT_HIGHLIGHT_CHILD_OPACITY;
|
|
493
|
+
return (state: IPressState): IStyleProp<IViewStyle> =>
|
|
494
|
+
highlightPressedStyle(state.pressed, base, underlayColor, childOpacity);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// `style`, `underlayColor` and `activeOpacity` are read untracked inside that computed, so the
|
|
498
|
+
// bump is what tells it they moved. See the ngDoCheck note below for the anchor half, which
|
|
499
|
+
// ngOnChanges cannot cover.
|
|
500
|
+
private readonly inputsRevision = signal(0);
|
|
501
|
+
private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(undefined);
|
|
502
|
+
|
|
503
|
+
ngOnChanges(): void {
|
|
504
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// The anchor's class-derived style is written by the renderer's addClass/removeClass at the USE
|
|
508
|
+
// SITE - it never appears in SimpleChanges, and nothing about it dirties THIS component's view.
|
|
509
|
+
// ngDoCheck runs during the PARENT's refresh even when this view is skipped, and the signal
|
|
510
|
+
// write is what then marks this view for refresh. `signal.set`'s own Object.is makes an
|
|
511
|
+
// unchanged poll a no-op, so there is no loop.
|
|
512
|
+
ngDoCheck(): void {
|
|
513
|
+
this.anchorStyle.set(anchorStyleProp<IViewStyle>(this.elementRef));
|
|
514
|
+
}
|
|
470
515
|
}
|
|
471
516
|
|
|
472
517
|
@Component({
|
|
473
518
|
selector: 'TouchableWithoutFeedback',
|
|
474
519
|
standalone: true,
|
|
520
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
475
521
|
imports: [Pressable],
|
|
476
522
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
477
523
|
template: `
|
|
@@ -545,7 +591,7 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps {
|
|
|
545
591
|
</Pressable>
|
|
546
592
|
`,
|
|
547
593
|
})
|
|
548
|
-
export class TouchableWithoutFeedback implements IAngularTouchableWithoutFeedbackProps {
|
|
594
|
+
export class TouchableWithoutFeedback implements DoCheck, IAngularTouchableWithoutFeedbackProps {
|
|
549
595
|
// delayPressIn/delayPressOut/minPressDuration ride the prop type for surface parity with the
|
|
550
596
|
// other two, but TouchableWithoutFeedback has no visual feedback, so nothing schedules off them
|
|
551
597
|
// (RN's TouchableWithoutFeedback is the same pure passthrough).
|
|
@@ -624,6 +670,20 @@ export class TouchableWithoutFeedback implements IAngularTouchableWithoutFeedbac
|
|
|
624
670
|
// The anchor's class-derived style goes first, then the explicit style, so an explicit [style]
|
|
625
671
|
// still beats the ambient class.
|
|
626
672
|
get mergedStyle(): IStyleProp<IViewStyle> {
|
|
627
|
-
return [
|
|
673
|
+
return [this.anchorStyle(), this.style];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// The anchor's class-derived style is written by the renderer's addClass/removeClass at the USE
|
|
677
|
+
// SITE - it never appears in SimpleChanges, and nothing about it dirties THIS component's view.
|
|
678
|
+
// A `class=` present at creation therefore worked, while one toggled later did not: the parent
|
|
679
|
+
// refreshed, the class landed on the anchor, and this component's own view was never refreshed,
|
|
680
|
+
// so whatever merged the anchor style in was never re-evaluated. Polling it into a signal from
|
|
681
|
+
// ngDoCheck fixes both halves: ngDoCheck runs during the PARENT's refresh even when this view is
|
|
682
|
+
// skipped, and the signal write is what then marks this view for refresh. `signal.set`'s own
|
|
683
|
+
// Object.is makes an unchanged poll a no-op, so there is no loop.
|
|
684
|
+
private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(undefined);
|
|
685
|
+
|
|
686
|
+
ngDoCheck(): void {
|
|
687
|
+
this.anchorStyle.set(anchorStyleProp<IViewStyle>(this.elementRef));
|
|
628
688
|
}
|
|
629
689
|
}
|