Skip to content

Fix/ngsubmit await async validators#68661

Open
manichandra wants to merge 2 commits into
angular:mainfrom
manichandra:fix/ngsubmit-await-async-validators
Open

Fix/ngsubmit await async validators#68661
manichandra wants to merge 2 commits into
angular:mainfrom
manichandra:fix/ngsubmit-await-async-validators

Conversation

@manichandra
Copy link
Copy Markdown

@manichandra manichandra commented May 10, 2026

PR Checklist

PR Type

  • 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?

NgForm and FormGroupDirective emit ngSubmit synchronously on form submission even when the form has pending async validators
(status === 'PENDING'). Handlers that check form.valid receive a false negative because async validation hasn't resolved yet.

Issue Number: #31021

What is the new behavior?

Adds an opt-in awaitAsyncValidators boolean input (default false) to both NgForm and AbstractFormDirective. When enabled and the
form is PENDING at submission time, ngSubmit is deferred until statusChanges emits a non-PENDING value.

Template-driven:

<form (ngSubmit)="onSubmit()" awaitAsyncValidators>

Reactive:
<form [formGroup]="myForm" (ngSubmit)="onSubmit()" awaitAsyncValidators>

Does this PR introduce a breaking change?

  • Yes
  • No
Other information

Pending subscription is cancelled on directive destroy to prevent memory leaks. If submit is called again while a previous async submit 
is still pending, the prior subscription is cancelled — only the latest submit wins.

@pullapprove pullapprove Bot requested a review from crisbeto May 10, 2026 16:55
@ngbot ngbot Bot added this to the Backlog milestone May 10, 2026
Comment on lines +336 to +346
if (this.awaitAsyncValidators && this.form.status === 'PENDING') {
this._pendingSubmitSub?.unsubscribe();
this._pendingSubmitSub = this.form.statusChanges
.pipe(
filter((status) => status !== 'PENDING'),
take(1),
)
.subscribe(() => {
this.ngSubmit.emit($event);
this.form._events.next(new FormSubmittedEvent(this.control));
});
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.

This is generaly not a recommended practice to hide async work in a sync function.
Here you wouldn't handle consecutive submits that would cancel the pending one (if you'd like to cancel it).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the feedback! Refactored to address both concerns:

  • Async in sync function: onSubmit() is now fully synchronous — it just pushes to a Subject. The async pipeline lives in the constructor where its nature is explicit.
  • Consecutive submits: Replaced manual unsubscribe() with switchMap, which makes the "cancel previous pending submit" behavior idiomatic and explicit.

Let me know if you'd prefer a different approach for the consecutive-submit semantics (e.g. blocking subsequent submits instead of cancelling).

@manichandra
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA.

@SkyZeroZx
Copy link
Copy Markdown
Contributor

SkyZeroZx commented May 11, 2026

I have read the CLA Document and I hereby sign the CLA.

The commits were co-authored with <noreply@anthropic.com> (You should remove the co-authored code in this case ) Please squash them into a single commit

On the other hand, I see the tests are failing; could you please review them as well

…sync validation completes

Adds an opt-in `awaitAsyncValidators` boolean input to `NgForm` and
`FormGroupDirective`. When set to `true`, the submit handler defers
`ngSubmit` emission until all async validators have settled (transitioned
from PENDING to VALID or INVALID).

onSubmit() remains synchronous — it pushes to a Subject. The async
pipeline lives in the constructor where its nature is explicit.
Consecutive submits are handled via switchMap, which cancels any
pending deferred submit when a new one arrives.

Fixes angular#31021
@manichandra manichandra force-pushed the fix/ngsubmit-await-async-validators branch from 44d8dcc to c3b6cfd Compare May 11, 2026 16:23
@google-cla google-cla Bot added cla: yes and removed cla: no labels May 11, 2026
Comment on lines 371 to 378
this.submittedReactive.set(true);
syncPendingControls(this.form, this._directives);
this.ngSubmit.emit($event);
this.form._events.next(new FormSubmittedEvent(this.control));

this._pendingSubmit$.next($event);

// Forms with `method="dialog"` have some special behavior
// that won't reload the page and that shouldn't be prevented.
return ($event?.target as HTMLFormElement | null)?.method === 'dialog';
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.

This isn't much better.
We're plugging an async API in a sync API. It will return before the async validators will have run and this is probably not what users would expect.

I don't think there is a trivial evolution here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Before pushing another update — the approach I'm considering is removing the constructor-based Subject pipeline entirely and instead handling the deferred emit directly inside onSubmit() with an inline statusChanges subscription (only when awaitAsyncValidators=true and the form is PENDING; default path stays fully synchronous). Does that direction address your concern, or did you have a different approach in mind?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

After further looking at it, I see the issue goes deeper than just where the async work lives — changing ngSubmit semantics makes a synchronous submit API behave asynchronously, which is probably not what users would expect.

I'll hold off on pushing more changes. Would the Angular team prefer this handled as documentation/workaround guidance, or would a separate explicit async submission API be worth discussing? @JeanMeche

@manichandra manichandra force-pushed the fix/ngsubmit-await-async-validators branch 2 times, most recently from 14225af to c3b6cfd Compare May 11, 2026 17:00
@google-cla google-cla Bot added cla: no and removed cla: yes labels May 11, 2026
…nt:false

When addControl sets up async validators via updateValueAndValidity({emitEvent:false}),
the validator subscription captures emitEvent:false in its closure. This meant
statusChanges would not emit when the validator resolved, so the ngSubmit
deferral never fired.

Fix: before subscribing to statusChanges in the pending-submit pipeline,
call _updateTreeValidity({emitEvent:true}) to restart validators with
emitEvent:true so statusChanges properly emits when validation completes.

Also add take(1) to test validator observables so subscriptions complete
cleanly after each emission, preventing stale subscriptions on re-validation.
@manichandra manichandra force-pushed the fix/ngsubmit-await-async-validators branch from d40a230 to 84e45d2 Compare May 11, 2026 18:47
@google-cla google-cla Bot added cla: yes and removed cla: no labels May 11, 2026
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.

3 participants