From 2fdd4da2a8683c94bb4014a8f943763139ed97fa Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 6 Oct 2025 16:05:06 -0400 Subject: [PATCH] refactor(forms): rename the control directive to the field directive (#64300) Renames the control directive and the input that users set to bind a field to a UI control. Previously users would do: ``` ``` Now users will do: ``` ``` PR Close #64300 --- goldens/public-api/forms/signals/index.api.md | 40 ++-- .../control_bindings/GOLDEN_PARTIAL.js | 38 +-- .../control_bindings/TEST_CASES.json | 10 +- .../control_bindings/control_bindings.js | 6 +- .../control_bindings/control_bindings.ts | 14 +- .../src/template/pipeline/ir/src/enums.ts | 4 +- .../src/template/pipeline/src/ingest.ts | 6 +- .../src/phases/attribute_extraction.ts | 2 +- .../src/phases/binding_specialization.ts | 2 +- .../core/src/render3/instructions/control.ts | 34 +-- .../core/src/render3/interfaces/control.ts | 4 +- .../test/acceptance/property_binding_spec.ts | 26 +-- packages/forms/signals/src/api/control.ts | 48 ++-- .../forms/signals/src/api/field_directive.ts | 23 +- packages/forms/signals/src/api/types.ts | 6 +- .../src/controls/interop_ng_control.ts | 2 +- packages/forms/signals/src/field/node.ts | 6 +- packages/forms/signals/src/field/state.ts | 6 +- .../signals/test/web/field_directive.spec.ts | 220 +++++++++--------- packages/forms/src/directives/ng_model.ts | 2 +- 20 files changed, 250 insertions(+), 249 deletions(-) diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index 81737dad5a7..efc607024ff 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -65,25 +65,6 @@ export interface ChildFieldContext extends RootFieldContext { readonly key: Signal; } -// @public -export const CONTROL: InjectionToken>; - -// @public -export class Control implements ɵControl { - // (undocumented) - readonly [ɵCONTROL]: undefined; - // (undocumented) - readonly field: i0.InputSignal>; - // (undocumented) - register(): void; - // (undocumented) - readonly state: i0.Signal>; - // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[control]", never, { "field": { "alias": "control"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>; - // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration, never>; -} - // @public export function createProperty(): Property; @@ -126,6 +107,25 @@ export class EmailValidationError extends _NgValidationError { readonly kind = "email"; } +// @public +export const FIELD: InjectionToken>; + +// @public +export class Field implements ɵControl { + // (undocumented) + readonly [ɵCONTROL]: undefined; + // (undocumented) + readonly field: i0.InputSignal>; + // (undocumented) + register(): void; + // (undocumented) + readonly state: i0.Signal>; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration, "[field]", never, { "field": { "alias": "field"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration, never>; +} + // @public export type FieldContext = TPathKind extends PathKind.Item ? ItemFieldContext : TPathKind extends PathKind.Child ? ChildFieldContext : RootFieldContext; @@ -138,13 +138,13 @@ export type FieldPath = { // @public export interface FieldState extends ɵFieldState { - readonly controls: Signal[]>; readonly dirty: Signal; // (undocumented) readonly disabledReasons: Signal; // (undocumented) readonly errors: Signal; readonly errorSummary: Signal; + readonly fieldBindings: Signal[]>; hasProperty(key: Property | AggregateProperty): boolean; readonly hidden: Signal; readonly invalid: Signal; diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/GOLDEN_PARTIAL.js index 9f0aaf64ae0..22d846539c6 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/GOLDEN_PARTIAL.js @@ -3,17 +3,17 @@ ****************************************************************************************************/ import { Component, Directive, input } from '@angular/core'; import * as i0 from "@angular/core"; -export class Control { +export class Field { constructor() { - this.control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : [])); + this.field = input(...(ngDevMode ? [undefined, { debugName: "field" }] : [])); } } -Control.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive }); -Control.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: Control, isStandalone: true, selector: "[control]", inputs: { control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); -i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Control, decorators: [{ +Field.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Field, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +Field.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: Field, isStandalone: true, selector: "[field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Field, decorators: [{ type: Directive, - args: [{ selector: '[control]' }] - }], propDecorators: { control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }] } }); + args: [{ selector: '[field]' }] + }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }] } }); export class MyComponent { constructor() { this.value = 'Hello, world!'; @@ -21,19 +21,19 @@ export class MyComponent { } MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: ` -
-
Not a form control either.
- - `, isInline: true, dependencies: [{ kind: "directive", type: Control, selector: "[control]", inputs: ["control"] }] }); +
+
Not a form control either.
+ + `, isInline: true, dependencies: [{ kind: "directive", type: Field, selector: "[field]", inputs: ["field"] }] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ type: Component, args: [{ template: ` -
-
Not a form control either.
- +
+
Not a form control either.
+ `, - imports: [Control], + imports: [Field], }] }] }); @@ -41,10 +41,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE * PARTIAL FILE: control_bindings.d.ts ****************************************************************************************************/ import * as i0 from "@angular/core"; -export declare class Control { - readonly control: import("@angular/core").InputSignal; - static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵdir: i0.ɵɵDirectiveDeclaration; +export declare class Field { + readonly field: import("@angular/core").InputSignal; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; } export declare class MyComponent { value: string; diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/TEST_CASES.json index 6e0ada3a2e3..00ecec89cfc 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/TEST_CASES.json @@ -2,15 +2,11 @@ "$schema": "../../test_case_schema.json", "cases": [ { - "description": "should generate control instruction for `control` property bindings", - "inputFiles": [ - "control_bindings.ts" - ], + "description": "should generate control instruction for `field` property bindings", + "inputFiles": ["control_bindings.ts"], "expectations": [ { - "files": [ - "control_bindings.js" - ] + "files": ["control_bindings.js"] } ] } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.js index 2ea074d9c99..63bc8535118 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.js @@ -3,7 +3,7 @@ MyComponent.ɵcmp = /* @__PURE__ */i0.ɵɵdefineComponent({ selectors: [["ng-component"]], decls: 4, vars: 2, - consts: [["control", "Not a form control"], [3, "control"]], + consts: [["field", "Not a form control"], [3, "field"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelement(0, "div", 0); @@ -16,11 +16,11 @@ MyComponent.ɵcmp = /* @__PURE__ */i0.ɵɵdefineComponent({ } if (rf & 2) { i0.ɵɵadvance(); - i0.ɵɵattribute("control", ctx.value); + i0.ɵɵattribute("field", ctx.value); i0.ɵɵadvance(2); i0.ɵɵcontrol(ctx.value); } }, - dependencies: [Control], + dependencies: [Field], encapsulation: 2 }); \ No newline at end of file diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.ts index 366a16bd5e3..ed0d037572a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.ts @@ -1,17 +1,17 @@ import {Component, Directive, input} from '@angular/core'; -@Directive({selector: '[control]'}) -export class Control { - readonly control = input(); +@Directive({selector: '[field]'}) +export class Field { + readonly field = input(); } @Component({ template: ` -
-
Not a form control either.
- +
+
Not a form control either.
+ `, - imports: [Control], + imports: [Field], }) export class MyComponent { value = 'Hello, world!'; diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts index 7fa8aa1ae8b..939c9b8936e 100644 --- a/packages/compiler/src/template/pipeline/ir/src/enums.ts +++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts @@ -301,7 +301,7 @@ export enum OpKind { AnimationListener, /** - * An operation to bind an expression to a `control` property of an element. + * An operation to bind an expression to a `field` property of an element. */ Control, @@ -309,7 +309,7 @@ export enum OpKind { * An operation to set up a corresponding {@link Control} operation. * * This is responsible for setting up event listeners on a native or custom form control when - * bound to a specialized control directive. + * bound to a specialized field directive. */ ControlCreate, } diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 8d005d68cb8..9cb2aac9bcc 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -21,10 +21,10 @@ import {BindingParser} from '../../../template_parser/binding_parser'; import * as ir from '../ir'; import { - TemplateCompilationMode, CompilationUnit, ComponentCompilationJob, HostBindingCompilationJob, + TemplateCompilationMode, type CompilationJob, type ViewCompilationUnit, } from './compilation'; @@ -1348,9 +1348,9 @@ function ingestElementBindings( ), ); - // If the input name is 'control', this could be a form control binding which requires a + // If the input name is 'field', this could be a form control binding which requires a // `ControlCreateOp` to properly initialize. - if (input.type === e.BindingType.Property && input.name === 'control') { + if (input.type === e.BindingType.Property && input.name === 'field') { unit.create.push(ir.createControlCreateOp(input.sourceSpan)); } } diff --git a/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts b/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts index c42efabc799..6562817e7c5 100644 --- a/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts +++ b/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts @@ -62,7 +62,7 @@ export function extractAttributes(job: CompilationJob): void { op.target, ir.BindingKind.Property, null, - 'control', + 'field', /* expression */ null, /* i18nContext */ null, /* i18nMessage */ null, diff --git a/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts b/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts index 76009d20b08..1d40952339e 100644 --- a/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts +++ b/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts @@ -127,7 +127,7 @@ export function specializeBindings(job: CompilationJob): void { op.sourceSpan, ), ); - } else if (op.name === 'control') { + } else if (op.name === 'field') { ir.OpList.replace(op, ir.createControlOp(op)); } else { ir.OpList.replace( diff --git a/packages/core/src/render3/instructions/control.ts b/packages/core/src/render3/instructions/control.ts index bd5cfb2cbc8..f6b5f4d4543 100644 --- a/packages/core/src/render3/instructions/control.ts +++ b/packages/core/src/render3/instructions/control.ts @@ -15,7 +15,7 @@ import {Renderer} from '../interfaces/renderer'; import {SanitizerFn} from '../interfaces/sanitization'; import {isComponentHost} from '../interfaces/type_checks'; import {LView, RENDERER} from '../interfaces/view'; -import {getLView, getCurrentTNode, getSelectedTNode, getTView, nextBindingIndex} from '../state'; +import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state'; import {getNativeByTNode} from '../util/view_utils'; import {listenToOutput} from '../view/directive_outputs'; import {listenToDomEvent, wrapListener} from '../view/listeners'; @@ -25,8 +25,8 @@ import {writeToDirectiveInput} from './write_to_directive_input'; /** * Possibly sets up a {@link ɵControl} to manage a native or custom form control. * - * Setup occurs if a `control` input is bound to a {@link ɵControl} directive on the current node, - * but not to a component. If a `control` input is bound to a component, we assume the component + * Setup occurs if a `field` input is bound to a {@link ɵControl} directive on the current node, + * but not to a component. If a `field` input is bound to a component, we assume the component * will manage the control in its own template. * * @codeGenApi @@ -36,7 +36,7 @@ export function ɵɵcontrolCreate(): void { const tNode = getCurrentTNode()!; // TODO(https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131712274) - // * cache the control directive index or instance for reuse. + // * cache the field directive index or instance for reuse. const control = getControlDirective(tNode, lView); if (!control) { return; @@ -67,7 +67,7 @@ export function ɵɵcontrolCreate(): void { } /** - * Updates a `control` property, and possibly other form control properties, on the current element. + * Updates a `field` property, and possibly other form control properties, on the current element. * * This is a specialized version of the `ɵɵproperty` instruction that handles updating additional * form control properties, if set up to do so by {@link ɵɵcontrolCreate} during creation. @@ -84,8 +84,8 @@ export function ɵɵcontrol(value: T, sanitizer?: SanitizerFn | null): void { if (bindingUpdated(lView, bindingIndex, value)) { const tView = getTView(); - setPropertyAndInputs(tNode, lView, 'control', value, lView[RENDERER], sanitizer); - ngDevMode && storePropertyBindingMetadata(tView.data, tNode, 'control', bindingIndex); + setPropertyAndInputs(tNode, lView, 'field', value, lView[RENDERER], sanitizer); + ngDevMode && storePropertyBindingMetadata(tView.data, tNode, 'field', bindingIndex); } // TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131711472 @@ -105,8 +105,8 @@ export function ɵɵcontrol(value: T, sanitizer?: SanitizerFn | null): void { } /** - * Returns the {@link ɵControl} directive on the specified node, if one is present and a `control` - * input is bound to it, but not to a component. If a `control` input is bound to a component, we + * Returns the {@link ɵControl} directive on the specified node, if one is present and a `field` + * input is bound to it, but not to a component. If a `field` input is bound to a component, we * assume the component will manage the control in its own template and return nothing to indicate * that the directive should not be set up. * @@ -114,7 +114,7 @@ export function ɵɵcontrol(value: T, sanitizer?: SanitizerFn | null): void { * @param lView The `LView` that contains the element. */ function getControlDirective(tNode: TNode, lView: LView): ɵControl | undefined { - const directiveIndices = tNode.inputs?.['control']; + const directiveIndices = tNode.inputs?.['field']; if (!directiveIndices) { // There are no matching inputs for the `[control]` property binding. return; @@ -123,14 +123,14 @@ function getControlDirective(tNode: TNode, lView: LView): ɵControl | unde if (isComponentHost(tNode)) { const componentIndex = tNode.directiveStart + tNode.componentOffset; if (directiveIndices.includes(componentIndex)) { - // If component has a `control` input, we assume that it will handle binding the field to the + // If component has a `field` input, we assume that it will handle binding the field to the // appropriate native/custom control in its template, so we do not attempt to bind any inputs // on this component. return; } } - // Search for the `Control` directive. + // Search for the `Field` directive. for (let index of directiveIndices) { const directive = lView[index]; if (ɵCONTROL in directive) { @@ -138,7 +138,7 @@ function getControlDirective(tNode: TNode, lView: LView): ɵControl | unde } } - // The `Control` directive was not imported by this component. + // The `Field` directive was not imported by this component. return; } @@ -193,7 +193,7 @@ function hasOutput(componentDef: ComponentDef, name: string): boolean { } /** - * Adds event listeners to a custom form control component to notify the `control` of changes. + * Adds event listeners to a custom form control component to notify the `field` of changes. * * @param lView The `LView` that contains the custom form control. * @param tNode The `TNode` of the custom form control. @@ -264,7 +264,7 @@ function isNativeControl(lView: LView, tNode: TNode): boolean { } /** - * Adds event listeners to a native form control element to notify the `control` of changes. + * Adds event listeners to a native form control element to notify the `field` of changes. * * @param lView The `LView` that contains the native form control. * @param tNode The `TNode` of the native form control. @@ -306,7 +306,7 @@ function listenToNativeControl(lView: LView<{} | null>, tNode: TNode, control: } /** - * Updates the inputs of a custom form control component with the latest state from the `control`. + * Updates the inputs of a custom form control component with the latest state from the `field`. * * @param lView The `LView` that contains the custom form control. * @param componentIndex The index of the custom form control component in the `LView`. @@ -361,7 +361,7 @@ function maybeWriteToDirectiveInput( } /** - * Updates the properties of a native form control element with the latest state from the `control`. + * Updates the properties of a native form control element with the latest state from the `field`. * * @param tNode The `TNode` of the native form control. * @param lView The `LView` that contains the native form control. diff --git a/packages/core/src/render3/interfaces/control.ts b/packages/core/src/render3/interfaces/control.ts index d1c6b51af0d..5beae476c7d 100644 --- a/packages/core/src/render3/interfaces/control.ts +++ b/packages/core/src/render3/interfaces/control.ts @@ -24,8 +24,8 @@ export interface ɵControl { * Registers this directive as a control of its associated form field. * * The presence of this directive alone is not sufficient to determine whether it'll control - * the bound field. If this directive's host is a component with a `control` input, we assume - * the component will forward the bound field to another control directive in its own template, + * the bound field. If this directive's host is a component with a `field` input, we assume + * the component will forward the bound field to another field directive in its own template, * and do nothing. */ register(): void; diff --git a/packages/core/test/acceptance/property_binding_spec.ts b/packages/core/test/acceptance/property_binding_spec.ts index dc7189c7045..e0190b82963 100644 --- a/packages/core/test/acceptance/property_binding_spec.ts +++ b/packages/core/test/acceptance/property_binding_spec.ts @@ -7,6 +7,8 @@ */ import {state, style, trigger} from '@angular/animations'; import {CommonModule} from '@angular/common'; +import {By, DomSanitizer, SafeUrl} from '@angular/platform-browser'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import { Component, Directive, @@ -17,8 +19,6 @@ import { ViewContainerRef, } from '../../src/core'; import {TestBed} from '../../testing'; -import {By, DomSanitizer, SafeUrl} from '@angular/platform-browser'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; describe('property bindings', () => { beforeEach(() => { @@ -611,34 +611,34 @@ describe('property bindings', () => { expect(otherDir.id).toBe(3); }); - it('should support input bindings named "control"', () => { - // Angular has specialized support for binding to form controls (e.g. `[control]="field"`). - // This test ensures that `[control]` property bindings can still target other inputs bearing + it('should support input bindings named "field"', () => { + // Angular has specialized support for binding to form controls (e.g. `[field]="field"`). + // This test ensures that `[field]` property bindings can still target other inputs bearing // the same name. - @Directive({selector: '[control]'}) - class Control { - @Input() control = 'Default control value'; + @Directive({selector: '[field]'}) + class Field { + @Input() field = 'Default control value'; } @Component({ template: ` -
+
`, - imports: [Control], + imports: [Field], }) class App { value?: string; } const fixture = TestBed.createComponent(App); - const control = fixture.debugElement.query(By.directive(Control)).injector.get(Control); - expect(control.control).toBe('Default control value'); + const control = fixture.debugElement.query(By.directive(Field)).injector.get(Field); + expect(control.field).toBe('Default control value'); fixture.componentInstance.value = 'Bound control value'; fixture.detectChanges(); - expect(control.control).toBe('Bound control value'); + expect(control.field).toBe('Bound control value'); }); }); diff --git a/packages/forms/signals/src/api/control.ts b/packages/forms/signals/src/api/control.ts index 15e71ebf3f3..b21c24ced6d 100644 --- a/packages/forms/signals/src/api/control.ts +++ b/packages/forms/signals/src/api/control.ts @@ -22,82 +22,82 @@ export interface FormUiControl { // inputs individually. Givn that, should they still be part of this interface? /** - * An input to receive the errors for the field. If implemented, the `Control` directive will + * An input to receive the errors for the field. If implemented, the `Field` directive will * automatically bind errors from the bound field to this input. */ readonly errors?: InputSignal[]>; /** - * An input to receive the disabled status for the field. If implemented, the `Control` directive + * An input to receive the disabled status for the field. If implemented, the `Field` directive * will automatically bind the disabled status from the bound field to this input. */ readonly disabled?: InputSignal; /** - * An input to receive the reasons for the disablement of the field. If implemented, the `Control` + * An input to receive the reasons for the disablement of the field. If implemented, the `Field` * directive will automatically bind the disabled reason from the bound field to this input. */ readonly disabledReasons?: InputSignal[]>; /** - * An input to receive the readonly status for the field. If implemented, the `Control` directive + * An input to receive the readonly status for the field. If implemented, the `Field` directive * will automatically bind the readonly status from the bound field to this input. */ readonly readonly?: InputSignal; /** - * An input to receive the hidden status for the field. If implemented, the `Control` directive + * An input to receive the hidden status for the field. If implemented, the `Field` directive * will automatically bind the hidden status from the bound field to this input. */ readonly hidden?: InputSignal; /** - * An input to receive the invalid status for the field. If implemented, the `Control` directive + * An input to receive the invalid status for the field. If implemented, the `Field` directive * will automatically bind the invalid status from the bound field to this input. */ readonly invalid?: InputSignal; /** - * An input to receive the pending status for the field. If implemented, the `Control` directive + * An input to receive the pending status for the field. If implemented, the `Field` directive * will automatically bind the pending status from the bound field to this input. */ readonly pending?: InputSignal; /** - * An input to receive the touched status for the field. If implemented, the `Control` directive + * An input to receive the touched status for the field. If implemented, the `Field` directive * will automatically bind the touched status from the bound field to this input. */ readonly touched?: ModelSignal | InputSignal | OutputRef; /** - * An input to receive the dirty status for the field. If implemented, the `Control` directive + * An input to receive the dirty status for the field. If implemented, the `Field` directive * will automatically bind the dirty status from the bound field to this input. */ readonly dirty?: InputSignal; /** - * An input to receive the name for the field. If implemented, the `Control` directive will + * An input to receive the name for the field. If implemented, the `Field` directive will * automatically bind the name from the bound field to this input. */ readonly name?: InputSignal; /** - * An input to receive the required status for the field. If implemented, the `Control` directive + * An input to receive the required status for the field. If implemented, the `Field` directive * will automatically bind the required status from the bound field to this input. */ readonly required?: InputSignal; /** - * An input to receive the min value for the field. If implemented, the `Control` directive will + * An input to receive the min value for the field. If implemented, the `Field` directive will * automatically bind the min value from the bound field to this input. */ readonly min?: InputSignal; /** - * An input to receive the min length for the field. If implemented, the `Control` directive will + * An input to receive the min length for the field. If implemented, the `Field` directive will * automatically bind the min length from the bound field to this input. */ readonly minLength?: InputSignal; /** - * An input to receive the max value for the field. If implemented, the `Control` directive will + * An input to receive the max value for the field. If implemented, the `Field` directive will * automatically bind the max value from the bound field to this input. */ readonly max?: InputSignal; /** - * An input to receive the max length for the field. If implemented, the `Control` directive will + * An input to receive the max length for the field. If implemented, the `Field` directive will * automatically bind the max length from the bound field to this input. */ readonly maxLength?: InputSignal; /** - * An input to receive the value patterns for the field. If implemented, the `Control` directive + * An input to receive the value patterns for the field. If implemented, the `Field` directive * will automatically bind the value patterns from the bound field to this input. */ readonly pattern?: InputSignal; @@ -105,11 +105,11 @@ export interface FormUiControl { /** * A contract for a form control that edits a `FieldTree` of type `TValue`. Any component that - * implements this contract can be used with the `Control` directive. + * implements this contract can be used with the `Field` directive. * * Many of the properties declared on this contract are optional. They do not need to be * implemented, but if they are will be kept in sync with the field state of the field bound to the - * `Control` directive. + * `Field` directive. * * @template TValue The type of `FieldTree` that the implementing component can edit. * @@ -119,7 +119,7 @@ export interface FormUiControl { export interface FormValueControl extends FormUiControl { /** * The value is the only required property in this contract. A component that wants to integrate - * with the `Control` directive via this contract, *must* provide a `model()` that will be kept in + * with the `Field` directive via this contract, *must* provide a `model()` that will be kept in * sync with the value of the bound `FieldTree`. */ readonly value: ModelSignal; @@ -129,18 +129,18 @@ export interface FormValueControl extends FormUiControl { // remove this restriction. /** * The implementing component *must not* define a `checked` property. This is reserved for - * components that want to integrate with the `Control` directive as a checkbox. + * components that want to integrate with the `Field` directive as a checkbox. */ readonly checked?: undefined; } /** * A contract for a form control that edits a boolean checkbox `FieldTree`. Any component that - * implements this contract can be used with the `Control` directive. + * implements this contract can be used with the `Field` directive. * * Many of the properties declared on this contract are optional. They do not need to be * implemented, but if they are will be kept in sync with the field state of the field bound to the - * `Control` directive. + * `Field` directive. * * @category control * @experimental 21.0.0 @@ -148,7 +148,7 @@ export interface FormValueControl extends FormUiControl { export interface FormCheckboxControl extends FormUiControl { /** * The checked is the only required property in this contract. A component that wants to integrate - * with the `Control` directive, *must* provide a `model()` that will be kept in sync with the + * with the `Field` directive, *must* provide a `model()` that will be kept in sync with the * value of the bound `FieldTree`. */ readonly checked: ModelSignal; @@ -157,7 +157,7 @@ export interface FormCheckboxControl extends FormUiControl { // generic for it? /** * The implementing component *must not* define a `value` property. This is reserved for - * components that want to integrate with the `Control` directive as a standard input. + * components that want to integrate with the `Field` directive as a standard input. */ readonly value?: undefined; } diff --git a/packages/forms/signals/src/api/field_directive.ts b/packages/forms/signals/src/api/field_directive.ts index 4ff099375cb..b34eb69ded5 100644 --- a/packages/forms/signals/src/api/field_directive.ts +++ b/packages/forms/signals/src/api/field_directive.ts @@ -21,10 +21,10 @@ import type {FieldNode} from '../field/node'; import type {FieldTree} from './types'; /** - * Lightweight DI token provided by the {@link Control} directive. + * Lightweight DI token provided by the {@link Field} directive. */ -export const CONTROL = new InjectionToken>( - typeof ngDevMode !== undefined && ngDevMode ? 'CONTROL' : '', +export const FIELD = new InjectionToken>( + typeof ngDevMode !== undefined && ngDevMode ? 'FIELD' : '', ); /** @@ -48,10 +48,10 @@ export const CONTROL = new InjectionToken>( * @category control * @experimental 21.0.0 */ -@Directive({selector: '[control]', providers: [{provide: CONTROL, useExisting: Control}]}) -export class Control implements ɵControl { +@Directive({selector: '[field]', providers: [{provide: FIELD, useExisting: Field}]}) +export class Field implements ɵControl { private readonly injector = inject(Injector); - readonly field = input.required>({alias: 'control'}); + readonly field = input.required>(); readonly state = computed(() => this.field()()); readonly [ɵCONTROL] = undefined; @@ -59,13 +59,18 @@ export class Control implements ɵControl { 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 - // (as opposed to just passing the field through to its `control` input). + // (as opposed to just passing the field through to its `field` input). effect( (onCleanup) => { const fieldNode = this.state() as FieldNode; - fieldNode.nodeState.controls.update((controls) => [...controls, this as Control]); + fieldNode.nodeState.fieldBindings.update((controls) => [ + ...controls, + this as Field, + ]); onCleanup(() => { - fieldNode.nodeState.controls.update((controls) => controls.filter((c) => c !== this)); + fieldNode.nodeState.fieldBindings.update((controls) => + controls.filter((c) => c !== this), + ); }); }, {injector: this.injector}, diff --git a/packages/forms/signals/src/api/types.ts b/packages/forms/signals/src/api/types.ts index 4e30eb6edd0..98342986a29 100644 --- a/packages/forms/signals/src/api/types.ts +++ b/packages/forms/signals/src/api/types.ts @@ -7,7 +7,7 @@ */ import {Signal, ɵFieldState} from '@angular/core'; -import type {Control} from './field_directive'; +import type {Field} from './field_directive'; import {AggregateProperty, Property} from './property'; import type {ValidationError, WithOptionalField, WithoutField} from './validation_errors'; @@ -304,9 +304,9 @@ export interface FieldState; /** - * A signal containing the `Control` directives this field is currently bound to. + * The {@link Field} directives that bind this field to a UI control. */ - readonly controls: Signal[]>; + readonly fieldBindings: Signal[]>; /** * Reads an aggregate property value from the field. diff --git a/packages/forms/signals/src/controls/interop_ng_control.ts b/packages/forms/signals/src/controls/interop_ng_control.ts index 9a4a8973b7a..5153882fdb4 100644 --- a/packages/forms/signals/src/controls/interop_ng_control.ts +++ b/packages/forms/signals/src/controls/interop_ng_control.ts @@ -43,7 +43,7 @@ export type InteropSharedKeys = | 'status'; /** - * A fake version of `NgControl` provided by the `Control` directive. This allows interoperability + * A fake version of `NgControl` provided by the `Field` directive. This allows interoperability * with a wider range of components designed to work with reactive forms, in particular ones that * inject the `NgControl`. The interop control does not implement *all* properties and methods of * the real `NgControl`, but does implement some of the most commonly used ones that have a clear diff --git a/packages/forms/signals/src/field/node.ts b/packages/forms/signals/src/field/node.ts index b26c2b17107..9916a056e20 100644 --- a/packages/forms/signals/src/field/node.ts +++ b/packages/forms/signals/src/field/node.ts @@ -7,7 +7,7 @@ */ import type {Signal, WritableSignal} from '@angular/core'; -import type {Control} from '../api/field_directive'; +import type {Field} from '../api/field_directive'; import { AggregateProperty, MAX, @@ -134,8 +134,8 @@ export class FieldNode implements FieldState { return this.nodeState.readonly; } - get controls(): Signal[]> { - return this.nodeState.controls; + get fieldBindings(): Signal[]> { + return this.nodeState.fieldBindings; } get submitting(): Signal { diff --git a/packages/forms/signals/src/field/state.ts b/packages/forms/signals/src/field/state.ts index 71e29a511ca..dd3b73a43ce 100644 --- a/packages/forms/signals/src/field/state.ts +++ b/packages/forms/signals/src/field/state.ts @@ -7,7 +7,7 @@ */ import {computed, signal, Signal} from '@angular/core'; -import type {Control} from '../api/field_directive'; +import type {Field} from '../api/field_directive'; import type {DisabledReason} from '../api/types'; import type {FieldNode} from './node'; import {reduceChildren, shortCircuitTrue} from './util'; @@ -61,8 +61,8 @@ export class FieldNodeState { this.selfTouched.set(false); } - /** The UI controls the field is currently bound to. */ - readonly controls = signal[]>([]); + /** The {@link Field} directives that bind this field to a UI control. */ + readonly fieldBindings = signal[]>([]); constructor(private readonly node: FieldNode) {} diff --git a/packages/forms/signals/test/web/field_directive.spec.ts b/packages/forms/signals/test/web/field_directive.spec.ts index 942d2b3fc73..a9715ee958a 100644 --- a/packages/forms/signals/test/web/field_directive.spec.ts +++ b/packages/forms/signals/test/web/field_directive.spec.ts @@ -20,7 +20,7 @@ import { } from '@angular/core'; import {TestBed} from '@angular/core/testing'; import { - Control, + Field, disabled, form, hidden, @@ -41,15 +41,15 @@ import { @Component({ selector: 'string-control', - template: ``, - imports: [Control], + template: ``, + imports: [Field], }) class TestStringControl { - readonly control = input.required>(); - readonly controlDirective = viewChild.required(Control); + readonly field = input.required>(); + readonly fieldDirective = viewChild.required(Field); } -describe('control directive', () => { +describe('field directive', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [provideZonelessChangeDetection()], @@ -60,8 +60,8 @@ describe('control directive', () => { describe('disabled', () => { it('native control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly disabled = signal(false); @@ -86,8 +86,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], - template: ``, + imports: [Field, CustomControl], + template: ``, }) class TestCmp { readonly disabled = signal(false); @@ -109,10 +109,10 @@ describe('control directive', () => { describe('name', () => { it('native control', () => { @Component({ - imports: [Control], + imports: [Field], template: ` @for (item of f; track item) { - + {{item().value()}} } `, @@ -160,10 +160,10 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], + imports: [Field, CustomControl], template: ` @for (item of f; track item) { - + } `, }) @@ -206,8 +206,8 @@ describe('control directive', () => { describe('readonly', () => { it('native control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly readonly = signal(true); @@ -232,8 +232,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], - template: ``, + imports: [Field, CustomControl], + template: ``, }) class TestCmp { readonly readonly = signal(false); @@ -255,8 +255,8 @@ describe('control directive', () => { describe('required', () => { it('native control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly required = signal(false); @@ -281,8 +281,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], - template: ``, + imports: [Field, CustomControl], + template: ``, }) class TestCmp { readonly required = signal(false); @@ -304,8 +304,8 @@ describe('control directive', () => { describe('max', () => { it('native control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly max = signal(10); @@ -330,8 +330,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], - template: ``, + imports: [Field, CustomControl], + template: ``, }) class TestCmp { readonly max = signal(10); @@ -351,8 +351,8 @@ describe('control directive', () => { it('is not set on native control if type does not support it', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly f = form(signal(5), (p) => { @@ -369,8 +369,8 @@ describe('control directive', () => { describe('min', () => { it('native control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly min = signal(10); @@ -395,8 +395,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], - template: ``, + imports: [Field, CustomControl], + template: ``, }) class TestCmp { readonly min = signal(10); @@ -416,8 +416,8 @@ describe('control directive', () => { it('is not set on native control if type does not support it', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly f = form(signal(15), (p) => { @@ -434,8 +434,8 @@ describe('control directive', () => { describe('maxLength', () => { it('native control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly maxLength = signal(20); @@ -460,8 +460,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], - template: ``, + imports: [Field, CustomControl], + template: ``, }) class TestCmp { readonly maxLength = signal(10); @@ -481,8 +481,8 @@ describe('control directive', () => { it('is not set on a native control that does not support it', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly f = form(signal(''), (p) => { @@ -499,8 +499,8 @@ describe('control directive', () => { describe('minLength', () => { it('native control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly minLength = signal(20); @@ -525,8 +525,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomControl], - template: ``, + imports: [Field, CustomControl], + template: ``, }) class TestCmp { readonly minLength = signal(10); @@ -546,8 +546,8 @@ describe('control directive', () => { it('is not set on a native control that does not support it', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { readonly f = form(signal(''), (p) => { @@ -564,9 +564,9 @@ describe('control directive', () => { it('synchronizes a basic form with a custom control', () => { @Component({ - imports: [Control], + imports: [Field], template: ` - + `, }) class TestCmp { @@ -594,8 +594,8 @@ describe('control directive', () => { it('synchronizes with a checkbox control', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal(false)); @@ -646,8 +646,8 @@ describe('control directive', () => { it('synchronizes with a textarea', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal('')); @@ -674,9 +674,9 @@ describe('control directive', () => { it('synchronizes with a select', () => { @Component({ - imports: [Control], + imports: [Field], template: ` - @@ -708,10 +708,10 @@ describe('control directive', () => { it('should assign correct value when unhiding select', () => { @Component({ - imports: [Control], + imports: [Field], template: ` @if (!f().hidden()) { - @for(opt of options; track opt) { } @@ -745,8 +745,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomInput], - template: ``, + imports: [Field, CustomInput], + template: ``, }) class TestCmp { f = form(signal('test')); @@ -786,8 +786,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomInput], - template: ``, + imports: [Field, CustomInput], + template: ``, }) class TestCmp { f = form(signal('test')); @@ -808,8 +808,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomInput], - template: ``, + imports: [Field, CustomInput], + template: ``, }) class TestCmp { f = form(signal(false)); @@ -834,18 +834,18 @@ describe('control directive', () => { expect(cmp.f().value()).toBe(false); }); - it('does not interfere with a component which accepts a control input directly', () => { + it('does not interfere with a component which accepts a field input directly', () => { @Component({ selector: 'my-wrapper', - template: `{{ control()().value() }}`, + template: `{{ field()().value() }}`, }) class WrapperCmp { - readonly control = input.required>(); + readonly field = input.required>(); } @Component({ - template: ``, - imports: [WrapperCmp, Control], + template: ``, + imports: [WrapperCmp, Field], }) class TestCmp { f = form(signal('test')); @@ -857,35 +857,35 @@ describe('control directive', () => { it('should update bound controls on the field when it is bound and unbound', async () => { const f = form(signal(''), {injector: TestBed.inject(Injector)}); - expect(f().controls()).toEqual([]); + expect(f().fieldBindings()).toEqual([]); const fixture = act(() => TestBed.createComponent(TestStringControl, { - bindings: [inputBinding('control', () => f)], + bindings: [inputBinding('field', () => f)], }), ); - expect(f().controls()).toEqual([fixture.componentInstance.controlDirective()]); + expect(f().fieldBindings()).toEqual([fixture.componentInstance.fieldDirective()]); act(() => fixture.destroy()); - expect(f().controls()).toEqual([]); + expect(f().fieldBindings()).toEqual([]); }); it('should track multiple bound controls per field', async () => { const f = form(signal(''), {injector: TestBed.inject(Injector)}); const fixture1 = act(() => TestBed.createComponent(TestStringControl, { - bindings: [inputBinding('control', () => f)], + bindings: [inputBinding('field', () => f)], }), ); const fixture2 = act(() => TestBed.createComponent(TestStringControl, { - bindings: [inputBinding('control', () => f)], + bindings: [inputBinding('field', () => f)], }), ); - expect(f().controls()).toEqual([ - fixture1.componentInstance.controlDirective(), - fixture2.componentInstance.controlDirective(), + expect(f().fieldBindings()).toEqual([ + fixture1.componentInstance.fieldDirective(), + fixture2.componentInstance.fieldDirective(), ]); }); @@ -895,24 +895,24 @@ describe('control directive', () => { const control = signal(f1); const fixture = act(() => TestBed.createComponent(TestStringControl, { - bindings: [inputBinding('control', control)], + bindings: [inputBinding('field', control)], }), ); - expect(f1().controls()).toEqual([fixture.componentInstance.controlDirective()]); - expect(f2().controls()).toEqual([]); + expect(f1().fieldBindings()).toEqual([fixture.componentInstance.fieldDirective()]); + expect(f2().fieldBindings()).toEqual([]); act(() => control.set(f2)); - expect(f1().controls()).toEqual([]); - expect(f2().controls()).toEqual([fixture.componentInstance.controlDirective()]); + expect(f1().fieldBindings()).toEqual([]); + expect(f2().fieldBindings()).toEqual([fixture.componentInstance.fieldDirective()]); }); it('should synchronize custom properties', () => { @Component({ template: ` - - + + `, - imports: [Control], + imports: [Field], }) class CustomPropsTestCmp { textInput = viewChild.required>('text'); @@ -943,9 +943,9 @@ describe('control directive', () => { it('should synchronize readonly', () => { @Component({ template: ` - + `, - imports: [Control], + imports: [Field], }) class ReadonlyTestCmp { textInput = viewChild.required>('text'); @@ -972,9 +972,9 @@ describe('control directive', () => { @Component({ template: ` - + `, - imports: [CustomInput, Control], + imports: [CustomInput, Field], }) class ReadonlyTestCmp { myInput = viewChild.required(CustomInput); @@ -1006,8 +1006,8 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomInput], - template: ``, + imports: [Field, CustomInput], + template: ``, }) class TestCmp { f = form(signal('test')); @@ -1052,10 +1052,10 @@ describe('control directive', () => { } @Component({ - imports: [Control, CustomInput], + imports: [Field, CustomInput], template: ` - + `, }) class TestCmp { @@ -1082,8 +1082,8 @@ describe('control directive', () => { describe('should work with different input types', () => { it('should sync string field with number type input', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal('123')); @@ -1110,8 +1110,8 @@ describe('control directive', () => { it('should sync number field with number type input', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal(123)); @@ -1138,8 +1138,8 @@ describe('control directive', () => { it('should sync string field with date type input', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal('2024-01-01')); @@ -1166,8 +1166,8 @@ describe('control directive', () => { it('should sync date field with date type input', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal(new Date('2024-01-01T12:00:00Z'))); @@ -1194,8 +1194,8 @@ describe('control directive', () => { it('should sync number field with date type input', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal(new Date('2024-01-01T12:00:00Z').valueOf())); @@ -1222,8 +1222,8 @@ describe('control directive', () => { it('should sync number field with datetime-local type input', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal(new Date('2024-01-01T12:30:00Z').valueOf())); @@ -1252,8 +1252,8 @@ describe('control directive', () => { it('should sync string field with color type input', () => { @Component({ - imports: [Control], - template: ``, + imports: [Field], + template: ``, }) class TestCmp { f = form(signal('#ff0000')); @@ -1282,12 +1282,12 @@ describe('control directive', () => { function setupRadioGroup() { @Component({ - imports: [Control], + imports: [Field], template: `
- - - + + +
`, }) diff --git a/packages/forms/src/directives/ng_model.ts b/packages/forms/src/directives/ng_model.ts index 635ec82cd68..344865fed05 100644 --- a/packages/forms/src/directives/ng_model.ts +++ b/packages/forms/src/directives/ng_model.ts @@ -94,7 +94,7 @@ const resolvedPromise = (() => Promise.resolve())(); * * To inspect the properties of the associated `FormControl` (like the validity state), * export the directive into a local template variable using `ngModel` as the key (ex: - * `#myVar="ngModel"`). You can then access the control using the directive's `control` property. + * `#myVar="ngModel"`). You can then access the control using the directive's `field` property. * However, the most commonly used properties (like `valid` and `dirty`) also exist on the control * for direct access. See a full list of properties directly available in * `AbstractControlDirective`.