mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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: ``` <input [control]="someField"> ``` Now users will do: ``` <input [filed]="someField"> ``` PR Close #64300
This commit is contained in:
committed by
Andrew Kushnir
parent
0acc91ef51
commit
2fdd4da2a8
+19
-19
@@ -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: `
|
||||
<div control="Not a form control"></div>
|
||||
<div [attr.control]="value">Not a form control either.</div>
|
||||
<input [control]="value">
|
||||
`, isInline: true, dependencies: [{ kind: "directive", type: Control, selector: "[control]", inputs: ["control"] }] });
|
||||
<div field="Not a form control"></div>
|
||||
<div [attr.field]="value">Not a form control either.</div>
|
||||
<input [field]="value">
|
||||
`, 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: `
|
||||
<div control="Not a form control"></div>
|
||||
<div [attr.control]="value">Not a form control either.</div>
|
||||
<input [control]="value">
|
||||
<div field="Not a form control"></div>
|
||||
<div [attr.field]="value">Not a form control either.</div>
|
||||
<input [field]="value">
|
||||
`,
|
||||
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<string | undefined>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<Control, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<Control, "[control]", never, { "control": { "alias": "control"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
||||
export declare class Field {
|
||||
readonly field: import("@angular/core").InputSignal<string | undefined>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<Field, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<Field, "[field]", never, { "field": { "alias": "field"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class MyComponent {
|
||||
value: string;
|
||||
|
||||
+3
-7
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
});
|
||||
+7
-7
@@ -1,17 +1,17 @@
|
||||
import {Component, Directive, input} from '@angular/core';
|
||||
|
||||
@Directive({selector: '[control]'})
|
||||
export class Control {
|
||||
readonly control = input<string>();
|
||||
@Directive({selector: '[field]'})
|
||||
export class Field {
|
||||
readonly field = input<string>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div control="Not a form control"></div>
|
||||
<div [attr.control]="value">Not a form control either.</div>
|
||||
<input [control]="value">
|
||||
<div field="Not a form control"></div>
|
||||
<div [attr.field]="value">Not a form control either.</div>
|
||||
<input [field]="value">
|
||||
`,
|
||||
imports: [Control],
|
||||
imports: [Field],
|
||||
})
|
||||
export class MyComponent {
|
||||
value = 'Hello, world!';
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function extractAttributes(job: CompilationJob): void {
|
||||
op.target,
|
||||
ir.BindingKind.Property,
|
||||
null,
|
||||
'control',
|
||||
'field',
|
||||
/* expression */ null,
|
||||
/* i18nContext */ null,
|
||||
/* i18nMessage */ null,
|
||||
|
||||
@@ -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<ir.UpdateOp>(op, ir.createControlOp(op));
|
||||
} else {
|
||||
ir.OpList.replace<ir.UpdateOp>(
|
||||
|
||||
@@ -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<T>(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<T>(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<T>(value: T, sanitizer?: SanitizerFn | null): void {
|
||||
* @param lView The `LView` that contains the element.
|
||||
*/
|
||||
function getControlDirective<T>(tNode: TNode, lView: LView): ɵControl<T> | 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<T>(tNode: TNode, lView: LView): ɵControl<T> | 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<T>(tNode: TNode, lView: LView): ɵControl<T> | 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<unknown>, 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<unknown>, 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.
|
||||
|
||||
@@ -24,8 +24,8 @@ export interface ɵControl<T> {
|
||||
* 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;
|
||||
|
||||
@@ -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: `
|
||||
<div [control]="value"></div>
|
||||
<div [field]="value"></div>
|
||||
`,
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<readonly WithOptionalField<ValidationError>[]>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
/**
|
||||
* 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<readonly WithOptionalField<DisabledReason>[]>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
/**
|
||||
* 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<boolean> | InputSignal<boolean> | OutputRef<boolean>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
/**
|
||||
* 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<string>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
/**
|
||||
* 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<number | undefined>;
|
||||
/**
|
||||
* 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<number | undefined>;
|
||||
/**
|
||||
* 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<number | undefined>;
|
||||
/**
|
||||
* 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<number | undefined>;
|
||||
/**
|
||||
* 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<readonly RegExp[]>;
|
||||
@@ -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<TValue> 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<TValue>;
|
||||
@@ -129,18 +129,18 @@ export interface FormValueControl<TValue> 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<TValue> 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<boolean>;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Control<unknown>>(
|
||||
typeof ngDevMode !== undefined && ngDevMode ? 'CONTROL' : '',
|
||||
export const FIELD = new InjectionToken<Field<unknown>>(
|
||||
typeof ngDevMode !== undefined && ngDevMode ? 'FIELD' : '',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -48,10 +48,10 @@ export const CONTROL = new InjectionToken<Control<unknown>>(
|
||||
* @category control
|
||||
* @experimental 21.0.0
|
||||
*/
|
||||
@Directive({selector: '[control]', providers: [{provide: CONTROL, useExisting: Control}]})
|
||||
export class Control<T> implements ɵControl<T> {
|
||||
@Directive({selector: '[field]', providers: [{provide: FIELD, useExisting: Field}]})
|
||||
export class Field<T> implements ɵControl<T> {
|
||||
private readonly injector = inject(Injector);
|
||||
readonly field = input.required<FieldTree<T>>({alias: 'control'});
|
||||
readonly field = input.required<FieldTree<T>>();
|
||||
readonly state = computed(() => this.field()());
|
||||
readonly [ɵCONTROL] = undefined;
|
||||
|
||||
@@ -59,13 +59,18 @@ export class Control<T> implements ɵControl<T> {
|
||||
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<unknown>]);
|
||||
fieldNode.nodeState.fieldBindings.update((controls) => [
|
||||
...controls,
|
||||
this as Field<unknown>,
|
||||
]);
|
||||
onCleanup(() => {
|
||||
fieldNode.nodeState.controls.update((controls) => controls.filter((c) => c !== this));
|
||||
fieldNode.nodeState.fieldBindings.update((controls) =>
|
||||
controls.filter((c) => c !== this),
|
||||
);
|
||||
});
|
||||
},
|
||||
{injector: this.injector},
|
||||
|
||||
@@ -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<TValue, TKey extends string | number = string | numb
|
||||
*/
|
||||
readonly keyInParent: Signal<TKey>;
|
||||
/**
|
||||
* 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 Control<unknown>[]>;
|
||||
readonly fieldBindings: Signal<readonly Field<unknown>[]>;
|
||||
|
||||
/**
|
||||
* Reads an aggregate property value from the field.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<unknown> {
|
||||
return this.nodeState.readonly;
|
||||
}
|
||||
|
||||
get controls(): Signal<readonly Control<unknown>[]> {
|
||||
return this.nodeState.controls;
|
||||
get fieldBindings(): Signal<readonly Field<unknown>[]> {
|
||||
return this.nodeState.fieldBindings;
|
||||
}
|
||||
|
||||
get submitting(): Signal<boolean> {
|
||||
|
||||
@@ -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<readonly Control<unknown>[]>([]);
|
||||
/** The {@link Field} directives that bind this field to a UI control. */
|
||||
readonly fieldBindings = signal<readonly Field<unknown>[]>([]);
|
||||
|
||||
constructor(private readonly node: FieldNode) {}
|
||||
|
||||
|
||||
@@ -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: `<input [control]="control()"/>`,
|
||||
imports: [Control],
|
||||
template: `<input [field]="field()"/>`,
|
||||
imports: [Field],
|
||||
})
|
||||
class TestStringControl {
|
||||
readonly control = input.required<FieldTree<string>>();
|
||||
readonly controlDirective = viewChild.required(Control);
|
||||
readonly field = input.required<FieldTree<string>>();
|
||||
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: `<input [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly disabled = signal(false);
|
||||
@@ -86,8 +86,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
template: `<custom-control [control]="f" />`,
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
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) {
|
||||
<input #control [control]="item">
|
||||
<input #control [field]="item">
|
||||
<span>{{item().value()}}</span>
|
||||
}
|
||||
`,
|
||||
@@ -160,10 +160,10 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
imports: [Field, CustomControl],
|
||||
template: `
|
||||
@for (item of f; track item) {
|
||||
<custom-control [control]="item" />
|
||||
<custom-control [field]="item" />
|
||||
}
|
||||
`,
|
||||
})
|
||||
@@ -206,8 +206,8 @@ describe('control directive', () => {
|
||||
describe('readonly', () => {
|
||||
it('native control', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<input [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly readonly = signal(true);
|
||||
@@ -232,8 +232,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
template: `<custom-control [control]="f" />`,
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly readonly = signal(false);
|
||||
@@ -255,8 +255,8 @@ describe('control directive', () => {
|
||||
describe('required', () => {
|
||||
it('native control', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<input [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly required = signal(false);
|
||||
@@ -281,8 +281,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
template: `<custom-control [control]="f" />`,
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly required = signal(false);
|
||||
@@ -304,8 +304,8 @@ describe('control directive', () => {
|
||||
describe('max', () => {
|
||||
it('native control', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<input type="number" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly max = signal(10);
|
||||
@@ -330,8 +330,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
template: `<custom-control [control]="f" />`,
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
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: `<input type="text" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="text" [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(5), (p) => {
|
||||
@@ -369,8 +369,8 @@ describe('control directive', () => {
|
||||
describe('min', () => {
|
||||
it('native control', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<input type="number" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly min = signal(10);
|
||||
@@ -395,8 +395,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
template: `<custom-control [control]="f" />`,
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
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: `<input type="text" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="text" [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(15), (p) => {
|
||||
@@ -434,8 +434,8 @@ describe('control directive', () => {
|
||||
describe('maxLength', () => {
|
||||
it('native control', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<textarea [control]="f"></textarea>`,
|
||||
imports: [Field],
|
||||
template: `<textarea [field]="f"></textarea>`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly maxLength = signal(20);
|
||||
@@ -460,8 +460,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
template: `<custom-control [control]="f" />`,
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
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: `<select [control]="f"></select>`,
|
||||
imports: [Field],
|
||||
template: `<select [field]="f"></select>`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''), (p) => {
|
||||
@@ -499,8 +499,8 @@ describe('control directive', () => {
|
||||
describe('minLength', () => {
|
||||
it('native control', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<textarea [control]="f"></textarea>`,
|
||||
imports: [Field],
|
||||
template: `<textarea [field]="f"></textarea>`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly minLength = signal(20);
|
||||
@@ -525,8 +525,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomControl],
|
||||
template: `<custom-control [control]="f" />`,
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
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: `<select [control]="f"></select>`,
|
||||
imports: [Field],
|
||||
template: `<select [field]="f"></select>`,
|
||||
})
|
||||
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: `
|
||||
<input [control]="f">
|
||||
<input [field]="f">
|
||||
`,
|
||||
})
|
||||
class TestCmp {
|
||||
@@ -594,8 +594,8 @@ describe('control directive', () => {
|
||||
|
||||
it('synchronizes with a checkbox control', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<input type="checkbox" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="checkbox" [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form(signal(false));
|
||||
@@ -646,8 +646,8 @@ describe('control directive', () => {
|
||||
|
||||
it('synchronizes with a textarea', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
template: `<textarea #textarea [control]="f"></textarea>`,
|
||||
imports: [Field],
|
||||
template: `<textarea #textarea [field]="f"></textarea>`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form(signal(''));
|
||||
@@ -674,9 +674,9 @@ describe('control directive', () => {
|
||||
|
||||
it('synchronizes with a select', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
imports: [Field],
|
||||
template: `
|
||||
<select #select [control]="f">
|
||||
<select #select [field]="f">
|
||||
<option value="one">One</option>
|
||||
<option value="two">Two</option>
|
||||
<option value="three">Three</option>
|
||||
@@ -708,10 +708,10 @@ describe('control directive', () => {
|
||||
|
||||
it('should assign correct value when unhiding select', () => {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
imports: [Field],
|
||||
template: `
|
||||
@if (!f().hidden()) {
|
||||
<select #select [control]="f">
|
||||
<select #select [field]="f">
|
||||
@for(opt of options; track opt) {
|
||||
<option [value]="opt">{{opt}}</option>
|
||||
}
|
||||
@@ -745,8 +745,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomInput],
|
||||
template: `<my-input [control]="f" />`,
|
||||
imports: [Field, CustomInput],
|
||||
template: `<my-input [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form<string>(signal('test'));
|
||||
@@ -786,8 +786,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomInput],
|
||||
template: `<my-input [control]="f" value />`,
|
||||
imports: [Field, CustomInput],
|
||||
template: `<my-input [field]="f" value />`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form<string>(signal('test'));
|
||||
@@ -808,8 +808,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomInput],
|
||||
template: `<my-input [control]="f" />`,
|
||||
imports: [Field, CustomInput],
|
||||
template: `<my-input [field]="f" />`,
|
||||
})
|
||||
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<FieldTree<string>>();
|
||||
readonly field = input.required<FieldTree<string>>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<my-wrapper [control]="f" />`,
|
||||
imports: [WrapperCmp, Control],
|
||||
template: `<my-wrapper [field]="f" />`,
|
||||
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: `
|
||||
<input #text type="text" [control]="f.text">
|
||||
<input #number type="number" [control]="f.number">
|
||||
<input #text type="text" [field]="f.text">
|
||||
<input #number type="number" [field]="f.number">
|
||||
`,
|
||||
imports: [Control],
|
||||
imports: [Field],
|
||||
})
|
||||
class CustomPropsTestCmp {
|
||||
textInput = viewChild.required<ElementRef<HTMLInputElement>>('text');
|
||||
@@ -943,9 +943,9 @@ describe('control directive', () => {
|
||||
it('should synchronize readonly', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<input #text type="text" [control]="f">
|
||||
<input #text type="text" [field]="f">
|
||||
`,
|
||||
imports: [Control],
|
||||
imports: [Field],
|
||||
})
|
||||
class ReadonlyTestCmp {
|
||||
textInput = viewChild.required<ElementRef<HTMLInputElement>>('text');
|
||||
@@ -972,9 +972,9 @@ describe('control directive', () => {
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<my-input [control]="f" />
|
||||
<my-input [field]="f" />
|
||||
`,
|
||||
imports: [CustomInput, Control],
|
||||
imports: [CustomInput, Field],
|
||||
})
|
||||
class ReadonlyTestCmp {
|
||||
myInput = viewChild.required<CustomInput>(CustomInput);
|
||||
@@ -1006,8 +1006,8 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomInput],
|
||||
template: `<my-input [control]="f" />`,
|
||||
imports: [Field, CustomInput],
|
||||
template: `<my-input [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form<string>(signal('test'));
|
||||
@@ -1052,10 +1052,10 @@ describe('control directive', () => {
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Control, CustomInput],
|
||||
imports: [Field, CustomInput],
|
||||
template: `
|
||||
<my-input [(value)]="model" [disabledReasons]="disabledReasons" [errors]="errors" />
|
||||
<my-input [control]="f" />
|
||||
<my-input [field]="f" />
|
||||
`,
|
||||
})
|
||||
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: `<input type="number" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f">`,
|
||||
})
|
||||
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: `<input type="number" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f">`,
|
||||
})
|
||||
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: `<input type="date" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="date" [field]="f">`,
|
||||
})
|
||||
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: `<input type="date" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="date" [field]="f">`,
|
||||
})
|
||||
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: `<input type="date" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="date" [field]="f">`,
|
||||
})
|
||||
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: `<input type="datetime-local" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="datetime-local" [field]="f">`,
|
||||
})
|
||||
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: `<input type="color" [control]="f">`,
|
||||
imports: [Field],
|
||||
template: `<input type="color" [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form(signal('#ff0000'));
|
||||
@@ -1282,12 +1282,12 @@ describe('control directive', () => {
|
||||
|
||||
function setupRadioGroup() {
|
||||
@Component({
|
||||
imports: [Control],
|
||||
imports: [Field],
|
||||
template: `
|
||||
<form>
|
||||
<input type="radio" value="a" [control]="f">
|
||||
<input type="radio" value="b" [control]="f">
|
||||
<input type="radio" value="c" [control]="f">
|
||||
<input type="radio" value="a" [field]="f">
|
||||
<input type="radio" value="b" [field]="f">
|
||||
<input type="radio" value="c" [field]="f">
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user