skip to content

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