From f254ff4f2e014064b4d6073341dec0c5a7a754bf Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 9 Dec 2025 09:47:51 -0800 Subject: [PATCH] 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 aff8b248b35931cbe7d644ad2594144b75064ca6) --- .../forms/signals/compat/index.api.md | 1 - goldens/public-api/forms/signals/index.api.md | 8 +++--- .../forms/signals/src/api/field_directive.ts | 25 +++++++++++++------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/goldens/public-api/forms/signals/compat/index.api.md b/goldens/public-api/forms/signals/compat/index.api.md index e70d55f0a00..737d0a695af 100644 --- a/goldens/public-api/forms/signals/compat/index.api.md +++ b/goldens/public-api/forms/signals/compat/index.api.md @@ -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 diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index 2bb34f78eb1..bb98a731c25 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -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 implements ɵControl { readonly update: typeof ɵcontrolUpdate; }; // (undocumented) - readonly classes: (readonly [string, i0.Signal])[]; + readonly element: HTMLElement; // (undocumented) readonly field: i0.InputSignal>; protected getOrCreateNgControl(): InteropNgControl; // (undocumented) + readonly injector: Injector; + // (undocumented) readonly state: i0.Signal<[T] extends [_angular_forms.AbstractControl] ? CompatFieldState : FieldState>; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration, "[field]", never, { "field": { "alias": "field"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; - get ɵinteropControl(): ɵInteropControl | undefined; - // (undocumented) - ɵregister(): void; } // @public diff --git a/packages/forms/signals/src/api/field_directive.ts b/packages/forms/signals/src/api/field_directive.ts index 4714542dbdd..9f91896f0f1 100644 --- a/packages/forms/signals/src/api/field_directive.ts +++ b/packages/forms/signals/src/api/field_directive.ts @@ -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 implements ɵControl { - private readonly injector = inject(Injector); + readonly element = inject>(ElementRef).nativeElement; + readonly injector = inject(Injector); + readonly field = input.required>(); + 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>(); - 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 implements ɵControl { /** 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 implements ɵControl { 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