Skip to content

Signal forms: a parse error on one control shows up on every other control bound to the same field #71632

Open

Description

@Flarette

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

forms

Is this a regression?

Yes

Description

In short. When one field is bound to several controls and one of them has a parse error, CVA and
native hosts receive that parse error in their errors too. So does NgControl.errors. Each
control should receive only the parse errors it produced, as custom controls do today.

Why it matters. Binding one field to two controls is a common pattern, for example a slider
next to a number input. Typing 5e into the number input then makes the slider show the number
input's parse error as its own. Parse errors became common in v22: native inputs produce them, and so do
legacy CVA validators.

Failure mode in user code.

@Component({
  selector: 'app-slider',
  template: `
    <input type="range" min="0" max="100" />
    @for (error of errors(); track error) {
      <small>{{ error.message ?? error.kind }}</small>
    }
  `,
  providers: [{provide: NG_VALUE_ACCESSOR, useExisting: Slider, multi: true}],
})
export class Slider implements ControlValueAccessor {
  readonly errors = input<readonly ValidationError[]>([]);
  // writeValue, registerOnChange, and registerOnTouched omitted.
}

@Component({
  imports: [FormField, Slider],
  template: `
    <app-slider [formField]="f.volume" />
    <input type="number" [formField]="f.volume" />
  `,
})
export class Mixer {
  readonly f = form(signal({volume: 50}));
}

Starting state: f.volume().value() is 50, both controls show 50, and neither has errors.

Current behavior:

1. User types "5e" into the number input
   number input  FormField.errors() → [{kind: 'parse'}]
   slider        errors input       → [{kind: 'parse'}]   the slider shows the error
   both          invalid input      → true

Expected behavior:

1. User types "5e" into the number input
   number input  FormField.errors() → [{kind: 'parse'}]
   slider        errors input       → []                  the slider shows nothing
   both          invalid input      → true

History. The filter existed for every host in v21.2.0-next.1
(ebae211add).
The refactor in v21.2.0-next.2
(a67e00741c)
kept it only for custom controls. The leak became visible in v22.0.0-next.0, when native inputs
started producing parse errors
(22afbb2f36).

Assumptions: expected vs current.

Expected Current
Each control receives only the parse errors of its own binding. ebae211add states this, and custom controls still work this way. CVA and native hosts receive the field's unfiltered errors().
inject(NgControl).errors reflects the binding it was injected for. It converts the field's unfiltered errors().
invalid is field-level, so every control on the field receives true. Same. This doesn't change.

Affected areas.

  • readFieldStateBindingValue() in packages/forms/signals/src/directive/bindings.ts, and its
    callers in control_cva.ts, control_native.ts, and control_custom.ts.
  • InteropNgControl.errors in packages/forms/signals/src/controls/interop_ng_control.ts, and how
    FormField creates it.
  • Consumers: directives with an errors input on native elements, CVA components with an errors
    input, and components that read NgControl.errors.
  • Not affected: FormField.errors(), which is already filtered, and custom controls.

Tradeoffs of the fix.

  • Routing every host through one helper that reads FormField.errors changes an internal
    function's signature and its three callers. Copying the special case into two more files is a
    smaller diff, but three copies are how the behavior drifted.
  • Filtering NgControl.errors means passing the binding's errors into InteropNgControl, which is
    an internal constructor change. It can land with the fix or later.
  • A control can receive invalid: true with empty errors. Custom controls already get that
    today, so UI that shows invalid styling without a message stays as it is.

Details.

Please provide a link to a minimal reproduction of the bug

form_field_errors_per_binding.spec.ts
in the triage PR. Both specs assert the expected behavior and fail 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 specs report:

Unexpected $[1].errorsInput[0] = 'parse from native (bad input)' in array.
Unexpected $[2].errorsInput[0] = 'parse from native (bad input)' in array.
Unexpected $[1].errors[0] = 'parse' 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). First affected prerelease: v21.2.0-next.2.

Anything else?

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

Activity

  1. added this to the needsTriage milestone on Oct 26, 2026
  2. added this to the needsTriage milestone on Oct 26, 2026
  3. Akxe commented on Oct 26, 2026

    @Akxe

    I do think that there is a need to be able to set new value to the route resource. For me, it is an optimistic update before I reload the whole resource.

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