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
@@ -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