mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(forms): support binding number|null to <input type="text">
`<input type="number">` often does not provide the desired user experience when editing numbers in a form. MDN even [describes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number#using_number_inputs) how text inputs should be used in many cases instead, via `<input type="text" inputmode="numeric">` or similar configurations. Previously, this did not work with Signal Forms without a custom input component/directive. This PR builds support for binding `number|null` models directly to `<input type="text">` native controls via `[formField]`. When a model has a number or `null` value, signal forms will preserve that status when the user makes edits/changes. Empty string values are converted to `null`, other values are parsed as numbers, and a parse error is raised when a non-numeric value is entered. Note that it's up to the UI developer to configure additional UI affordances such as setting an appropriate `inputmode`, rejecting non-numeric keypresses, etc. Fixes #66903 Fixes #66157
This commit is contained in:
committed by
Leon Senft
parent
e850643b1b
commit
41b1410cb8
@@ -106,7 +106,7 @@ runInEachFileSystem(() => {
|
||||
expect(extractMessage(diags[0])).toBe(`Type 'null' is not assignable to type 'string'.`);
|
||||
});
|
||||
|
||||
it('should infer an input without a `type` as a string field', () => {
|
||||
it('should reject a numeric field without null on a bare input', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -123,6 +123,120 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(extractMessage(diags[0])).toContain(
|
||||
`is not assignable to type '{ (): string; set: (v: string) => void; } | { (): number | null; set: (v: number | null) => void; }'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow a string field on a bare input', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {FormField, form} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
template: '<input [formField]="f"/>',
|
||||
imports: [FormField]
|
||||
})
|
||||
export class Comp {
|
||||
f = form(signal(''));
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow a number|null field on a text input', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {FormField, form} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
template: '<input type="text" [formField]="f"/>',
|
||||
imports: [FormField]
|
||||
})
|
||||
export class Comp {
|
||||
f = form(signal<number | null>(42));
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should reject a string|null field on a text input', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {FormField, form} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
template: '<input type="text" [formField]="f"/>',
|
||||
imports: [FormField]
|
||||
})
|
||||
export class Comp {
|
||||
f = form(signal<string | null>(null));
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(extractMessage(diags[0])).toContain(
|
||||
`is not assignable to type '{ (): string; set: (v: string) => void; } | { (): number | null; set: (v: number | null) => void; }'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject a boolean field on a text input', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {FormField, form} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
template: '<input type="text" [formField]="f"/>',
|
||||
imports: [FormField]
|
||||
})
|
||||
export class Comp {
|
||||
f = form(signal(true));
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(extractMessage(diags[0])).toContain(
|
||||
`is not assignable to type '{ (): string; set: (v: string) => void; } | { (): number | null; set: (v: number | null) => void; }'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject a number field on a textarea', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {FormField, form} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
template: '<textarea [formField]="f"></textarea>',
|
||||
imports: [FormField]
|
||||
})
|
||||
export class Comp {
|
||||
f = form(signal(0));
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(extractMessage(diags[0])).toBe(`Type 'number' is not assignable to type 'string'.`);
|
||||
|
||||
Reference in New Issue
Block a user