skip to content

Reactive Control Tree

Reactive forms declare FormControl, FormGroup and FormArray in the class and bind them with formControlName. Interviewers probe setValue vs patchValue, disabled controls and the change streams.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Angular reactive forms, how do FormControl, FormGroup and FormArray fit together, and how does a template bind to them?

level: juniorimportance: must knowfreq 80%

answer

  1. three kinds of AbstractControl
  2. one value, named children, indexed children
  3. the model lives in the class
  4. formGroup, formControlName, formGroupName
  5. ReactiveFormsModule in imports

basics

~20 s

FormControl holds one value, FormGroup holds named child controls, and FormArray holds an indexed list; all extend AbstractControl and nest into a tree built in the class. The template binds it with [formGroup], formControlName, formGroupName and formArrayName from ReactiveFormsModule.

solid answer

~40 s

All three classes extend `AbstractControl`, so each has a `value`, a `status`, validators, `touched`/`dirty` flags and change streams. `FormControl` wraps a single field, `FormGroup` aggregates named children into an object value, and `FormArray` aggregates an ordered list into an array value. You build the tree in the component class, for example a shipping form with a `recipient` control and a nested `address` group, and the tree is the source of truth. In the template, after importing `ReactiveFormsModule`, `[formGroup]="form"` binds the root, `formGroupName="address"` steps into a nested group, `formControlName="city"` binds an input to a child by name, and `formArrayName` steps into an array. A lone control can bind directly with `[formControl]`. Values, validity and state bubble up from children to parents.

code

ts · 32 lines
ts
import {Component} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';

@Component({
  selector: 'app-shipping-address',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="save()">
      <input formControlName="recipient" />
      <div formGroupName="address">
        <input formControlName="street" />
        <input formControlName="city" />
        <input formControlName="postcode" />
      </div>
      <button type="submit" [disabled]="form.invalid">Ship here</button>
    </form>
  `,
})
export class ShippingAddress {
  readonly form = new FormGroup({
    recipient: new FormControl('', {nonNullable: true, validators: Validators.required}),
    address: new FormGroup({
      street: new FormControl('', {nonNullable: true}),
      city: new FormControl('', {nonNullable: true, validators: Validators.required}),
      postcode: new FormControl('', {nonNullable: true}),
    }),
  });

  save(): void {
    console.log(this.form.value); // {recipient, address: {street, city, postcode}}
  }
}

go deeper

for a junior

Know the three classes, that the tree is built in the class, and how [formGroup], formGroupName and formControlName bind the template to it.

for a middle

Explain the shared AbstractControl API, how values and status bubble from children to parents, and when [formControl] replaces formControlName.

for a senior

Structure large forms deliberately: nested groups that match the domain object, sub-groups passed to child components, and paths that survive refactors.

for a principal

Decide where form models live and how they map to API contracts, so that several teams build forms the same way.

## One base class, three shapes Angular's reactive forms model every piece of a form as an **`AbstractControl`**. Three concrete classes give it shape: | Class | Holds | `value` shape | Typical use | |---|---|---|---| | `FormControl` | one value | the value itself | a text input, a checkbox, a select | | `FormGroup` | named children | an object keyed by child name | a whole form, or a section such as an address | | `FormArray` | ordered children | an array | a list of phone numbers or order lines | Because they share the base class, every node offers the same toolkit: - **State**: `value`, `status` (`VALID`, `INVALID`, `PENDING`, `DISABLED`), `errors`, `touched`/`dirty`. - **Writes**: `setValue()`, `patchValue()`, `reset()`, `enable()`/`disable()`. - **Navigation**: `get('address.city')` finds a descendant by path. - **Streams**: `valueChanges`, `statusChanges` and the unified `events` observable. Changes **bubble up**: when a control's value changes, its parent group recomputes its own value and status, and so on to the root. ## Building the tree in the class The tree is created in TypeScript, which is the defining trait of reactive forms: the model exists before the template renders, so code can read, write and observe it immediately. A shipping form might be a root `FormGroup` with a `recipient` control and a nested `address` group holding `street`, `city` and `postcode`. `FormGroup` values are objects mirroring that nesting, so `form.value` looks like `{recipient: '...', address: {street: '...', city: '...', postcode: '...'}}`. ## Binding the template Import **`ReactiveFormsModule`** in the standalone component, then connect elements to the tree with directives: 1. **`[formGroup]="form"`** on the `<form>` binds the root group and provides it to descendants. 2. **`formGroupName="address"`** on a wrapper steps into a nested group by name. 3. **`formControlName="city"`** on an input binds it to the child control with that name in the nearest group. 4. **`formArrayName="phones"`** steps into a `FormArray`; inside it, children are addressed by index. 5. **`[formControl]="searchBox"`** binds a standalone `FormControl` that is not part of any group, such as a search field. The directives use `ControlValueAccessor` implementations to move values between the DOM and the controls. View-to-model and model-to-view updates are **synchronous**, unlike template-driven forms. ## Common binding mistakes - **Forgetting `ReactiveFormsModule`**: the compiler rejects `[formGroup]` as an unknown property. - **A `formControlName` outside any `[formGroup]`**: Angular throws, because the directive needs a parent container to look the name up in. - **A name that is not in the group**: Angular throws "Cannot find control with name", which usually means a typo or a nesting mismatch. - **Using `ngModel` inside `[formGroup]`**: Angular throws and points you to `formControlName`, because the two styles cannot share one form. ## Splitting a large form across components Because the model is an object, a parent can hand a **sub-group** to a child component as an input, and the child binds it with its own `[formGroup]`: - the parent keeps one tree, so `form.value` and `form.valid` still cover everything; - the child needs `ReactiveFormsModule` and an input typed as the sub-group, for example the address group; - no dependency-injection bridging is needed, unlike template-driven forms, because nothing is looked up across the component boundary. This is the usual way to reuse an address block in both the billing and the shipping section of a checkout. ## Why teams choose this model - The model is plain TypeScript, so it can be **unit-tested without a template**. - Values are **typed**: `new FormControl('')` is inferred from its initial value. - Code can **react** to changes, **add or remove** controls at runtime and **compose** validators. - Large forms can be split across components by passing sub-groups down as inputs. For small, fixed forms that extra setup can be overkill, which is where template-driven forms still fit.

  • When would you bind with [formControl] instead of formControlName?
    `[formControl]` takes a `FormControl` instance directly, so it suits a control that belongs to no group, such as a search box or a filter field. `formControlName` takes a string and looks the control up in the nearest parent group, which is the normal choice inside a `[formGroup]` form.
  • How do you read the city control from the class without walking the tree by hand?
    Use `form.get('address.city')` with a dot-separated path, or the typed property path `form.controls.address.controls.city`. The second form keeps full type information, while `get()` returns a possibly-null `AbstractControl` that you narrow before use.

saying these in an interview costs you the question

  • FormGroup and FormArray are unrelated to FormControl
  • Reactive forms build their controls from the template markup
  • formControlName works without a parent formGroup
  • Reactive forms update the model asynchronously like ngModel
  • FormsModule is enough to use formGroup in a standalone component
open as a page

In Angular reactive forms, what is the difference between setValue() and patchValue() on a FormGroup, and when does each one throw?

level: middleimportance: must knowfreq 78%

basics

~10 s

setValue() requires a value for every child control and rejects unknown keys, throwing otherwise; patchValue() updates only the keys it receives and ignores the rest. On a single FormControl both behave the same.

open as a page

In Angular reactive forms, why does form.value leave out a disabled control, and how should you disable a control bound with formControlName?

level: middleimportance: should knowfreq 55%

basics

~20 s

A disabled control is excluded from its parent's value and validity, so form.value omits it while getRawValue() includes it. Disable it through the model with control.disable() or {value, disabled: true}; a [disabled] binding on formControlName only logs a warning.

open as a page

In Angular reactive forms, what does FormBuilder add over calling new FormGroup() directly, and when would you inject NonNullableFormBuilder instead?

level: middleimportance: should knowfreq 50%

basics

~20 s

FormBuilder is an automatically provided service that turns plain values and arrays into FormControl, FormGroup, FormRecord and FormArray instances; it adds no new behaviour. NonNullableFormBuilder builds every implicit control with nonNullable: true, so reset() restores initial values instead of null.

open as a page

An Angular reactive form copies billing into shipping on billing valueChanges, a shipping listener writes back, and the console fills with 'Maximum call stack size exceeded'. Why, and how do emitEvent: false and the events stream help?

level: seniorimportance: should knowfreq 45%

basics

~20 s

setValue() and patchValue() emit valueChanges synchronously, so two listeners that write into each other's controls recurse until the call stack overflows. Pass {emitEvent: false} for programmatic writes, or use the events stream's source to ignore changes you caused.

open as a page