Skip to content

Signal forms: compatForm() fields ignore parse errors, so submit() sends the last valid value #71633

Open

Description

@Flarette

Which @angular/* package(s) are the source of the bug?

forms

Is this a regression?

No

Description

In short. When a compatForm() field is bound with [formField] to a control that can't parse
what the user typed, the field ignores the parse error. For example, <input type="number"> with
30e in it. The field stays valid, no error shows, and submit() runs with the last value that
parsed. The same template on a plain form() field blocks the submit.

Why it matters. compatForm() is the documented way to migrate a reactive form to signal forms
step by step, and the migration guide
binds compat fields with [formField]. The failure is silent: the user sees one value, and the app
saves another.

Failure mode in user code.

@Component({
  imports: [FormField],
  template: `
    <input type="number" [formField]="f.age" />
    @if (f.age().invalid()) {
      <p>Enter a valid age.</p>
    }
    <button (click)="save()">Save</button>
  `,
})
export class Profile {
  readonly model = signal({age: new FormControl(30, {nonNullable: true})});
  readonly f = compatForm(this.model);

  save() {
    submit(this.f, async () => {
      await saveProfile({age: this.model().age.value});
    });
  }
}

Starting state: the FormControl value is 30, the input shows 30, f.age().errors() is [],
and f.age().invalid() is false.

Current behavior:

1. User types "30e"   input shows "30e"; FormControl value stays 30
                      f.age().errors()  → []
                      f.age().invalid() → false
                      no error message
2. User clicks Save   the action runs and saves {age: 30}

Expected behavior:

1. User types "30e"   input shows "30e"; FormControl value stays 30
                      f.age().errors()  → [{kind: 'parse', ...}]
                      f.age().invalid() → true
                      "Enter a valid age." shows
2. User clicks Save   the action doesn't run

Assumptions: expected vs current.

Expected Current
A compat field reports its controls' parse errors like a plain field does: in errors(), in invalid(), and at the submit gate. CompatValidationState.parseErrors is hardcoded to [].
The FormControl's own state still shows through ("Reactive state is proxied automatically" in the guide). Parse errors add to it. errors, valid, invalid, and syncValid come only from the FormControl.
The control that reported the parse error can show it. FormField.errors() filters the field's errors(), so the binding's own errors are empty too.

Affected areas.

  • CompatValidationState in packages/forms/signals/compat/src/compat_validation_state.ts:
    parseErrors, errors, syncValid, valid, and invalid.
  • Every source of parse errors, on a compat field:
    • Native inputs with bad input, such as number, date, and time.
    • Legacy NG_VALIDATORS on a CVA, which are shimmed into parse errors.
    • Custom controls that report parseErrors.
  • submit() on a compat root, and on a plain root with compat children.
  • The migration guide's statements that reactive state is proxied and that errors() returns
    CompatValidationError.

Tradeoffs of the fix.

  • After the fix, f.age().invalid() is true while the FormControl and its parent FormGroup
    still report valid. Parse errors live in the signal-forms layer, so reactive code that checks
    formGroup.valid disagrees with submit().
  • Writing parse errors into the FormControl with setErrors() would avoid that split, but the next
    updateValueAndValidity() wipes them, and it fights reactive forms' ownership of the control.
  • errors() would mix CompatValidationError with parse errors, which have no control or
    context. The public type doesn't change, and the guide's errorMessage() helper on main
    already falls back to error.message ?? error.kind.
  • Only documenting the limitation is cheapest, but it leaves a silent submit with the wrong value.

For comparison, the two APIs that compat bridges both catch this case. Reactive forms'
NumberValueAccessor writes null for bad input, so Validators.required fails. Plain signal
forms block the submit.

Related bug, same file. A compat field's errorSummary is always empty, so the root's
errorSummary() omits every FormControl error: #71634.

Details.

Please provide a link to a minimal reproduction of the bug

compat_parse_errors.spec.ts
in the triage PR. The repro spec asserts the expected behavior and fails today. The web test
runner ignores --filter, so this runs the whole Chromium suite:

pnpm bazel test //packages/forms/signals/test/web:test_chromium --test_output=all

Please provide the exception or error you saw

No error is thrown. The repro spec reports:

Expected $.fieldErrors.length = 0 to equal 1.
Expected $.fieldInvalid = false to equal true.
Expected $.rootInvalid = false to equal true.
Unexpected $.actionSaw[0] = 5 in array.

Please provide the environment you discovered this bug in (run ng version)

angular/angular main at ce30bb0889 (2026-10-16), Bazel target
//packages/forms/signals/test/web:test_chromium. The affected source files are unchanged on main at
3e173fc844 (2026-10-24). Native inputs have produced parse errors since v22.0.0-next.0.

Anything else?

I searched for an existing issue or PR and found none.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions