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:
Miles Malerba
2025-10-06 16:05:06 -04:00
committed by Andrew Kushnir
parent 0acc91ef51
commit 2fdd4da2a8
20 changed files with 250 additions and 249 deletions
+24 -24
View File
@@ -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},
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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> {
+3 -3
View File
@@ -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>
`,
})
+1 -1
View File
@@ -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`.