diff --git a/packages/core/src/render3/instructions/control.ts b/packages/core/src/render3/instructions/control.ts index 38fe5c53ff5..8e640d60e49 100644 --- a/packages/core/src/render3/instructions/control.ts +++ b/packages/core/src/render3/instructions/control.ts @@ -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, 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 { * * Applies to `` with a numeric or date `type` attribute and custom controls. */ - readonly max: Signal; + readonly max?: Signal; /** * A signal indicating the field's maximum string length, if applicable. * * Applies to ``, ``, @@ -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 { 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: ``, + }) + 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 { + readonly value = model(''); + readonly maxLength = input(123); + } + + @Component({ + imports: [Field, CustomControl], + template: ``, + }) + 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: ``, @@ -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 { 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: ``, @@ -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: ``, + }) + 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 { + readonly value = model(''); + readonly minLength = input(123); + } + + @Component({ + imports: [Field, CustomControl], + template: ``, + }) + 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 { 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 { + readonly value = model(''); + readonly pattern = input([/abc/]); + } + + @Component({ + imports: [Field, CustomControl], + template: ``, + }) + 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/]); + }); }); });