Skip to content

Commit 3e7ce0d

Browse files
leonsenftatscott
authored andcommitted
fix(forms): restrict SignalFormsConfig to a readonly API
Introduce `FormFieldBinding` to represent a binding between a field and a UI control through a `FormField` directive. This interface is used to restrict `SignalFormsConfig` and `formFieldBindings` to a readonly API. Fix #65779.
1 parent a1a6c52 commit 3e7ce0d

4 files changed

Lines changed: 48 additions & 9 deletions

File tree

goldens/public-api/forms/signals/index.api.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ export class FormField<T> {
180180
static ɵfac: i0.ɵɵFactoryDeclaration<FormField<any>, never>;
181181
}
182182

183+
// @public
184+
export interface FormFieldBinding {
185+
readonly element: HTMLElement;
186+
focus(options?: FocusOptions): void;
187+
readonly injector: Injector;
188+
readonly state: Signal<ReadonlyFieldState<unknown>>;
189+
}
190+
183191
// @public (undocumented)
184192
export interface FormFieldBindingOptions {
185193
readonly focus?: (focusOptions?: FocusOptions) => void;
@@ -480,7 +488,7 @@ export interface ReadonlyFieldState<TValue, TKey extends string | number = strin
480488
readonly errorSummary: Signal<ValidationError.WithFieldTree[]>;
481489
readonly fieldTree: ReadonlyFieldTree<unknown, TKey>;
482490
focusBoundControl(options?: FocusOptions): void;
483-
readonly formFieldBindings: Signal<readonly FormField<unknown>[]>;
491+
readonly formFieldBindings: Signal<readonly FormFieldBinding[]>;
484492
hasMetadata(key: MetadataKey<any, any, any>): boolean;
485493
readonly hidden: Signal<boolean>;
486494
readonly invalid: Signal<boolean>;
@@ -580,7 +588,7 @@ export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> =
580588
// @public
581589
export interface SignalFormsConfig {
582590
classes?: {
583-
[className: string]: (state: FormField<unknown>) => boolean;
591+
[className: string]: (formField: FormFieldBinding) => boolean;
584592
};
585593
}
586594

packages/forms/signals/src/api/di.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import {type Provider} from '@angular/core';
10+
import type {FormFieldBinding} from '../api/types';
1011
import {SIGNAL_FORMS_CONFIG} from '../field/di';
11-
import type {FormField} from '../directive/form_field_directive';
1212

1313
/**
1414
* Configuration options for signal forms.
@@ -17,7 +17,9 @@ import type {FormField} from '../directive/form_field_directive';
1717
*/
1818
export interface SignalFormsConfig {
1919
/** A map of CSS class names to predicate functions that determine when to apply them. */
20-
classes?: {[className: string]: (state: FormField<unknown>) => boolean};
20+
classes?: {
21+
[className: string]: (formField: FormFieldBinding) => boolean;
22+
};
2123
}
2224

2325
/**

packages/forms/signals/src/api/types.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Signal, WritableSignal} from '@angular/core';
9+
import {Injector, Signal, WritableSignal} from '@angular/core';
1010
import {AbstractControl} from '@angular/forms';
1111
import type {FormField} from '../directive/form_field_directive';
1212
import type {MetadataKey, ValidationError} from './rules';
@@ -451,7 +451,7 @@ export interface ReadonlyFieldState<TValue, TKey extends string | number = strin
451451
/**
452452
* The {@link FormField} directives that bind this field to a UI control.
453453
*/
454-
readonly formFieldBindings: Signal<readonly FormField<unknown>[]>;
454+
readonly formFieldBindings: Signal<readonly FormFieldBinding[]>;
455455

456456
/**
457457
* Reads a metadata value from the field.
@@ -573,6 +573,36 @@ export type FieldStateByMode<
573573
TMode extends 'writable' | 'readonly',
574574
> = TMode extends 'writable' ? FieldState<TValue, TKey> : ReadonlyFieldState<TValue, TKey>;
575575

576+
/**
577+
* Represents a binding between a field and a UI control through a {@link FormField} directive.
578+
*
579+
* @experimental 21.3.0
580+
*/
581+
export interface FormFieldBinding {
582+
/**
583+
* The HTML element on which the {@link FormField} directive is applied.
584+
*/
585+
readonly element: HTMLElement;
586+
587+
/**
588+
* The node injector for the element hosting this field binding.
589+
*/
590+
readonly injector: Injector;
591+
592+
/**
593+
* The {@link FieldState} of the field bound to the {@link FormField} directive.
594+
*/
595+
readonly state: Signal<ReadonlyFieldState<unknown>>;
596+
597+
/**
598+
* Focuses this field binding.
599+
*
600+
* By default, this will focus {@link element}. However, custom controls can implement their own
601+
* focus behavior.
602+
*/
603+
focus(options?: FocusOptions): void;
604+
}
605+
576606
/**
577607
* Allows declaring whether the Rules are supported for a given path.
578608
*

packages/forms/signals/src/directive/form_field_directive.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class FormField<T> {
115115
readonly destroyRef = inject(DestroyRef);
116116

117117
/**
118-
* The node injector for the element this field binding.
118+
* The node injector for the DOM element hosting this field binding.
119119
*/
120120
readonly injector = inject(Injector);
121121

@@ -196,8 +196,7 @@ export class FormField<T> {
196196
*/
197197
private installClassBindingEffect(): void {
198198
const classes = Object.entries(this.config?.classes ?? {}).map(
199-
([className, computation]) =>
200-
[className, computed(() => computation(this as FormField<unknown>))] as const,
199+
([className, computation]) => [className, computed(() => computation(this))] as const,
201200
);
202201
if (classes.length === 0) {
203202
return;

0 commit comments

Comments
 (0)