@@ -16,17 +16,32 @@ import {
1616 InjectionToken ,
1717 Injector ,
1818 input ,
19+ ɵRuntimeError as RuntimeError ,
20+ signal ,
21+ untracked ,
1922 ɵcontrolUpdate as updateControlBinding ,
2023 ɵCONTROL ,
2124 ɵInteropControl ,
25+ type ɵFormFieldBindingOptions ,
2226 type ɵFormFieldDirective ,
2327} from '@angular/core' ;
2428import { NG_VALUE_ACCESSOR , NgControl } from '@angular/forms' ;
2529import { InteropNgControl } from '../controls/interop_ng_control' ;
30+ import { SignalFormsErrorCode } from '../errors' ;
2631import { SIGNAL_FORMS_CONFIG } from '../field/di' ;
2732import type { FieldNode } from '../field/node' ;
2833import type { FieldTree } from './types' ;
2934
35+ export interface FormFieldBindingOptions extends ɵFormFieldBindingOptions {
36+ /**
37+ * Focuses the binding.
38+ *
39+ * If not specified, Signal Forms will attempt to focus the host element of the `FormField` when
40+ * asked to focus this binding.
41+ */
42+ focus ?: VoidFunction ;
43+ }
44+
3045/**
3146 * Lightweight DI token provided by the {@link FormField} directive.
3247 *
@@ -79,6 +94,7 @@ export class FormField<T> {
7994 readonly injector = inject ( Injector ) ;
8095 readonly formField = input . required < FieldTree < T > > ( ) ;
8196 readonly state = computed ( ( ) => this . formField ( ) ( ) ) ;
97+ private readonly bindingOptions = signal < FormFieldBindingOptions | undefined > ( undefined ) ;
8298
8399 readonly [ ɵCONTROL ] = controlInstructions ;
84100
@@ -109,8 +125,21 @@ export class FormField<T> {
109125 return ( this . interopNgControl ??= new InteropNgControl ( this . state ) ) ;
110126 }
111127
112- /** @internal */
113- ɵregister ( ) {
128+ /**
129+ * Registers this `FormField` as a binding on its associated `FieldState`.
130+ *
131+ * This method should be called at most once for a given `FormField`. A `FormField` placed on a
132+ * custom control (`FormUiControl`) automatically registers that custom control as a binding.
133+ */
134+ registerAsBinding ( bindingOptions ?: FormFieldBindingOptions ) {
135+ if ( untracked ( this . bindingOptions ) ) {
136+ throw new RuntimeError (
137+ SignalFormsErrorCode . BINDING_ALREADY_REGISTERED ,
138+ ngDevMode && 'FormField already registered as a binding' ,
139+ ) ;
140+ }
141+
142+ this . bindingOptions . set ( bindingOptions ) ;
114143 // Register this control on the field state it is currently bound to. We do this at the end of
115144 // initialization so that it only runs if we are actually syncing with this control
116145 // (as opposed to just passing the field state through to its `formField` input).
@@ -132,7 +161,14 @@ export class FormField<T> {
132161 }
133162
134163 /** Focuses this UI control. */
135- focus ?( ) : void ;
164+ focus ( ) {
165+ const bindingOptions = untracked ( this . bindingOptions ) ;
166+ if ( bindingOptions ?. focus ) {
167+ bindingOptions . focus ( ) ;
168+ } else {
169+ this . element . focus ( ) ;
170+ }
171+ }
136172}
137173
138174// We can't add `implements ɵFormFieldDirective<T>` to `Field` even though it should conform to the interface.
0 commit comments