From 71e8672837eb6c2da2570eb2341e896fbf7ca5a3 Mon Sep 17 00:00:00 2001 From: Leon Senft Date: Wed, 17 Sep 2025 14:09:25 -0700 Subject: [PATCH] fix(forms): test that minLength/maxLength properties are propagated to controls (#63884) Ensure that minLength and maxLength are only bound to native input elements that support them. PR Close #63884 --- .../core/src/render3/instructions/control.ts | 20 ++- .../test/web/control_directive.spec.ts | 130 ++++++++++++++++++ 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/packages/core/src/render3/instructions/control.ts b/packages/core/src/render3/instructions/control.ts index 3a48210435f..bd5cfb2cbc8 100644 --- a/packages/core/src/render3/instructions/control.ts +++ b/packages/core/src/render3/instructions/control.ts @@ -388,9 +388,11 @@ function updateNativeControl(tNode: TNode, lView: LView, control: ɵControl { expect(element.min).toBe(''); }); }); + + describe('maxLength', () => { + it('native control', () => { + @Component({ + imports: [Control], + template: ``, + }) + class TestCmp { + readonly maxLength = signal(20); + readonly f = form(signal(''), (p) => { + maxLength(p, this.maxLength); + }); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const element = fixture.nativeElement.firstChild as HTMLTextAreaElement; + expect(element.maxLength).toBe(20); + + act(() => fixture.componentInstance.maxLength.set(15)); + expect(element.maxLength).toBe(15); + }); + + it('custom control', () => { + @Component({selector: 'custom-control', template: ``}) + class CustomControl { + readonly value = model(''); + readonly maxLength = input(null); + } + + @Component({ + imports: [Control, CustomControl], + template: ``, + }) + class TestCmp { + readonly maxLength = signal(10); + readonly f = form(signal(''), (p) => { + maxLength(p, this.maxLength); + }); + readonly customControl = viewChild.required(CustomControl); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const component = fixture.componentInstance; + expect(component.customControl().maxLength()).toBe(10); + + act(() => component.maxLength.set(5)); + expect(component.customControl().maxLength()).toBe(5); + }); + + it('is not set on a native control that does not support it', () => { + @Component({ + imports: [Control], + template: ``, + }) + class TestCmp { + readonly f = form(signal(''), (p) => { + maxLength(p, 10); + }); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const element = fixture.nativeElement.firstChild as HTMLSelectElement; + expect(element.getAttribute('maxLength')).toBeNull(); + }); + }); + + describe('minLength', () => { + it('native control', () => { + @Component({ + imports: [Control], + template: ``, + }) + class TestCmp { + readonly minLength = signal(20); + readonly f = form(signal(''), (p) => { + minLength(p, this.minLength); + }); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const element = fixture.nativeElement.firstChild as HTMLTextAreaElement; + expect(element.minLength).toBe(20); + + act(() => fixture.componentInstance.minLength.set(15)); + expect(element.minLength).toBe(15); + }); + + it('custom control', () => { + @Component({selector: 'custom-control', template: ``}) + class CustomControl { + readonly value = model(''); + readonly minLength = input(null); + } + + @Component({ + imports: [Control, CustomControl], + template: ``, + }) + class TestCmp { + readonly minLength = signal(10); + readonly f = form(signal(''), (p) => { + minLength(p, this.minLength); + }); + readonly customControl = viewChild.required(CustomControl); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const component = fixture.componentInstance; + expect(component.customControl().minLength()).toBe(10); + + act(() => component.minLength.set(5)); + expect(component.customControl().minLength()).toBe(5); + }); + + it('is not set on a native control that does not support it', () => { + @Component({ + imports: [Control], + template: ``, + }) + class TestCmp { + readonly f = form(signal(''), (p) => { + minLength(p, 10); + }); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const element = fixture.nativeElement.firstChild as HTMLSelectElement; + expect(element.getAttribute('minLength')).toBeNull(); + }); + }); }); it('synchronizes a basic form with a custom control', () => {