Skip to content

Commit 394ad0c

Browse files
alxhubatscott
authored andcommitted
fix(forms): allow late-bound input types for signals forms
Ensure that input [type] bindings are evaluated dynamically rather than cached eagerly during initialization. This allows late-bound expressions for input types to correctly apply constraints like min/max and maxLength. Fixes angular#66987
1 parent 9c55fcb commit 394ad0c

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

packages/forms/signals/src/directive/form_field.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ export class FormField<T> {
131131

132132
// Compute some helper booleans about the type of element we're sitting on.
133133
private readonly elementIsNativeFormElement = isNativeFormElement(this.element);
134-
private readonly elementAcceptsNumericValues = isNumericFormElement(this.element);
135134
private readonly elementAcceptsTextualValues = isTextualFormElement(this.element);
135+
private _elementAcceptsNumericValues: boolean | undefined;
136136

137137
/**
138138
* Utility that casts `this.element` to `NativeFormControl` to avoid repeated type guards. Only
@@ -360,7 +360,8 @@ export class FormField<T> {
360360
switch (key) {
361361
case 'min':
362362
case 'max':
363-
return this.elementAcceptsNumericValues;
363+
return (this._elementAcceptsNumericValues ??= isNumericFormElement(this.element));
364+
364365
case 'minLength':
365366
case 'maxLength':
366367
return this.elementAcceptsTextualValues;

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2633,6 +2633,25 @@ describe('field directive', () => {
26332633
expect(element.min).toBe('5');
26342634
});
26352635

2636+
it('should apply min/max if type is late-bound to a numeric type during initialization', () => {
2637+
@Component({
2638+
imports: [FormField],
2639+
template: `<input [type]="inputType()" [formField]="f" />`,
2640+
})
2641+
class TestCmp {
2642+
readonly inputType = signal('number');
2643+
readonly min = signal(10);
2644+
readonly f = form(signal(15), (p) => {
2645+
min(p, this.min);
2646+
});
2647+
}
2648+
2649+
const fixture = act(() => TestBed.createComponent(TestCmp));
2650+
const input = fixture.nativeElement.firstChild as HTMLInputElement;
2651+
2652+
expect(input.min).toBe('10');
2653+
});
2654+
26362655
it('should bind to a custom control host directive', () => {
26372656
@Directive()
26382657
class CustomControlDir implements FormValueControl<number> {
@@ -3021,6 +3040,24 @@ describe('field directive', () => {
30213040
expect(element.getAttribute('maxlength')).toBeNull();
30223041
});
30233042

3043+
it('should apply maxLength if type is late-bound to a textual type during initialization', () => {
3044+
@Component({
3045+
imports: [FormField],
3046+
template: `<input [type]="inputType()" [formField]="f" />`,
3047+
})
3048+
class TestCmp {
3049+
readonly inputType = signal('email');
3050+
readonly f = form(signal('abc'), (p) => {
3051+
maxLength(p, 10);
3052+
});
3053+
}
3054+
3055+
const fixture = act(() => TestBed.createComponent(TestCmp));
3056+
const input = fixture.nativeElement.firstChild as HTMLInputElement;
3057+
3058+
expect(input.maxLength).toBe(10);
3059+
});
3060+
30243061
it('should be reset when field changes on native control', () => {
30253062
@Component({
30263063
imports: [FormField],

0 commit comments

Comments
 (0)