feat(forms): expose element on signal forms Field directive

This allows for easier focusing of the relevant element based on the
`field` property of a `ValidationError`

(cherry picked from commit aff8b248b3)
This commit is contained in:
Miles Malerba 2025-12-09 09:47:51 -08:00 committed by Alex Rickabaugh
parent 91532626a9
commit f254ff4f2e
3 changed files with 20 additions and 14 deletions

View file

@ -20,7 +20,6 @@ import { ɵCONTROL } from '@angular/core';
import { ɵControl } from '@angular/core';
import { ɵcontrolUpdate } from '@angular/core';
import { ɵFieldState } from '@angular/core';
import { ɵInteropControl } from '@angular/core';
import { ɵɵcontrolCreate } from '@angular/core';
// @public

View file

@ -30,7 +30,6 @@ import { ɵCONTROL } from '@angular/core';
import { ɵControl } from '@angular/core';
import { ɵcontrolUpdate } from '@angular/core';
import { ɵFieldState } from '@angular/core';
import { ɵInteropControl } from '@angular/core';
import { ɵɵcontrolCreate } from '@angular/core';
// @public
@ -147,19 +146,18 @@ export class Field<T> implements ɵControl<T> {
readonly update: typeof ɵcontrolUpdate;
};
// (undocumented)
readonly classes: (readonly [string, i0.Signal<boolean>])[];
readonly element: HTMLElement;
// (undocumented)
readonly field: i0.InputSignal<FieldTree<T>>;
protected getOrCreateNgControl(): InteropNgControl;
// (undocumented)
readonly injector: Injector;
// (undocumented)
readonly state: i0.Signal<[T] extends [_angular_forms.AbstractControl<any, any, any>] ? CompatFieldState<T, string | number> : FieldState<T, string | number>>;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<Field<any>, "[field]", never, { "field": { "alias": "field"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<Field<any>, never>;
get ɵinteropControl(): ɵInteropControl | undefined;
// (undocumented)
ɵregister(): void;
}
// @public

View file

@ -8,17 +8,18 @@
import {
computed,
ɵɵcontrolCreate as createControlBinding,
Directive,
effect,
ElementRef,
inject,
InjectionToken,
Injector,
input,
ɵcontrolUpdate as updateControlBinding,
ɵCONTROL,
ɵControl,
ɵcontrolUpdate as updateControlBinding,
ɵInteropControl,
ɵɵcontrolCreate as createControlBinding,
} from '@angular/core';
import {NG_VALUE_ACCESSOR, NgControl} from '@angular/forms';
import {InteropNgControl} from '../controls/interop_ng_control';
@ -71,14 +72,18 @@ const controlInstructions = {
],
})
export class Field<T> implements ɵControl<T> {
private readonly injector = inject(Injector);
readonly element = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
readonly injector = inject(Injector);
readonly field = input.required<FieldTree<T>>();
readonly state = computed(() => this.field()());
readonly [ɵCONTROL] = controlInstructions;
private config = inject(SIGNAL_FORMS_CONFIG, {optional: true});
/** @internal */
readonly classes = Object.entries(this.config?.classes ?? {}).map(
([className, computation]) => [className, computed(() => computation(this.state()))] as const,
);
readonly field = input.required<FieldTree<T>>();
readonly state = computed(() => this.field()());
readonly [ɵCONTROL] = controlInstructions;
/** Any `ControlValueAccessor` instances provided on the host element. */
private readonly controlValueAccessors = inject(NG_VALUE_ACCESSOR, {optional: true, self: true});
@ -86,7 +91,11 @@ export class Field<T> implements ɵControl<T> {
/** A lazily instantiated fake `NgControl`. */
private interopNgControl: InteropNgControl | undefined;
/** A `ControlValueAccessor`, if configured, for the host component. */
/**
* A `ControlValueAccessor`, if configured, for the host component.
*
* @internal
*/
get ɵinteropControl(): ɵInteropControl | undefined {
return this.controlValueAccessors?.[0] ?? this.interopNgControl?.valueAccessor ?? undefined;
}
@ -96,7 +105,7 @@ export class Field<T> implements ɵControl<T> {
return (this.interopNgControl ??= new InteropNgControl(this.state));
}
// TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131861631
/** @internal */
ɵregister() {
// Register this control on the field it is currently bound to. We do this at the end of
// initialization so that it only runs if we are actually syncing with this control