diff --git a/packages/forms/signals/src/directive/bindings.ts b/packages/forms/signals/src/directive/bindings.ts index 436bd5121d8..0de9b8fd834 100644 --- a/packages/forms/signals/src/directive/bindings.ts +++ b/packages/forms/signals/src/directive/bindings.ts @@ -76,7 +76,7 @@ export function bindingUpdated( key: TKey, value: unknown, ) { - if (!Object.is(bindings[key], value)) { + if (bindings[key] !== value) { bindings[key] = value; return true; } diff --git a/packages/forms/signals/src/directive/control_native.ts b/packages/forms/signals/src/directive/control_native.ts index 1bff19c644d..aa6fcd412de 100644 --- a/packages/forms/signals/src/directive/control_native.ts +++ b/packages/forms/signals/src/directive/control_native.ts @@ -41,19 +41,13 @@ export function nativeControlCreate( ): () => void { let updateMode = false; const input = parent.nativeFormElement; - const bindings = createBindings(); // 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(); + return () => { const state = parent.state(); diff --git a/packages/forms/signals/src/directive/native.ts b/packages/forms/signals/src/directive/native.ts index 5ce84ecc6da..d536a4d15e8 100644 --- a/packages/forms/signals/src/directive/native.ts +++ b/packages/forms/signals/src/directive/native.ts @@ -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}; } 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 . */ export function setNativeNumberControlValue(element: HTMLInputElement, value: number) { // Writing `NaN` causes a warning in the console, so we instead write `''`. diff --git a/packages/forms/signals/test/web/number_input.spec.ts b/packages/forms/signals/test/web/number_input.spec.ts index b148f4fdfa7..6e11aabcd3f 100644 --- a/packages/forms/signals/test/web/number_input.spec.ts +++ b/packages/forms/signals/test/web/number_input.spec.ts @@ -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: ``, - }) - class TestCmp { - readonly data = signal(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: ``, - }) - class TestCmp { - readonly data = signal(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: ``, - }) - class TestCmp { - readonly data = signal(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: ``, - }) - class TestCmp { - readonly data = signal(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(fn: () => T): T { try { return fn();