Skip to content

fix(forms): preserve intermediate keystrokes in signal formField - #70391

Open
shoemoney wants to merge 2 commits into
angular:mainfrom
shoemoney:fix/formfield-intermediate
Open

fix(forms): preserve intermediate keystrokes in signal formField#70391
shoemoney wants to merge 2 commits into
angular:mainfrom
shoemoney:fix/formfield-intermediate

Conversation

@shoemoney

@shoemoney shoemoney commented Aug 26, 2026

Copy link
Copy Markdown

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

When a user types an intermediate value into a signal FormField number input (such as -, 1., or 1.0), the native control is normalized via valueAsNumber on every keystroke, discarding the raw input. This causes the display to jump and intermediate input to be lost, making it impossible to type decimal numbers naturally.

Fixes #69635.

What is the new behavior?

The fix guards the native value write-back when the input is focused and contains an intermediate value. The isIntermediate() function detects incomplete decimal input (leading -, trailing ., strings like 1.0 that parse to the same numeric value but differ in representation). While intermediate, the raw string is preserved in the DOM until the user blurs the field.

Parsing now also goes through a strict decimal parser shared by isIntermediate() and the text-input numeric read path: Number() and parseFloat() must agree on the same value. This closes two edge cases raised in review:

  • A typed -0 is preserved instead of being normalized to 0 on the next update pass.
  • Non-decimal numeric literals such as 0b0101 and 0x22 are rejected as parse errors instead of being silently accepted by Number().

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Unit tests cover: preserving -, ., -., trailing ., decimal strings whose numeric values match controlValue but whose string representation differs (e.g. 1.0 vs 1, 1.00 vs 1), preserving a typed -0 and -0.5 on both a number input and a text input with a numeric model, and rejecting 0b0101 / 0x22 as parse errors.

Fix verified RED->GREEN. Signal Forms number [formField] overwrites intermediate keystrokes, dropping - and trailing zeros at control_native.ts:124
@pullapprove
pullapprove Bot requested a review from kirjs August 26, 2026 00:03
@ngbot ngbot Bot added this to the Backlog milestone Aug 26, 2026
@JeanMeche

Copy link
Copy Markdown
Member

That would be a fix for #69635. Last time we tried to fix it, #69637 was breaking.

Comment on lines +125 to +128
const isFocused = typeof document !== 'undefined' && document.activeElement === input;
if (!(isFocused && isIntermediate(input.value, controlValue))) {
setNativeControlValue(input, controlValue);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need a set of unit tests to illustrate the change.

@JeanMeche

Copy link
Copy Markdown
Member

I've run the unit tests added in PR #69637 against this branch to check if they all pass. It seems the current fix in this PR does not fully handle all the edge cases covered by those tests.

Specifically, the following 3 tests are failing:

  1. Preserving negative decimals:

    • numeric inputs > parsing logic > should preserve a negative decimal typed into a number input
    • text input with numeric model > should preserve a negative decimal typed into a text input with a numeric model
      Both fail with Expected '0' to be '-0'.. When the user types -0, it is still being normalized/parsed to 0.
  2. Parsing non-decimal numeric text:

    • text input with numeric model > should not parse non-decimal numeric text
      Fails with Expected 5 to be 42.. The test checks inputs like 0b0101 and 0x22, which shouldn't be parsed as decimal numbers (they should result in a parse error), but the current implementation likely uses Number() which incorrectly parses 0b0101 into 5.

For context, PR #69637 resolved these issues by introducing a custom parseDecimalNumber function that strictly handles decimal inputs and preserves intermediate strings correctly. You might need to incorporate a similar strict decimal parsing logic to get these edge cases completely green.

Use strict decimal parsing (Number and parseFloat must agree) instead
of Number() alone when reading a numeric text input, so non-decimal
literals like 0b0101 and 0x22 are rejected as parse errors instead of
silently accepted.

Reuse the same strict parser in isIntermediate so a typed -0 is kept
on screen instead of being normalized to 0 on the next update pass.

Adds unit test coverage for both cases.
@shoemoney

Copy link
Copy Markdown
Author

Added unit tests and switched to a strict decimal parser (Number and parseFloat must agree) shared between isIntermediate and the text-input numeric read path. This fixes the -0 normalization and stops 0b0101/0x22 from being accepted as numbers. Pushed in e3553b8.


if (controlValueChanged || radioValueChanged) {
setNativeControlValue(input, controlValue);
const isFocused = typeof document !== 'undefined' && document.activeElement === input;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this might cause any problems with SSR because we're calling document. Could I possibly use DOCUMENT to be safer, or is there a reason not to?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forms: signals - number [formField] loses characters while typing (parsed value written back to the input on every keystroke)

3 participants