diff --git a/goldens/public-api/forms/signals/compat/index.api.md b/goldens/public-api/forms/signals/compat/index.api.md index a32d2568bdc..5a15dfe09a3 100644 --- a/goldens/public-api/forms/signals/compat/index.api.md +++ b/goldens/public-api/forms/signals/compat/index.api.md @@ -5,7 +5,6 @@ ```ts import { AbstractControl } from '@angular/forms'; -import * as _angular_forms from '@angular/forms'; import { ControlValueAccessor } from '@angular/forms'; import { EventEmitter } from '@angular/core'; import { FormControlState } from '@angular/forms'; diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index f700d84975d..0084d2a9ffe 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -5,7 +5,6 @@ ```ts import { AbstractControl } from '@angular/forms'; -import * as _angular_forms from '@angular/forms'; import { ControlValueAccessor } from '@angular/forms'; import { FormControlStatus } from '@angular/forms'; import { HttpResourceOptions } from '@angular/common/http'; @@ -121,6 +120,9 @@ export class EmailValidationError extends BaseNgValidationError { readonly kind = "email"; } +// @public +export type Field = () => FieldState; + // @public export type FieldContext = TPathKind extends PathKind.Item ? ItemFieldContext : TPathKind extends PathKind.Child ? ChildFieldContext : RootFieldContext; @@ -189,14 +191,14 @@ export class FormField { readonly element: HTMLElement; readonly errors: Signal; // (undocumented) - readonly fieldTree: i0.InputSignal>; + readonly field: i0.InputSignal>; focus(options?: FocusOptions): void; readonly injector: Injector; protected get interopNgControl(): InteropNgControl; registerAsBinding(bindingOptions?: FormFieldBindingOptions): void; - readonly state: Signal<[T] extends [_angular_forms.AbstractControl] ? CompatFieldState : FieldState>; + readonly state: Signal>; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[formField]", ["formField"], { "fieldTree": { "alias": "formField"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[formField]", ["formField"], { "field": { "alias": "formField"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } diff --git a/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts b/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts index 792aff707bc..3392b885ddf 100644 --- a/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts +++ b/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts @@ -48,7 +48,7 @@ runInEachFileSystem(() => { const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( - `Type 'null' is not assignable to type 'FieldTree'.`, + `Type 'null' is not assignable to type 'Field'.`, ); }); @@ -70,7 +70,7 @@ runInEachFileSystem(() => { const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( - `Type 'string' is not assignable to type 'FieldTree'.`, + `Type 'string' is not assignable to type 'Field'.`, ); }); diff --git a/packages/forms/signals/src/api/types.ts b/packages/forms/signals/src/api/types.ts index 20c663d5f9d..16b91de0a3c 100644 --- a/packages/forms/signals/src/api/types.ts +++ b/packages/forms/signals/src/api/types.ts @@ -171,6 +171,20 @@ export type AsyncValidationResult = | ValidationResult | 'pending'; +/** + * A field accessor function that returns the state of the field. + * + * @template TValue The type of the value stored in the field. + * @template TKey The type of the property key which this field resides under in its parent. + * + * @category types + * @experimental 21.2.0 + */ +export type Field = () => FieldState< + TValue, + TKey +>; + /** * An object that represents a tree of fields in a form. This includes both primitive value fields * (e.g. fields that contain a `string` or `number`), as well as "grouping fields" that contain diff --git a/packages/forms/signals/src/directive/form_field_directive.ts b/packages/forms/signals/src/directive/form_field_directive.ts index 3ab6897266c..91a09d44031 100644 --- a/packages/forms/signals/src/directive/form_field_directive.ts +++ b/packages/forms/signals/src/directive/form_field_directive.ts @@ -26,7 +26,7 @@ import { } from '@angular/core'; import {type ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl} from '@angular/forms'; import {type ValidationError} from '../api/rules'; -import type {FieldTree} from '../api/types'; +import type {Field} from '../api/types'; import {InteropNgControl} from '../controls/interop_ng_control'; import {RuntimeErrorCode} from '../errors'; import {SIGNAL_FORMS_CONFIG} from '../field/di'; @@ -97,7 +97,7 @@ export const FORM_FIELD = new InjectionToken>( ], }) export class FormField { - readonly fieldTree = input.required>({alias: 'formField'}); + readonly field = input.required>({alias: 'formField'}); /** @internal */ readonly renderer = inject(Renderer2); @@ -108,7 +108,7 @@ export class FormField { /** * `FieldState` for the currently bound field. */ - readonly state = computed(() => this.fieldTree()()); + readonly state = computed(() => this.field()()); /** * The node injector for the element this field binding. @@ -162,7 +162,7 @@ export class FormField { () => this.parseErrorsSource()?.().map((err) => ({ ...err, - fieldTree: untracked(this.fieldTree), + fieldTree: untracked(this.state).fieldTree, formField: this as FormField, })) ?? [], ); diff --git a/packages/forms/signals/test/web/compat_form.spec.ts b/packages/forms/signals/test/web/compat_form.spec.ts index e7720eaaaeb..938ed703dea 100644 --- a/packages/forms/signals/test/web/compat_form.spec.ts +++ b/packages/forms/signals/test/web/compat_form.spec.ts @@ -12,14 +12,14 @@ import {FormControl} from '@angular/forms'; import {compatForm} from '../../compat'; import {FormField} from '../../public_api'; -describe('compatForm with [field] directive', () => { +describe('compatForm with [formField] directive', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [provideZonelessChangeDetection()], }); }); - it('should bind compat form to input with [field] directive', () => { + it('should bind compat form to input with [formField] directive', () => { @Component({ imports: [FormField], template: ` @@ -44,7 +44,7 @@ describe('compatForm with [field] directive', () => { expect(ageInput.value).toBe('5'); }); - it('should bind root-level FormControl to input with [field] directive', () => { + it('should bind root-level FormControl to input with [formField] directive', () => { @Component({ imports: [FormField], template: ``, diff --git a/packages/forms/signals/test/web/dynamic_binding.spec.ts b/packages/forms/signals/test/web/dynamic_binding.spec.ts index a9747610c66..7d1ce20de2e 100644 --- a/packages/forms/signals/test/web/dynamic_binding.spec.ts +++ b/packages/forms/signals/test/web/dynamic_binding.spec.ts @@ -18,17 +18,17 @@ import { import {TestBed} from '@angular/core/testing'; import { disabled, - FieldTree, form, FormCheckboxControl, FormField, FormValueControl, required, + type Field, } from '@angular/forms/signals'; describe('createComponent', () => { describe('FormValueControl', () => { - it(`synchronizes value from '[field]' binding`, () => { + it(`synchronizes value from '[formField]' binding`, () => { @Component({template: ''}) class CustomInput implements FormValueControl { readonly value = model.required(); @@ -62,7 +62,7 @@ describe('createComponent', () => { expect(control().value()).toBe('from component'); }); - it(`synchronizes properties from '[field]' binding`, () => { + it(`synchronizes properties from '[formField]' binding`, () => { @Component({template: ''}) class CustomInput implements FormValueControl { readonly value = model.required(); @@ -98,7 +98,7 @@ describe('createComponent', () => { }); describe('FormCheckboxControl', () => { - it(`synchronizes value from '[field]' binding`, () => { + it(`synchronizes value from '[formField]' binding`, () => { @Component({template: ''}) class CustomCheckbox implements FormCheckboxControl { readonly checked = model.required(); @@ -132,7 +132,7 @@ describe('createComponent', () => { expect(control().value()).toBe(true); }); - it(`synchronizes properties from '[field]' binding`, () => { + it(`synchronizes properties from '[formField]' binding`, () => { @Component({template: ''}) class CustomCheckbox implements FormCheckboxControl { readonly checked = model.required(); @@ -170,7 +170,7 @@ describe('createComponent', () => { it(`should not treat component with '[formField]' input as a control`, () => { @Component({template: ''}) class TestCmp { - readonly formField = input.required>(); + readonly formField = input.required>(); readonly value = model.required(); } @@ -193,7 +193,7 @@ describe('createComponent', () => { expect(control().formFieldBindings()).toHaveSize(0); }); - it(`should throw for invalid '[field]' binding host`, () => { + it(`should throw for invalid '[formField]' binding host`, () => { @Component({template: ''}) class InvalidFieldHost {} diff --git a/packages/forms/signals/test/web/focus.spec.ts b/packages/forms/signals/test/web/focus.spec.ts index a40d7657f2e..37e2bd2e1fe 100644 --- a/packages/forms/signals/test/web/focus.spec.ts +++ b/packages/forms/signals/test/web/focus.spec.ts @@ -19,7 +19,7 @@ import { import {TestBed} from '@angular/core/testing'; import {FormControl} from '@angular/forms'; import {compatForm} from '../../compat'; -import {FormField, form, type FieldTree} from '../../public_api'; +import {FormField, form, type Field, type FieldTree} from '../../public_api'; describe('FieldState focus behavior', () => { it('should focus a native control', async () => { @@ -174,7 +174,7 @@ describe('FieldState focus behavior', () => { template: ``, }) class CustomControl { - formField = input.required>(); + formField = input.required>(); } @Component({ @@ -198,7 +198,7 @@ describe('FieldState focus behavior', () => { template: ``, }) class CustomControl { - formField = input.required>(); + formField = input.required>(); input = viewChild.required>('input'); constructor() { diff --git a/packages/forms/signals/test/web/form_field_directive.spec.ts b/packages/forms/signals/test/web/form_field_directive.spec.ts index 6fd401d803a..b255e34b0c5 100644 --- a/packages/forms/signals/test/web/form_field_directive.spec.ts +++ b/packages/forms/signals/test/web/form_field_directive.spec.ts @@ -47,7 +47,7 @@ import { requiredError, validateAsync, type DisabledReason, - type FieldTree, + type Field, type FormCheckboxControl, type FormValueControl, type ValidationError, @@ -60,7 +60,7 @@ import { imports: [FormField], }) class TestStringControl { - readonly formField = input.required>(); + readonly formField = input.required>(); readonly fieldDirective = viewChild.required(FormField); } @@ -3226,7 +3226,7 @@ describe('field directive', () => { template: `{{ formField()().value() }}`, }) class WrapperCmp { - readonly formField = input.required>(); + readonly formField = input.required>(); } @Component({ @@ -3300,7 +3300,7 @@ describe('field directive', () => { template: ``, }) class ComplexControl { - readonly formField = input.required>(); + readonly formField = input.required>(); } @Component({ @@ -3321,7 +3321,7 @@ describe('field directive', () => { template: ``, }) class ComplexControl { - readonly formField = input.required>(); + readonly formField = input.required>(); constructor() { inject(FormField, {optional: true, self: true})?.registerAsBinding(); @@ -4252,7 +4252,7 @@ describe('field directive', () => { template: '', }) class CustomSubform { - readonly formField = input.required>(); + readonly formField = input.required>(); } @Component({