mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(forms): only propagate schema defined properties from field to control (#64446)
Prior to this change, `FieldState` defined a signal for each built-in property. This unfortunately meant that the `Field` directive had no way of knowing which property had actually been defined in the schema, and would thus attempt to propagate them all to the bound form control. This meant that the default values of these signals would override the default or template defined values of these control properties. Now these properties are `undefined` by default, and only initialized if defined in the schema. Thus the `Field` directive will not attempt to bind any properties that aren't explicitly managed by the schema. PR Close #64446
This commit is contained in:
committed by
Jessica Janiuk
parent
4261a8c583
commit
fd9af2afaf
@@ -347,15 +347,16 @@ function updateCustomControl(
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'invalid', state.invalid);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'disabled', state.disabled);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'disabledReasons', state.disabledReasons);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'name', state.name);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'readonly', state.readonly);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'touched', state.touched);
|
||||
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'max', state.max);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'maxLength', state.maxLength);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'min', state.min);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'minLength', state.minLength);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'name', state.name);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'pattern', state.pattern);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'readonly', state.readonly);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'required', state.required);
|
||||
maybeWriteToDirectiveInput(componentDef, component, 'touched', state.touched);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,9 +371,9 @@ function maybeWriteToDirectiveInput(
|
||||
componentDef: ComponentDef<unknown>,
|
||||
component: unknown,
|
||||
inputName: string,
|
||||
source: () => unknown,
|
||||
source?: () => unknown,
|
||||
) {
|
||||
if (inputName in componentDef.inputs) {
|
||||
if (source && inputName in componentDef.inputs) {
|
||||
writeToDirectiveInput(componentDef, component, inputName, source());
|
||||
}
|
||||
}
|
||||
@@ -395,16 +396,27 @@ function updateNativeControl(tNode: TNode, lView: LView, control: ɵControl<unkn
|
||||
renderer.setAttribute(input, 'name', state.name());
|
||||
setBooleanAttribute(renderer, input, 'disabled', state.disabled());
|
||||
setBooleanAttribute(renderer, input, 'readonly', state.readonly());
|
||||
setBooleanAttribute(renderer, input, 'required', state.required());
|
||||
|
||||
if (state.required) {
|
||||
setBooleanAttribute(renderer, input, 'required', state.required());
|
||||
}
|
||||
|
||||
if (tNode.flags & TNodeFlags.isNativeNumericControl) {
|
||||
setOptionalAttribute(renderer, input, 'max', state.max());
|
||||
setOptionalAttribute(renderer, input, 'min', state.min());
|
||||
if (state.max) {
|
||||
setOptionalAttribute(renderer, input, 'max', state.max());
|
||||
}
|
||||
if (state.min) {
|
||||
setOptionalAttribute(renderer, input, 'min', state.min());
|
||||
}
|
||||
}
|
||||
|
||||
if (tNode.flags & TNodeFlags.isNativeTextControl) {
|
||||
setOptionalAttribute(renderer, input, 'maxLength', state.maxLength());
|
||||
setOptionalAttribute(renderer, input, 'minLength', state.minLength());
|
||||
if (state.maxLength) {
|
||||
setOptionalAttribute(renderer, input, 'maxLength', state.maxLength());
|
||||
}
|
||||
if (state.minLength) {
|
||||
setOptionalAttribute(renderer, input, 'minLength', state.minLength());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,28 +83,28 @@ export interface ɵFieldState<T> {
|
||||
*
|
||||
* Applies to `<input>` with a numeric or date `type` attribute and custom controls.
|
||||
*/
|
||||
readonly max: Signal<number | undefined>;
|
||||
readonly max?: Signal<number | undefined>;
|
||||
|
||||
/**
|
||||
* A signal indicating the field's maximum string length, if applicable.
|
||||
*
|
||||
* Applies to `<input>`, `<textarea>`, and custom controls.
|
||||
*/
|
||||
readonly maxLength: Signal<number | undefined>;
|
||||
readonly maxLength?: Signal<number | undefined>;
|
||||
|
||||
/**
|
||||
* A signal indicating the field's minimum value, if applicable.
|
||||
*
|
||||
* Applies to `<input>` with a numeric or date `type` attribute and custom controls.
|
||||
*/
|
||||
readonly min: Signal<number | undefined>;
|
||||
readonly min?: Signal<number | undefined>;
|
||||
|
||||
/**
|
||||
* A signal indicating the field's minimum string length, if applicable.
|
||||
*
|
||||
* Applies to `<input>`, `<textarea>`, and custom controls.
|
||||
*/
|
||||
readonly minLength: Signal<number | undefined>;
|
||||
readonly minLength?: Signal<number | undefined>;
|
||||
|
||||
/**
|
||||
* A signal of a unique name for the field, by default based on the name of its parent field.
|
||||
@@ -114,7 +114,7 @@ export interface ɵFieldState<T> {
|
||||
/**
|
||||
* A signal indicating the patterns the field must match.
|
||||
*/
|
||||
readonly pattern: Signal<readonly RegExp[]>;
|
||||
readonly pattern?: Signal<readonly RegExp[]>;
|
||||
|
||||
/**
|
||||
* A signal indicating whether the field is currently readonly.
|
||||
@@ -124,7 +124,7 @@ export interface ɵFieldState<T> {
|
||||
/**
|
||||
* A signal indicating whether the field is required.
|
||||
*/
|
||||
readonly required: Signal<boolean>;
|
||||
readonly required?: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* A signal indicating whether the field has been touched by the user.
|
||||
|
||||
@@ -146,28 +146,32 @@ export class FieldNode implements FieldState<unknown> {
|
||||
return this.nodeState.name;
|
||||
}
|
||||
|
||||
get max(): Signal<number | undefined> {
|
||||
return this.property(MAX);
|
||||
private propertyOrUndefined<M>(prop: AggregateProperty<M, any>): Signal<M> | undefined {
|
||||
return this.hasProperty(prop) ? this.property(prop) : undefined;
|
||||
}
|
||||
|
||||
get maxLength(): Signal<number | undefined> {
|
||||
return this.property(MAX_LENGTH);
|
||||
get max(): Signal<number | undefined> | undefined {
|
||||
return this.propertyOrUndefined(MAX);
|
||||
}
|
||||
|
||||
get min(): Signal<number | undefined> {
|
||||
return this.property(MIN);
|
||||
get maxLength(): Signal<number | undefined> | undefined {
|
||||
return this.propertyOrUndefined(MAX_LENGTH);
|
||||
}
|
||||
|
||||
get minLength(): Signal<number | undefined> {
|
||||
return this.property(MIN_LENGTH);
|
||||
get min(): Signal<number | undefined> | undefined {
|
||||
return this.propertyOrUndefined(MIN);
|
||||
}
|
||||
|
||||
get pattern(): Signal<readonly RegExp[]> {
|
||||
return this.property(PATTERN);
|
||||
get minLength(): Signal<number | undefined> | undefined {
|
||||
return this.propertyOrUndefined(MIN_LENGTH);
|
||||
}
|
||||
|
||||
get required(): Signal<boolean> {
|
||||
return this.property(REQUIRED);
|
||||
get pattern(): Signal<readonly RegExp[]> | undefined {
|
||||
return this.propertyOrUndefined(PATTERN);
|
||||
}
|
||||
|
||||
get required(): Signal<boolean> | undefined {
|
||||
return this.propertyOrUndefined(REQUIRED);
|
||||
}
|
||||
|
||||
property<M>(prop: AggregateProperty<M, any>): Signal<M>;
|
||||
@@ -175,7 +179,7 @@ export class FieldNode implements FieldState<unknown> {
|
||||
property<M>(prop: Property<M> | AggregateProperty<M, any>): Signal<M> | M | undefined {
|
||||
return this.propertyState.get(prop);
|
||||
}
|
||||
hasProperty(prop: Property<unknown> | AggregateProperty<unknown, any>): boolean {
|
||||
hasProperty(prop: Property<any> | AggregateProperty<any, any>): boolean {
|
||||
return this.propertyState.has(prop);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export class FieldPropertyState {
|
||||
* @param prop
|
||||
* @returns
|
||||
*/
|
||||
has(prop: Property<unknown> | AggregateProperty<unknown, unknown>): boolean {
|
||||
has(prop: Property<any> | AggregateProperty<any, any>): boolean {
|
||||
if (prop instanceof AggregateProperty) {
|
||||
// For aggregate properties, they get added to the map lazily, on first access, so we can't
|
||||
// rely on checking presence in the properties map. Instead we check if there is any logic for
|
||||
|
||||
@@ -284,7 +284,7 @@ export class LogicContainer {
|
||||
}
|
||||
|
||||
/** Checks whether there is logic for the given aggregate property. */
|
||||
hasAggregateProperty(prop: AggregateProperty<unknown, unknown>) {
|
||||
hasAggregateProperty(prop: AggregateProperty<any, any>) {
|
||||
return this.aggregateProperties.has(prop);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('field directive', () => {
|
||||
|
||||
describe('properties', () => {
|
||||
describe('disabled', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input [field]="f">`,
|
||||
@@ -81,7 +81,7 @@ describe('field directive', () => {
|
||||
expect(input.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<boolean> {
|
||||
readonly value = model(false);
|
||||
@@ -110,7 +110,7 @@ describe('field directive', () => {
|
||||
});
|
||||
|
||||
describe('name', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `
|
||||
@@ -155,7 +155,7 @@ describe('field directive', () => {
|
||||
expect(fixture.nativeElement.innerText).toBe('ba');
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: `{{value()}}`})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
@@ -207,7 +207,7 @@ describe('field directive', () => {
|
||||
});
|
||||
|
||||
describe('readonly', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input [field]="f">`,
|
||||
@@ -227,7 +227,7 @@ describe('field directive', () => {
|
||||
expect(element.readOnly).toBe(false);
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
@@ -256,7 +256,7 @@ describe('field directive', () => {
|
||||
});
|
||||
|
||||
describe('required', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input [field]="f">`,
|
||||
@@ -276,7 +276,27 @@ describe('field directive', () => {
|
||||
expect(element.required).toBe(true);
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to native control and override attribute', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input [field]="f" required>`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly required = signal(false);
|
||||
readonly f = form(signal(''), (p) => {
|
||||
required(p, {when: this.required});
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const element = fixture.nativeElement.firstChild;
|
||||
expect(element.required).withContext("'required' should be overridden").toBe(false);
|
||||
|
||||
act(() => fixture.componentInstance.required.set(true));
|
||||
expect(element.required).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
@@ -302,10 +322,47 @@ describe('field directive', () => {
|
||||
act(() => component.required.set(true));
|
||||
expect(component.customControl().required()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not bind to native control by default', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input [field]="f" required>`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const element = fixture.nativeElement.firstChild;
|
||||
expect(element.required).withContext("'required' should be unchanged").toBe(true);
|
||||
});
|
||||
|
||||
it('should not bind to custom control by default', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly required = input(true);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().required())
|
||||
.withContext("'required' should be unchanged")
|
||||
.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('max', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f">`,
|
||||
@@ -325,7 +382,7 @@ describe('field directive', () => {
|
||||
expect(element.max).toBe('5');
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
readonly value = model(0);
|
||||
@@ -367,10 +424,45 @@ describe('field directive', () => {
|
||||
const element = fixture.nativeElement.firstChild as HTMLInputElement;
|
||||
expect(element.max).toBe('');
|
||||
});
|
||||
|
||||
it('should not bind to native control by default', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f" max="123">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(0));
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const element = fixture.nativeElement.firstChild as HTMLInputElement;
|
||||
expect(element.max).withContext("'max' should be unchanged").toBe('123');
|
||||
});
|
||||
|
||||
it('should not bind to custom control by default', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
readonly value = model(0);
|
||||
readonly max = input<number | undefined>(123);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(0));
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().max()).withContext("'max' should be unchanged").toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('min', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f">`,
|
||||
@@ -390,7 +482,7 @@ describe('field directive', () => {
|
||||
expect(element.min).toBe('5');
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
readonly value = model(0);
|
||||
@@ -432,10 +524,45 @@ describe('field directive', () => {
|
||||
const element = fixture.nativeElement.firstChild as HTMLInputElement;
|
||||
expect(element.min).toBe('');
|
||||
});
|
||||
|
||||
it('should not bind to native control by default', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<input type="number" [field]="f" min="123">`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(0));
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const element = fixture.nativeElement.firstChild as HTMLInputElement;
|
||||
expect(element.min).withContext("'min' should be unchanged").toBe('123');
|
||||
});
|
||||
|
||||
it('should not bind to custom control by default', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
readonly value = model(0);
|
||||
readonly min = input<number | undefined>(123);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(0));
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().min()).withContext("'min' should be unchanged").toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxLength', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<textarea [field]="f"></textarea>`,
|
||||
@@ -455,7 +582,7 @@ describe('field directive', () => {
|
||||
expect(element.maxLength).toBe(15);
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
@@ -497,10 +624,47 @@ describe('field directive', () => {
|
||||
const element = fixture.nativeElement.firstChild as HTMLSelectElement;
|
||||
expect(element.getAttribute('maxLength')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not bind to native control by default', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<textarea [field]="f" maxLength="123"></textarea>`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const element = fixture.nativeElement.firstChild as HTMLTextAreaElement;
|
||||
expect(element.maxLength).withContext("'maxLength' should be unchanged").toBe(123);
|
||||
});
|
||||
|
||||
it('should not bind to custom control by default', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly maxLength = input<number | undefined>(123);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().maxLength())
|
||||
.withContext("'maxLength' should be unchanged")
|
||||
.toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('minLength', () => {
|
||||
it('native control', () => {
|
||||
it('should bind to native control', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<textarea [field]="f"></textarea>`,
|
||||
@@ -520,7 +684,7 @@ describe('field directive', () => {
|
||||
expect(element.minLength).toBe(15);
|
||||
});
|
||||
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
@@ -547,7 +711,7 @@ describe('field directive', () => {
|
||||
expect(component.customControl().minLength()).toBe(5);
|
||||
});
|
||||
|
||||
it('is not set on a native control that does not support it', () => {
|
||||
it('should not bind to native control that does not support it', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<select [field]="f"></select>`,
|
||||
@@ -562,10 +726,47 @@ describe('field directive', () => {
|
||||
const element = fixture.nativeElement.firstChild as HTMLSelectElement;
|
||||
expect(element.getAttribute('minLength')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not bind to native control by default', () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `<textarea [field]="f" minLength="123"></textarea>`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const element = fixture.nativeElement.firstChild as HTMLTextAreaElement;
|
||||
expect(element.minLength).withContext("'minLength' should be unchanged").toBe(123);
|
||||
});
|
||||
|
||||
it('should not bind to custom control by default', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly minLength = input<number | undefined>(123);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().minLength())
|
||||
.withContext("'minLength' should be unchanged")
|
||||
.toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pattern', () => {
|
||||
it('custom control', () => {
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
@@ -591,6 +792,29 @@ describe('field directive', () => {
|
||||
act(() => component.pattern.set(/def/));
|
||||
expect(component.customControl().pattern()).toEqual([/def/]);
|
||||
});
|
||||
|
||||
it('should not bind to custom control by default', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly pattern = input<readonly RegExp[]>([/abc/]);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Field, CustomControl],
|
||||
template: `<custom-control [field]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().pattern())
|
||||
.withContext("'pattern' should be unchanged")
|
||||
.toEqual([/abc/]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user