skip to content

In an Angular template-driven form, why are the form's controls still empty in ngAfterViewInit, so that form.setValue() throws, although the inputs are already rendered?

level: seniorimportance: should knowfreq 36%

answer

  1. registration is deferred
  2. a resolved promise
  3. after the change-detection pass
  4. the ExpressionChanged reason
  5. bind the model instead

basics

~20 s

Template-driven forms register each ngModel with NgForm, and apply model values, in a microtask after the current change-detection pass. During ngAfterViewInit the group is still empty, so setValue() throws; bind values through ngModel or wait for the form to settle.

solid answer

~40 s

`NgForm.addControl()` and `NgModel`'s value updates both run inside `Promise.resolve().then(...)`, so they happen in a microtask after the change-detection pass that created the directives. That pass includes the parent's `ngAfterViewInit`, so at that point `form.controls` is `{}`, `form.value` is `{}`, and `form.setValue({...})` throws because no controls are registered yet. Angular does this deliberately: registering or changing control state during the same pass would alter bindings that were already checked, which is what `ExpressionChangedAfterItHasBeenChecked` reports in development. The fixes are to set values through the bound model properties, which is the template-driven way, to act after the form settles (for example in the submit handler or after awaiting a microtask), or to switch to a reactive form when code must drive values at creation time.

go deeper

for a junior

Remember that a template-driven form's controls are created by the template and are not ready in ngOnInit or ngAfterViewInit.

for a middle

Explain the timeline: directives schedule registration and value writes in a microtask, the pass ends, then the form fills in and the view is re-checked.

for a senior

Diagnose the resulting bugs and tests quickly, prefer model-bound values over imperative setValue, and move code-driven forms to a reactive model.

for a principal

Use this timing constraint as an input to the forms strategy: if features need programmatic control at creation time, template-driven is the wrong base.

## What "asynchronous model creation" means In a template-driven form nothing exists until the template renders. During the first change-detection pass, Angular creates the `NgForm` directive for the `<form>` and an `NgModel` directive for every input. But two important steps are **deferred to a microtask**: 1. **Registration.** `NgForm.addControl(dir)` wraps its work in a resolved promise: finding the right container, calling `registerControl(name, control)`, wiring the value accessor and computing validity all happen in `.then()`. 2. **Model-to-view writes.** When `[ngModel]` receives a value, `NgModel` applies it to its `FormControl` in the same kind of microtask, then marks the view for check. So the timeline of the first render is: - change detection creates the directives and runs `ngOnChanges`, which **schedules** registration; - the component's `ngAfterViewInit` runs, still inside the same pass; - the pass ends, the microtask queue drains, controls are **registered** and values applied; - the view is marked for check, and the next pass shows the model values. ## Symptoms you will see | Where you look | What you get | |---|---| | `f.controls` in `ngOnInit` or `ngAfterViewInit` | `{}` | | `f.value` in the same hooks | `{}` | | `f.setValue({...})` or `f.form.get('email')!.setValue(...)` | throws: no controls registered yet, or `get()` returns `null` | | `#email="ngModel"` value read in `ngAfterViewInit` | not yet the bound model value | | a unit test right after the first `detectChanges()` | the input is still empty | ## Why Angular defers the work A `FormGroup`'s validity and value are aggregated from its children. If `NgModel` registered synchronously while the template was being checked, the form's `valid` and `value` would change **after** expressions such as `[disabled]="f.invalid"` earlier in the same template had been evaluated. In development Angular re-checks and throws `ExpressionChangedAfterItHasBeenChecked` for exactly that. Deferring registration and value writes to a microtask, and marking the view for check afterwards, lets the change land in the **next** pass instead. The source comment on `NgModel` describes this extra run as deliberate. ## How to work with it - **Set data through the model.** Assign the component properties bound with `[(ngModel)]`; the form picks them up. This is the template-driven way and needs no timing tricks. - **Read the form when it has settled.** In an `(ngSubmit)` handler, or in response to a user event, registration finished long ago. - **If code must act on the form right after it renders**, wait for the microtask: `await Promise.resolve()` or subscribe to the form group's value changes. Treat this as a smell rather than a pattern. - **In tests**, call `fixture.detectChanges()`, then `await fixture.whenStable()`, then `detectChanges()` again before asserting input values. Angular's forms overview notes that template-driven tests depend on change detection in this way. - **Switch to a reactive form** when the component needs to populate, patch or observe controls in code at creation time; its model exists synchronously in the class. ## A diagnosis walk-through 1. The component reads saved data in `ngAfterViewInit` and calls `this.form().setValue(saved)`. 2. The development build throws: "There are no form controls registered with this group yet", and for `ngModel` the message suggests checking on the next tick, for example with `setTimeout`. 3. That `setTimeout(() => ..., 0)` does work, because by then the microtasks have run. It is a stopgap rather than a design: it breaks again when fields render later, for example inside an `@if` that becomes true afterwards or a `@defer` block. 4. The durable fix is to assign `saved.email` and friends to the properties bound with `[(ngModel)]`, or to move the form to a reactive model if it must be driven from code. ## Zoneless and OnPush Since v21 new apps are zoneless by default and since v22 components are `OnPush` by default. The deferred updates still reach the screen, because `NgModel` calls `markForCheck()` after applying a value, which schedules change detection without zone.js. What changes is only how you wait in tests: prefer `await fixture.whenStable()` over `fakeAsync` helpers that assume zone.js.

  • Why doesn't this problem exist for a reactive form's setValue() in ngOnInit?
    In a reactive form the `FormGroup` and its controls are created in the class before the template renders, so they exist synchronously. `setValue()` works immediately and the template simply binds to the existing model. Only the directive-built model of template-driven forms has to wait for registration.
  • In a unit test, the input bound with [(ngModel)]="email" is still empty after fixture.detectChanges(). What do you add?
    Wait for the microtasks that register the control and write the model value: `await fixture.whenStable()`, then call `fixture.detectChanges()` again so the view reflects the applied value, and only then read `input.value`. The same wait is needed after changing the component property in a test.

It is like handing sign-up cards to a clerk who files them only after the current queue is served. The cards are in the building as soon as you hand them over, but asking the filing cabinet for them straight away returns nothing until the clerk's next round.

saying these in an interview costs you the question

  • Template-driven controls exist as soon as the constructor runs
  • NgModel writes its bound value to the control synchronously
  • The deferral is a bug that zoneless mode removes
  • ngAfterViewInit runs after all form controls are registered
  • Reactive forms register controls asynchronously too