Skip to content

Commit 71e8672

Browse files
leonsenftAndrewKushnir
authored andcommitted
fix(forms): test that minLength/maxLength properties are propagated to controls (angular#63884)
Ensure that minLength and maxLength are only bound to native input elements that support them. PR Close angular#63884
1 parent acd7c83 commit 71e8672

File tree

2 files changed

+147
-3
lines changed

2 files changed

+147
-3
lines changed

packages/core/src/render3/instructions/control.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,11 @@ function updateNativeControl(tNode: TNode, lView: LView, control: ɵControl<unkn
388388
}
389389

390390
// TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131711472
391-
// * use tag and type attribute to determine which of these properties to bind.
392-
setOptionalAttribute(renderer, input, 'maxLength', state.maxLength());
393-
setOptionalAttribute(renderer, input, 'minLength', state.minLength());
391+
// * cache this in `tNode.flags`.
392+
if (isTextInput(input)) {
393+
setOptionalAttribute(renderer, input, 'maxLength', state.maxLength());
394+
setOptionalAttribute(renderer, input, 'minLength', state.minLength());
395+
}
394396
}
395397

396398
/** Checks if a given value is a Date or null */
@@ -413,6 +415,18 @@ function isNumericInput(control: NativeControlElement) {
413415
return false;
414416
}
415417

418+
/**
419+
* Returns whether `control` is a text-based input.
420+
*
421+
* This is not the same as an input with `type="text"`, but rather any input that accepts
422+
* text-based input which includes numeric types.
423+
*/
424+
function isTextInput(
425+
control: NativeControlElement,
426+
): control is HTMLInputElement | HTMLTextAreaElementNarrowed {
427+
return !(control instanceof HTMLSelectElement);
428+
}
429+
416430
/**
417431
* Returns the value from a native control element.
418432
*

packages/forms/signals/test/web/control_directive.spec.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,136 @@ describe('control directive', () => {
429429
expect(element.min).toBe('');
430430
});
431431
});
432+
433+
describe('maxLength', () => {
434+
it('native control', () => {
435+
@Component({
436+
imports: [Control],
437+
template: `<textarea [control]="f"></textarea>`,
438+
})
439+
class TestCmp {
440+
readonly maxLength = signal(20);
441+
readonly f = form(signal(''), (p) => {
442+
maxLength(p, this.maxLength);
443+
});
444+
}
445+
446+
const fixture = act(() => TestBed.createComponent(TestCmp));
447+
const element = fixture.nativeElement.firstChild as HTMLTextAreaElement;
448+
expect(element.maxLength).toBe(20);
449+
450+
act(() => fixture.componentInstance.maxLength.set(15));
451+
expect(element.maxLength).toBe(15);
452+
});
453+
454+
it('custom control', () => {
455+
@Component({selector: 'custom-control', template: ``})
456+
class CustomControl {
457+
readonly value = model('');
458+
readonly maxLength = input<number | null>(null);
459+
}
460+
461+
@Component({
462+
imports: [Control, CustomControl],
463+
template: `<custom-control [control]="f" />`,
464+
})
465+
class TestCmp {
466+
readonly maxLength = signal(10);
467+
readonly f = form(signal(''), (p) => {
468+
maxLength(p, this.maxLength);
469+
});
470+
readonly customControl = viewChild.required(CustomControl);
471+
}
472+
473+
const fixture = act(() => TestBed.createComponent(TestCmp));
474+
const component = fixture.componentInstance;
475+
expect(component.customControl().maxLength()).toBe(10);
476+
477+
act(() => component.maxLength.set(5));
478+
expect(component.customControl().maxLength()).toBe(5);
479+
});
480+
481+
it('is not set on a native control that does not support it', () => {
482+
@Component({
483+
imports: [Control],
484+
template: `<select [control]="f"></select>`,
485+
})
486+
class TestCmp {
487+
readonly f = form(signal(''), (p) => {
488+
maxLength(p, 10);
489+
});
490+
}
491+
492+
const fixture = act(() => TestBed.createComponent(TestCmp));
493+
const element = fixture.nativeElement.firstChild as HTMLSelectElement;
494+
expect(element.getAttribute('maxLength')).toBeNull();
495+
});
496+
});
497+
498+
describe('minLength', () => {
499+
it('native control', () => {
500+
@Component({
501+
imports: [Control],
502+
template: `<textarea [control]="f"></textarea>`,
503+
})
504+
class TestCmp {
505+
readonly minLength = signal(20);
506+
readonly f = form(signal(''), (p) => {
507+
minLength(p, this.minLength);
508+
});
509+
}
510+
511+
const fixture = act(() => TestBed.createComponent(TestCmp));
512+
const element = fixture.nativeElement.firstChild as HTMLTextAreaElement;
513+
expect(element.minLength).toBe(20);
514+
515+
act(() => fixture.componentInstance.minLength.set(15));
516+
expect(element.minLength).toBe(15);
517+
});
518+
519+
it('custom control', () => {
520+
@Component({selector: 'custom-control', template: ``})
521+
class CustomControl {
522+
readonly value = model('');
523+
readonly minLength = input<number | null>(null);
524+
}
525+
526+
@Component({
527+
imports: [Control, CustomControl],
528+
template: `<custom-control [control]="f" />`,
529+
})
530+
class TestCmp {
531+
readonly minLength = signal(10);
532+
readonly f = form(signal(''), (p) => {
533+
minLength(p, this.minLength);
534+
});
535+
readonly customControl = viewChild.required(CustomControl);
536+
}
537+
538+
const fixture = act(() => TestBed.createComponent(TestCmp));
539+
const component = fixture.componentInstance;
540+
expect(component.customControl().minLength()).toBe(10);
541+
542+
act(() => component.minLength.set(5));
543+
expect(component.customControl().minLength()).toBe(5);
544+
});
545+
546+
it('is not set on a native control that does not support it', () => {
547+
@Component({
548+
imports: [Control],
549+
template: `<select [control]="f"></select>`,
550+
})
551+
class TestCmp {
552+
readonly f = form(signal(''), (p) => {
553+
minLength(p, 10);
554+
});
555+
}
556+
557+
const fixture = act(() => TestBed.createComponent(TestCmp));
558+
const element = fixture.nativeElement.firstChild as HTMLSelectElement;
559+
expect(element.getAttribute('minLength')).toBeNull();
560+
});
561+
});
432562
});
433563

434564
it('synchronizes a basic form with a custom control', () => {

0 commit comments

Comments
 (0)