In Angular reactive forms, how do FormControl, FormGroup and FormArray fit together, and how does a template bind to them?
answer
- three kinds of AbstractControl
- one value, named children, indexed children
- the model lives in the class
- formGroup, formControlName, formGroupName
- ReactiveFormsModule in imports
basics
~20 sFormControl 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 sAll 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 linesimport {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
Know the three classes, that the tree is built in the class, and how [formGroup], formGroupName and formControlName bind the template to it.
Explain the shared AbstractControl API, how values and status bubble from children to parents, and when [formControl] replaces formControlName.
Structure large forms deliberately: nested groups that match the domain object, sub-groups passed to child components, and paths that survive refactors.
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