@@ -18,6 +18,7 @@ import {
18
18
InputSignal ,
19
19
OutputEmitterRef ,
20
20
OutputRef ,
21
+ OutputRefSubscription ,
21
22
untracked ,
22
23
} from '@angular/core' ;
23
24
import { ControlValueAccessor , NG_VALUE_ACCESSOR , NgControl } from '@angular/forms' ;
@@ -38,7 +39,7 @@ import {InteropNgControl} from './interop_ng_control';
38
39
export class FieldDirective < T > {
39
40
readonly injector = inject ( Injector ) ;
40
41
readonly field = input . required < Field < T > > ( ) ;
41
- readonly el = inject ( ElementRef ) ;
42
+ readonly el : ElementRef < HTMLElement > = inject ( ElementRef ) ;
42
43
readonly cvaArray = inject < ControlValueAccessor [ ] > ( NG_VALUE_ACCESSOR , { optional : true } ) ;
43
44
44
45
private _ngControl : InteropNgControl | undefined ;
@@ -107,12 +108,28 @@ export class FieldDirective<T> {
107
108
const cleanupValue = cmp . value . subscribe ( ( newValue ) =>
108
109
this . field ( ) . $state . value . set ( newValue ) ,
109
110
) ;
110
- const cleanupTouch = cmp . touch ?. subscribe ( ( ) => this . field ( ) . $state . markAsTouched ( ) ) ;
111
+ let cleanupTouch : OutputRefSubscription | undefined ;
112
+ let cleanupDefaultTouch : ( ( ) => void ) | undefined ;
113
+ if ( cmp . touch !== undefined ) {
114
+ cleanupTouch = cmp . touch . subscribe ( ( ) => this . field ( ) . $state . markAsTouched ( ) ) ;
115
+ } else {
116
+ // If the component did not give us a touch event stream, use the standard touch logic,
117
+ // marking it touched when the focus moves from inside the host element to outside.
118
+ const listener = ( event : FocusEvent ) => {
119
+ const newActiveEl = event . relatedTarget ;
120
+ if ( ! this . el . nativeElement . contains ( newActiveEl as Element | null ) ) {
121
+ this . field ( ) . $state . markAsTouched ( ) ;
122
+ }
123
+ } ;
124
+ this . el . nativeElement . addEventListener ( 'focusout' , listener ) ;
125
+ cleanupDefaultTouch = ( ) => this . el . nativeElement . removeEventListener ( 'focusout' , listener ) ;
126
+ }
111
127
112
128
// Cleanup for output binding subscriptions:
113
129
injector . get ( DestroyRef ) . onDestroy ( ( ) => {
114
130
cleanupValue . unsubscribe ( ) ;
115
131
cleanupTouch ?. unsubscribe ( ) ;
132
+ cleanupDefaultTouch ?.( ) ;
116
133
} ) ;
117
134
} else {
118
135
throw new Error ( `Unhandled control?` ) ;
0 commit comments