GeezerVison is showing github.com — test(forms): add repro specs for compat parse errors by Flarette · Pull Request #71628 · angular/angular
Conversation
Flarette
commented
Oct 26, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
Issue #71633 | Triage notes | Repro specs
Why the change
A compatForm() field drops the parse errors its controls report, so bad input leaves the field valid and submit() runs with the last good value; this draft adds a failing repro spec and asks you to settle how compat validity should work before I write the fix.
Special things to note
- This is a triage draft with no fix. The repro spec fails on purpose, so CI is red.
- The fix makes a compat field report
invalid while its FormControl still reports valid. That
split is the main decision below.
- A related bug,
FormControl errors missing from errorSummary(), has its own triage PR:
test(forms): add repro specs for compat errorSummary #71630. Both fixes change CompatValidationState, so whichever lands second needs a rebase.
Change outline
What the branch adds:
packages/forms/signals/
├── test/web/compat_parse_errors.spec.ts # 1 failing repro, 1 passing control spec
└── triage/compat-parse-errors.md # observed output, root cause, history
Where a parse error goes, for <input type="number" [formField]="f.age"> with 5e typed:
FormField.parseErrors() → [{kind: 'parse'}]
CompatValidationState
- parseErrors → [] (hardcoded)
- errors, valid, invalid, syncValid → the FormControl only
+ parseErrors → the parse errors of every binding
+ errors → parse errors, then the FormControl's errors
+ valid, syncValid → the FormControl's value, and no parse errors
+ invalid → the FormControl's value, or any parse error
FormField.errors() (filters the field's errors())
- → []
+ → [{kind: 'parse'}]
submit(f, action)
- runs the action; the model still holds 5
+ doesn't run the action
Recommended fix: merge binding parse errors in the compat layer
Do what FieldValidationState does (compat/src/compat_validation_state.ts):
- readonly errors: Signal<CompatValidationError[]>;
+ readonly errors: Signal<ValidationError.WithFieldTree[]>;
- readonly parseErrors: Signal<ValidationError.WithFormField[]> = computed(() => []);
+ readonly parseErrors: Signal<ValidationError.WithFormField[]> = computed(
+ () => this.node.formFieldBindings().flatMap((binding) => binding.parseErrors()),
+ {equal: shallowArrayEquals},
+ );
constructor(private readonly node: CompatFieldNode, options: CompatFieldNodeOptions) {
- this.syncValid = getControlStatusSignal(options, (c) => c.status === 'VALID');
- this.errors = getControlStatusSignal(options, extractNestedReactiveErrors);
- this.valid = getControlStatusSignal(options, (c) => c.valid);
- this.invalid = getControlStatusSignal(options, (c) => c.invalid);
+ const controlSyncValid = getControlStatusSignal(options, (c) => c.status === 'VALID');
+ const controlErrors = getControlStatusSignal(options, extractNestedReactiveErrors);
+ const controlValid = getControlStatusSignal(options, (c) => c.valid);
+ const controlInvalid = getControlStatusSignal(options, (c) => c.invalid);
+ const hasParseErrors = computed(() => this.parseErrors().length > 0);
+
+ this.errors = computed(() => [...this.parseErrors(), ...controlErrors()], {
+ equal: shallowArrayEquals,
+ });
+ this.syncValid = computed(() => controlSyncValid() && !hasParseErrors());
+ this.valid = computed(() => controlValid() && !hasParseErrors());
+ this.invalid = computed(() => controlInvalid() || hasParseErrors());
this.pending = getControlStatusSignal(options, (c) => c.pending);
}
status already derives from errors() and pending(). That covers both submit paths: a compat
root reads its own invalid and valid, and a plain root reads each compat child's status().
Alternatives considered
- Write parse errors into the
FormControl with setErrors(). The next
updateValueAndValidity() wipes them, and it fights reactive forms' ownership of the control.
Not recommended.
- Only document the limitation. This is the cheapest option, but it leaves a silent submit with
the wrong value.
Design decisions
- Signal-layer validity and
FormControl validity disagree. After the fix,
f.age().invalid() is true while FormControl.valid and its parent FormGroup.valid stay
true. Code that gates on the reactive status disagrees with submit(). The migration guide says
"Reactive state is proxied automatically" (migration.md:46), so the guide needs a sentence on
this. Today compat is worse than both APIs it bridges: reactive forms' NumberValueAccessor
writes null for bad input, so Validators.required catches it, and plain signal forms block the
submit.
- Error types in
errors(). The guide says errors() returns CompatValidationError
(migration.md:48). After the fix, parse errors appear too, and they have no control or
context. The public type doesn't change, because compatForm() returns a plain FieldTree.
The errorMessage() helper the guide added on main already handles this: it checks
instanceof CompatValidationError and falls back to error.message ?? error.kind.
- Order. Parse errors come first, as in
FieldValidationState.
Questions for maintainers
- Is it acceptable for a compat field to report
invalid while its FormControl reports valid,
with a sentence in the migration guide?
- Should parse errors come before the
FormControl's errors in errors(), as they do on plain
fields?
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: triage. Failing repro specs and triage notes, with no library changes.
What is the current behavior?
Issue Number: #71633
What is the new behavior?
No behavior changes yet. This PR adds failing repro specs and triage notes. The fix will replace the
notes on this branch once the questions above are answered.
Does this PR introduce a breaking change?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
A `compatForm()` field drops parse errors from its bindings, so a
number input with bad input leaves the field valid and `submit()` runs
with the last good value.
These specs assert the expected behavior and fail on purpose. The
triage notes explain the root cause. A fix will replace the notes once
maintainers settle the design questions.
|
Going to close the PRs as they're won't me merged as is. Let's move the discussions to the issues.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|