mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Revert "fix(forms): preserve intermediate number values in signal forms"
This reverts commit 2e32872720.
This commit is contained in:
@@ -76,7 +76,7 @@ export function bindingUpdated<TKey extends string>(
|
||||
key: TKey,
|
||||
value: unknown,
|
||||
) {
|
||||
if (!Object.is(bindings[key], value)) {
|
||||
if (bindings[key] !== value) {
|
||||
bindings[key] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,19 +41,13 @@ export function nativeControlCreate(
|
||||
): () => void {
|
||||
let updateMode = false;
|
||||
const input = parent.nativeFormElement;
|
||||
const bindings = createBindings<ControlBindingKey | 'controlValue'>();
|
||||
|
||||
// TODO: (perf) ok to always create this?
|
||||
const parser = createParser(
|
||||
// Read from the model value
|
||||
() => parent.state().value(),
|
||||
// Write to the buffered "control value"
|
||||
(rawValue: unknown) => {
|
||||
// Mark the parsed value as already seen from this native control so the next update pass
|
||||
// does not reformat and write it back over the user's in-progress input text.
|
||||
bindings['controlValue'] = rawValue;
|
||||
parent.state().controlValue.set(rawValue);
|
||||
},
|
||||
(rawValue: unknown) => parent.state().controlValue.set(rawValue),
|
||||
// Our parse function doesn't care about the raw value that gets passed in,
|
||||
// It just reads the newly parsed value directly off the input element.
|
||||
(_rawValue: unknown) => getNativeControlValue(input, parent.state().value, validityMonitor),
|
||||
@@ -100,6 +94,8 @@ export function nativeControlCreate(
|
||||
);
|
||||
}
|
||||
|
||||
const bindings = createBindings<ControlBindingKey | 'controlValue'>();
|
||||
|
||||
return () => {
|
||||
const state = parent.state();
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ export function getNativeControlValue(
|
||||
if (element.value === '') {
|
||||
return {value: null};
|
||||
}
|
||||
const parsed = parseDecimalNumber(element.value);
|
||||
if (parsed === undefined) {
|
||||
const parsed = Number(element.value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return {error: new NativeInputParseError() as WithoutFieldTree<NativeInputParseError>};
|
||||
}
|
||||
return {value: parsed};
|
||||
@@ -164,17 +164,6 @@ export function setNativeControlValue(element: NativeFormControl, value: unknown
|
||||
element.value = value as string;
|
||||
}
|
||||
|
||||
function parseDecimalNumber(value: string): number | undefined {
|
||||
const parsed = Number(value);
|
||||
// `parseFloat` does not consume non-decimal prefixes like `0b`/`0x`, while `Number`
|
||||
// rejects trailing garbage. Requiring both to agree keeps decimal input permissive
|
||||
// without accepting other JavaScript numeric literal forms.
|
||||
if (Number.isNaN(parsed) || !Object.is(parsed, parseFloat(value))) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/** Writes a value to a native <input type="number">. */
|
||||
export function setNativeNumberControlValue(element: HTMLInputElement, value: number) {
|
||||
// Writing `NaN` causes a warning in the console, so we instead write `''`.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component, signal, viewChildren} from '@angular/core';
|
||||
import {Component, signal, viewChildren, Injectable} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {FormField, form} from '../../public_api';
|
||||
import {InputValidityMonitor} from '../../src/directive/input_validity_monitor';
|
||||
@@ -58,90 +58,6 @@ describe('numeric inputs', () => {
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve a negative decimal typed into a number input', () => {
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
template: `<input type="number" step="0.01" [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal<number | null>(null);
|
||||
readonly f = form(this.data);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
|
||||
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '-', true);
|
||||
});
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBeNull();
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([
|
||||
jasmine.objectContaining({kind: 'parse'}),
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '-0', false);
|
||||
});
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBe(0);
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([]);
|
||||
expect(input.value).toBe('-0');
|
||||
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '-0.5', false);
|
||||
});
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBe(-0.5);
|
||||
expect(input.value).toBe('-0.5');
|
||||
});
|
||||
|
||||
it('should preserve fractional zeroes while editing a number input', () => {
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
template: `<input type="number" step="0.01" [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal<number | null>(null);
|
||||
readonly f = form(this.data);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
|
||||
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1', false);
|
||||
});
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1.0', false);
|
||||
});
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1.00', false);
|
||||
});
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1.000', false);
|
||||
});
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1.0000', false);
|
||||
});
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBe(1);
|
||||
expect(input.value).toBe('1.0000');
|
||||
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1.00005', false);
|
||||
});
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1.0000', false);
|
||||
});
|
||||
act(() => {
|
||||
validityMonitor.setInputState(input, '1.00004', false);
|
||||
});
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBe(1.00004);
|
||||
expect(input.value).toBe('1.00004');
|
||||
});
|
||||
|
||||
it('should clear parse errors on one control when another control for the same field updates the model', () => {
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
@@ -289,73 +205,6 @@ describe('text input with numeric model', () => {
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve a negative decimal typed into a text input with a numeric model', () => {
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
template: `<input type="text" inputmode="decimal" [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal<number | null>(null);
|
||||
readonly f = form(this.data);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
|
||||
|
||||
setInputValue(input, '-');
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBeNull();
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([
|
||||
jasmine.objectContaining({kind: 'parse'}),
|
||||
]);
|
||||
expect(input.value).toBe('-');
|
||||
|
||||
setInputValue(input, '-0');
|
||||
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([]);
|
||||
expect(input.value).toBe('-0');
|
||||
|
||||
setInputValue(input, '-0.');
|
||||
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([]);
|
||||
expect(input.value).toBe('-0.');
|
||||
|
||||
setInputValue(input, '-0.5');
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBe(-0.5);
|
||||
expect(input.value).toBe('-0.5');
|
||||
});
|
||||
|
||||
it('should not parse non-decimal numeric text', () => {
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
template: `<input type="text" [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal<number | null>(42);
|
||||
readonly f = form(this.data);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
|
||||
|
||||
setInputValue(input, '0b0101');
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBe(42);
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([
|
||||
jasmine.objectContaining({kind: 'parse'}),
|
||||
]);
|
||||
expect(input.value).toBe('0b0101');
|
||||
|
||||
setInputValue(input, '0x22');
|
||||
|
||||
expect(fixture.componentInstance.f().value()).toBe(42);
|
||||
expect(fixture.componentInstance.f().errors()).toEqual([
|
||||
jasmine.objectContaining({kind: 'parse'}),
|
||||
]);
|
||||
expect(input.value).toBe('0x22');
|
||||
});
|
||||
|
||||
it('should produce a parse error when user types non-numeric text', () => {
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
@@ -459,13 +308,6 @@ describe('text input with numeric model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function setInputValue(input: HTMLInputElement, value: string) {
|
||||
act(() => {
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event('input'));
|
||||
});
|
||||
}
|
||||
|
||||
function act<T>(fn: () => T): T {
|
||||
try {
|
||||
return fn();
|
||||
|
||||
Reference in New Issue
Block a user