Skip to content

fix(forms): update status classes once deferred registration resolves the control#69311

Open
gregkaczan wants to merge 1 commit into
angular:mainfrom
gregkaczan:fix-forms-status-classes-deferred-registration
Open

fix(forms): update status classes once deferred registration resolves the control#69311
gregkaczan wants to merge 1 commit into
angular:mainfrom
gregkaczan:fix-forms-status-classes-deferred-registration

Conversation

@gregkaczan

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

What is the current behavior?

NgForm registers ngModel/ngModelGroup controls in a deferred microtask (resolvedPromise.then(...) in addControl/addFormGroup) without notifying any view. When the NgControlStatus/NgControlStatusGroup host bindings are first checked before that registration lands, control is still null, the optional-chained signal reads in AbstractControlStatus short-circuit, and the view never establishes a reactive dependency.

In zoneless OnPush apps, an [ngModelGroup] host with no rendered controls (the collapsed accordion/section pattern) therefore never receives its ng-* status classes, and later model changes (markAllAsTouched() etc.) are never reflected — permanently stale DOM. The same stale binding throws NG0100 on every tick under provideCheckNoChangesConfig({exhaustive: true}).

Issue Number: #69310

What is the new behavior?

AbstractControlDirective tracks an internal _controlResolved signal. The status host-binding getters read it, so the first binding pass always creates a reactive dependency even while control is null; NgForm's deferred addControl/addFormGroup callbacks set it once the control is registered, which wakes the host view. This also covers addControl swapping the NgModel control instance for a previously registered one.

Status classes on group hosts now appear once the deferred registration lands and keep tracking the control's _touched/_pristine/_status signals from the second pass onward — with no behavior change for views that already worked (the signal flips once, causing at most one extra refresh per directive).

Verified against the issue's reproduction: the repro spec fails on stock 21.2.16 and passes with this change ported onto it. Live demo of the patched behavior: https://stackblitz.com/github/gregkaczan/ng69310-fix-demo (broken-state counterpart linked from #69310).

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Side observation from the issue — isUntouched/isDirty/isInvalid/isPending not reading the corresponding signals — is left as-is: they are evaluated in the same host-binding pass as their signal-reading positive counterparts, which the existing comments rely on deliberately.

@pullapprove pullapprove Bot requested a review from crisbeto June 11, 2026 12:12
@ngbot ngbot Bot added this to the Backlog milestone Jun 11, 2026
@google-cla

google-cla Bot commented Jun 11, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

… the control

`NgForm` registers `ngModel`/`ngModelGroup` controls in a deferred
microtask without notifying any view. When the `NgControlStatus`/
`NgControlStatusGroup` host bindings are first checked before that
registration lands, `control` is still `null`, the optional-chained
signal reads short-circuit, and the view never establishes a reactive
dependency. In zoneless OnPush apps an `ngModelGroup` host with no
rendered controls (e.g. a collapsed form section) therefore never
receives its `ng-*` status classes and never reflects later model
changes such as `markAllAsTouched()`. The same stale binding throws
NG0100 under `provideCheckNoChangesConfig({exhaustive: true})`.

Track an internal `_controlResolved` signal on
`AbstractControlDirective`: the status host bindings read it so the
first binding pass always creates a dependency, and `NgForm`'s deferred
`addControl`/`addFormGroup` callbacks set it once the control exists.
This also covers `addControl` swapping the `NgModel` control instance
for a previously registered one.

Fixes angular#69310
@gregkaczan gregkaczan force-pushed the fix-forms-status-classes-deferred-registration branch from ca74800 to 0f8a61b Compare June 11, 2026 12:48
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.

1 participant