Angular 29 ๐ ฐ๏ธ Reactive Forms โ The Basics
Angular has two form systems. Template-driven forms live in the template โ directives like ngModel do the work, and the component barely knows a form exists. Reactive forms live in the class โ you build a model out of FormControl, FormGroup, and FormArray objects, and the template just wires them up. Reactive forms are more code up front, but they’re typed, testable, synchronous, and predictable. This chapter covers the basics: FormControl, FormGroup, FormBuilder, the directives that connect them to the template, and the patterns that make forms maintainable.
Key point: A reactive form is a model in the class โ a tree of FormControl and FormGroup objects that hold the values, validation state, and dirty/touched flags. The template binds to the model with [formGroup] and formControlName. The model is the source of truth; the template reflects it. Nothing about the form exists only in the DOM.
What reactive forms are
A reactive form is a form whose model is built in code.
import { Component, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-login',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email">
<input formControlName="password" type="password">
<button type="submit">Log in</button>
</form>
`
})
export class LoginComponent {
form = new FormGroup({
email: new FormControl(''),
password: new FormControl('')
});
onSubmit(): void {
console.log(this.form.value);
// { email: '...', password: '...' }
}
}
The form property is a FormGroup with two controls. The template binds to it with [formGroup] and formControlName. Reading this.form.value gives the current values.
What reactive forms give you:
- A synchronous model โ no waiting for the template to update
- Typed values and validators
- Testable โ construct a
FormGroup, call methods, assert the state - Predictable โ the model drives the template, not vice versa
- Composable โ nested groups, arrays, custom controls
What they don’t give you:
- Less boilerplate (template-driven wins there)
- Automatic two-way binding (you wire it explicitly)
- Magic โ everything is explicit
Why “reactive”: The form reacts to changes in the model. The model holds observables for value and status changes. Every control is a stream of state.
Why the model matters: The class owns the form. It can read values, set them, validate, reset, mark as touched, and subscribe to changes โ all without the DOM. That’s what makes reactive forms testable and predictable.
Why two form systems: Template-driven forms are convenient for simple forms โ one
[(ngModel)]and you’re done. Reactive forms scale โ complex validation, dynamic controls, custom controls, and testing all favor the model-driven approach. Angular ships both because both fit different cases.
FormControl โ a single field
FormControl is the smallest unit โ one input, one value.
import { FormControl } from '@angular/forms';
const email = new FormControl('');
email.value; // ''
email.valueChanges.subscribe(v => console.log(v));
What a control tracks:
| Property | Meaning |
|---|---|
value | Current value |
valueChanges | Observable of value changes |
status | 'VALID', 'INVALID', 'PENDING', 'DISABLED' |
statusChanges | Observable of status changes |
valid / invalid | Boolean shortcuts |
pristine / dirty | Touched by user? |
touched / untouched | Blurred at least once? |
errors | Validation errors or null |
pending | Async validation in progress |
Default value:
const name = new FormControl('Alice');
name.value; // 'Alice'
Typed controls (Angular 14+):
const email = new FormControl<string>('');
const age = new FormControl<number | null>(null);
The type parameter restricts what values are accepted.
With a nonNullable option:
const email = new FormControl('', { nonNullable: true });
// email.value: string, never null
Without it, FormControl can be reset to null. With nonNullable: true, it stays a string.
Reading and writing:
email.setValue('new@example.com'); // sets value
email.value; // 'new@example.com'
email.reset(); // resets to initial value
Watching changes:
email.valueChanges.subscribe(value => {
console.log('email is now', value);
});
Why FormControl matters: It’s the atom. Every form is a tree of controls. Mastering the control’s properties โ value, status, errors, pristine, touched โ is mastering reactive forms.
Why typed controls: Angular 14 added generics to FormControl. FormControl<string> guarantees the value is a string. FormControl<number | null> allows null. Types catch mistakes at compile time.
Why
nonNullable: By default, aFormControlcan be reset tonull. If the field should always have a value of the declared type,nonNullable: truekeeps it. It’s a small option that prevents null errors downstream.
FormGroup โ a group of controls
FormGroup groups related controls into one object.
import { FormGroup, FormControl } from '@angular/forms';
const userForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
age: new FormControl<number | null>(null)
});
userForm.value;
// { name: '', email: '', age: null }
A FormGroup value is an object whose keys match the group’s controls.
Nested groups:
const form = new FormGroup({
user: new FormGroup({
name: new FormControl(''),
email: new FormControl('')
}),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl('')
})
});
form.value;
// {
// user: { name: '', email: '' },
// address: { street: '', city: '' }
// }
Groups nest, and the value is a nested object.
Reading and writing:
form.setValue({
user: { name: 'Alice', email: 'a@b.c' },
address: { street: '1 Main', city: 'Lisbon' }
});
form.patchValue({
user: { name: 'Bob' }
});
setValue requires all values; patchValue accepts a partial.
The group’s status: A group is invalid if any control is invalid.
form.valid; // true if all controls are valid
form.invalid; // true if any is invalid
form.status; // 'VALID' | 'INVALID' | 'PENDING'
Accessing controls:
form.get('user'); // FormGroup
form.get('user.name'); // FormControl
form.controls.user; // FormGroup
form.controls.user.controls.name; // FormControl
Why FormGroup matters: It’s the structure. Forms are trees; FormGroup makes the branches. Its value is an object; its status is the AND of its children’s statuses.
Why nested groups: Real forms have sections โ user info, address, payment. Nested groups mirror that structure, and the values reflect it.
Why
setValuevspatchValue:setValueenforces that you provide every field โ good for full resets.patchValueaccepts partial updates โ good for updates. The distinction prevents accidental missing fields insetValueand allows flexible updates inpatchValue.
ReactiveFormsModule โ the imports
Reactive forms need ReactiveFormsModule in the component’s imports.
import { ReactiveFormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
// ...
})
What ReactiveFormsModule provides:
FormControldirective ([formControl])FormGroupdirective ([formGroup])FormGroupNamedirectiveFormControlNamedirectiveFormArrayNamedirectiveFormBuilderservice- Validators and validation infrastructure
Why the module: Unlike template-driven forms, reactive forms don’t need FormsModule. ReactiveFormsModule is the counterpart โ it brings the directives that connect form models to templates.
Why standalone imports: Each component imports what it uses. If only some components have forms, only those import ReactiveFormsModule.
Common mistake: Importing FormsModule for reactive forms. That’s the wrong module โ it’s for template-driven forms.
Why two modules: Each form system has its own directives and services. Keeping them separate lets you import only what you need.
FormsModuleforngModel;ReactiveFormsModulefor[formGroup]and friends.
Binding to the template
Reactive form directives connect the model to the template.
[formGroup] on <form>:
<form [formGroup]="form" (ngSubmit)="onSubmit()">
...
</form>
formControlName on inputs:
<input formControlName="email" type="email">
formGroupName for nested groups:
<div formGroupName="user">
<input formControlName="name">
<input formControlName="email">
</div>
[formControl] for a single control:
<input [formControl]="emailControl" type="email">
For standalone controls not inside a group.
[formArrayName] for arrays:
<div formArrayName="phones">
<div *ngFor="let phone of phones.controls; let i = index">
<input [formControlName]="i">
</div>
</div>
How the binding works:
[formGroup]provides the group to its childrenformControlName="x"finds the control namedxin the group- Changes in the DOM update the model; changes in the model update the DOM
Why explicit binding: Reactive forms don’t bind with [(ngModel)]. The template binds with [formGroup] and formControlName โ directional, not two-way at the directive level. The directives keep the model and the DOM in sync through the form API.
Why “formControlName” not “formControl”: formControlName takes a name โ the key in the group. [formControl] takes the control object. Different inputs, different directives.
Why the template is thin: In reactive forms, the template just wires up inputs. All the logic โ validation, computed state, submission โ is in the class. The template is declarative markup; the class is the form.
FormBuilder โ the shorthand
FormBuilder reduces boilerplate for building forms.
import { FormBuilder, Validators } from '@angular/forms';
@Component({ /* ... */ })
export class LoginComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
}
Each control is an array: [initialValue, validators, asyncValidators].
The full syntax for a control:
this.fb.control(initialValue, validators, asyncValidators, options)
What fb.group accepts:
| Form | Meaning |
|---|---|
'name': 'Alice' | Value only |
'name': ['Alice'] | Value in array |
'name': ['Alice', validators] | Value + validators |
'name': ['Alice', validators, asyncValidators] | All three |
'name': new FormControl('Alice') | Existing control |
Nested groups:
form = this.fb.group({
user: this.fb.group({
name: [''],
email: ['']
}),
address: this.fb.group({
street: [''],
city: ['']
})
});
Arrays:
form = this.fb.group({
emails: this.fb.array([
this.fb.control('')
])
});
Why FormBuilder: It’s the shortest way to build a form. The array syntax is compact, and FormBuilder handles the details. It’s the idiomatic choice for any form beyond trivial.
Why inject(FormBuilder): Same as any service. FormBuilder is injectable; inject() gets it.
Why field initializer: form = this.fb.group(...) runs during construction โ before ngOnInit. The form exists immediately.
Why
fbnotformBuilder: Convention. The shorthandfbis standard in Angular code. It’s short and recognizable. The variable name doesn’t matter; the pattern does.
Reading form values
The form’s value is available synchronously.
this.form.value;
// { email: 'a@b.c', password: 'secret123' }
Value vs getRawValue:
| Method | Includes disabled controls |
|---|---|
value | โ |
getRawValue() | โ |
Disabled controls are excluded from value by default. getRawValue() includes them.
Type-safe value (with typed forms):
interface LoginForm {
email: string;
password: string;
}
form = this.fb.group({
email: ['', Validators.required],
password: ['', Validators.required]
});
Since Angular 14, the type of form.value is inferred from the controls. form.value.email is string.
Watching the value:
this.form.valueChanges.subscribe(value => {
console.log(value);
});
Watching status:
this.form.statusChanges.subscribe(status => {
console.log(status); // 'VALID' | 'INVALID' | 'PENDING'
});
Why synchronous: form.value is immediate. No need to wait for the DOM. That’s why reactive forms are easy to test โ construct a form, set values, read the value.
Why getRawValue: Disabled controls are excluded from value to reflect what the user can edit. Sometimes you want the raw values including disabled ones โ that’s getRawValue.
Why typed values: The type inference means
form.value.emailisstring, notany. No casts, noasโ just autocomplete and checking.
Setting form values
Two methods โ setValue and patchValue.
setValue โ all values required:
this.form.setValue({
email: 'a@b.c',
password: 'secret123'
});
Every field must be provided. Missing or extra keys throw.
patchValue โ partial update:
this.form.patchValue({
email: 'new@example.com'
});
Only the provided fields update.
When to use which:
| Method | Use |
|---|---|
setValue | Full reset or load |
patchValue | Partial update |
reset โ back to initial:
this.form.reset();
// All controls back to their initial values
reset with values:
this.form.reset({
email: 'reset@example.com'
});
Resets the form to the given values โ or to initial values if omitted.
Why the distinction: setValue catches missing fields โ good for loading. patchValue is flexible โ good for updates. The API is designed so the “wrong” one fails loudly.
Why reset: Reset clears values, marks controls pristine and untouched, and clears the submitted state. It’s the full “start over” operation.
Why not just assign to
.value: The control’s value setter doesn’t update the form’s tracking โ dirty state, validation, etc.setValueandpatchValuerun through the form machinery, updating everything correctly.
A full example
A login form with validation, submission, and status display.
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-login',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label>
Email
<input formControlName="email" type="email">
@if (email.invalid && email.touched) {
<span class="error">
@if (email.errors?.['required']) { Email is required. }
@if (email.errors?.['email']) { Invalid email format. }
</span>
}
</label>
<label>
Password
<input formControlName="password" type="password">
@if (password.invalid && password.touched) {
<span class="error">
@if (password.errors?.['required']) { Password is required. }
@if (password.errors?.['minlength']) {
Password must be at least 8 characters.
}
</span>
}
</label>
<button type="submit" [disabled]="form.invalid">Log in</button>
<p>Status: {{ form.status }}</p>
</form>
`
})
export class LoginComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
get email() {
return this.form.controls.email;
}
get password() {
return this.form.controls.password;
}
onSubmit(): void {
if (this.form.invalid) return;
const { email, password } = this.form.value;
console.log('Logging in', email, password);
}
}
What this shows:
FormBuilderbuilds the form with validationformControlNamewires each inputform.invaliddisables the submit buttonemail.invalid && email.touchedshows errors only after interactionemail.errors?.['required']checks specific errors- Getters expose controls to the template
By the time the user submits, the form is validated and typed.
Why getters:
this.form.controls.emailis verbose in the template. A getterget email()shortens it to
Complete Example Session
# ============================================
# PART 1: BASIC FORMCONTROL
# ============================================
cat > control.ts << 'EOF'
import { FormControl } from '@angular/forms';
const name = new FormControl('Alice');
console.log(name.value); // 'Alice'
name.setValue('Bob');
console.log(name.value); // 'Bob'
name.reset();
console.log(name.value); // 'Alice' (initial)
EOF
npx tsc --noEmit control.ts
# (no errors)
# ============================================
# PART 2: FORMGROUP
# ============================================
cat > group.ts << 'EOF'
import { FormGroup, FormControl } from '@angular/forms';
const form = new FormGroup({
name: new FormControl(''),
email: new FormControl('')
});
console.log(form.value);
form.patchValue({ name: 'Alice' });
console.log(form.value);
EOF
npx tsc --noEmit group.ts
# (no errors)
# ============================================
# PART 3: NESTED GROUPS
# ============================================
cat > nested.ts << 'EOF'
import { FormGroup, FormControl } from '@angular/forms';
const form = new FormGroup({
user: new FormGroup({
name: new FormControl(''),
email: new FormControl('')
}),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl('')
})
});
console.log(form.value);
form.get('user.name')?.setValue('Alice');
console.log(form.value);
EOF
npx tsc --noEmit nested.ts
# (no errors)
# ============================================
# PART 4: FORMBUILDER
# ============================================
cat > builder.ts << 'EOF'
import { FormBuilder, Validators } from '@angular/forms';
const fb = new FormBuilder();
const form = fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
console.log(form.value);
console.log(form.valid);
form.patchValue({
email: 'a@b.c',
password: 'secret123'
});
console.log(form.valid);
EOF
npx tsc --noEmit builder.ts
# (no errors)
# ============================================
# PART 5: READING AND SETTING
# ============================================
cat > read-set.ts << 'EOF'
import { FormBuilder } from '@angular/forms';
const fb = new FormBuilder();
const form = fb.group({
name: [''],
email: ['']
});
console.log(form.value); // both empty
form.setValue({ name: 'Alice', email: 'a@b.c' });
console.log(form.value);
form.patchValue({ name: 'Bob' });
console.log(form.value);
EOF
npx tsc --noEmit read-set.ts
# (no errors)
# ============================================
# PART 6: TYPED CONTROLS
# ============================================
cat > typed.ts << 'EOF'
import { FormControl } from '@angular/forms';
const email = new FormControl<string>('', { nonNullable: true });
email.setValue('a@b.c');
console.log(email.value.toUpperCase());
const age = new FormControl<number | null>(null);
age.setValue(30);
console.log(age.value);
EOF
npx tsc --noEmit typed.ts
# (no errors)
# ============================================
# PART 7: WATCHING CHANGES
# ============================================
cat > changes.ts << 'EOF'
import { FormControl } from '@angular/forms';
const email = new FormControl('');
email.valueChanges.subscribe(v => console.log('value', v));
email.statusChanges.subscribe(s => console.log('status', s));
email.setValue('a@b.c');
console.log('dirty:', email.dirty);
console.log('touched:', email.touched);
EOF
npx tsc --noEmit changes.ts
# (no errors)
# ============================================
# PART 8: RUN AND OBSERVE
# ============================================
npx tsc control.ts group.ts nested.ts builder.ts read-set.ts typed.ts changes.ts
node control.js
# [ Alice ]
# [ Bob ]
# [ Alice ]
node group.js
# [ { name: '', email: '' } ]
# [ { name: 'Alice', email: '' } ]
node nested.js
# [ { user: { name: '', email: '' }, address: { street: '', city: '' } } ]
# [ { user: { name: 'Alice', email: '' }, address: { street: '', city: '' } } ]
node builder.js
# [ { email: '', password: '' } ]
# [ false ]
# [ true ]
node read-set.js
# [ { name: '', email: '' } ]
# [ { name: 'Alice', email: 'a@b.c' } ]
# [ { name: 'Bob', email: 'a@b.c' } ]
node typed.js
# [ A@B.C ]
# [ 30 ]
node changes.js
# [ value ]
# [ status VALID ]
# [ value a@b.c ]
# [ status VALID ]
# [ dirty: true ]
# [ touched: false ]
Quick Reference
The Three Building Blocks
| Class | Purpose |
|---|---|
FormControl | Single value |
FormGroup | Object of controls |
FormArray | Array of controls |
Import
| Need | Import |
|---|---|
| Reactive directives | ReactiveFormsModule |
| Classes | @angular/forms |
| Validators | @angular/forms |
Template Directives
| Directive | Applied to |
|---|---|
[formGroup] | <form> or <div> |
formControlName="x" | <input> |
[formControl]="ctrl" | <input> |
formGroupName="x" | Nested group |
formArrayName="x" | Array |
FormControl Properties
| Property | Meaning |
|---|---|
value | Current value |
valueChanges | Observable |
status | VALID / INVALID / PENDING / DISABLED |
statusChanges | Observable |
valid / invalid | Booleans |
pristine / dirty | User-modified? |
touched / untouched | Blurred? |
errors | Error object or null |
pending | Async validation in progress |
FormGroup Methods
| Method | Purpose |
|---|---|
value | Group value |
getRawValue() | Include disabled |
setValue(obj) | All values |
patchValue(obj) | Partial update |
reset(value?) | Reset form |
get(path) | Access a control |
addControl / removeControl | Dynamic controls |
FormBuilder Syntax
| Form | Meaning |
|---|---|
fb.control(v) | Single control |
fb.group({...}) | Group |
fb.array([...]) | Array |
[v] | Value |
[v, validators] | Value + sync validators |
[v, validators, asyncValidators] | All |
Value vs Raw Value
| Method | Disabled controls |
|---|---|
value | Excluded |
getRawValue() | Included |
Setting Values
| Method | Requirement |
|---|---|
setValue | All keys required |
patchValue | Partial allowed |
Typed Controls
| Form | Type |
|---|---|
FormControl<string> | string | null |
new FormControl('', { nonNullable: true }) | string |
FormControl<number | null> | number | null |
Common Validators
| Validator | Meaning |
|---|---|
Validators.required | Non-empty |
Validators.email | Email format |
Validators.minLength(n) | Min chars |
Validators.maxLength(n) | Max chars |
Validators.pattern(re) | Regex match |
Validators.min(n) | Min number |
Validators.max(n) | Max number |
Reading Form State
| Expression | Meaning |
|---|---|
form.valid | All controls valid |
form.value | All values |
form.status | VALID/INVALID/PENDING |
form.dirty | User changed something |
form.touched | At least one blur |
control.errors?.['key'] | Specific error |
Error Cases
| Error | Cause |
|---|---|
Cannot find control | formControlName mismatch |
Must supply a value for form control | setValue missing key |
formGroup expects a FormGroup | Wrong binding |
No value accessor | Missing module |
Signal Forms (Newer)
Angular is introducing a signal-based form system. The class form remains for now; signal forms will coexist. This chapter covers the class-based reactive forms.
Testing
const form = new FormGroup({
email: new FormControl('', Validators.required)
});
form.setValue({ email: '' });
expect(form.valid).toBe(false);
form.setValue({ email: 'a@b.c' });
expect(form.valid).toBe(true);
No DOM, no TestBed. Construct the form and assert.
Best Practices
โ Do This:
// Use FormBuilder for real forms
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]]
}); // โ
// Use typed controls
const email = new FormControl<string>('', { nonNullable: true }); // โ
// Use getters for template access
get email() { return this.form.controls.email; } // โ
// Disable submit when invalid
<button [disabled]="form.invalid">Submit</button> // โ
// Show errors after interaction
@if (email.invalid && email.touched) { } // โ
// Use patchValue for partial updates
form.patchValue({ name: 'Alice' }); // โ
// Use setValue for full load
form.setValue({ name: 'A', email: 'b' }); // โ
// Subscribe to valueChanges for reactive behavior
form.valueChanges.subscribe(v => this.onChange(v)); // โ
// Test forms without TestBed
const form = new FormGroup({}); expect(form.valid).toBe(true); // โ
// Import ReactiveFormsModule in the component
imports: [ReactiveFormsModule] // โ
โ Don’t Do This:
// Don't use ngModel with reactive forms
<input [(ngModel)]="name" formControlName="name"> // โ
// Don't import FormsModule for reactive forms
imports: [FormsModule] // โ ๏ธ
// Don't forget formControlName
<input type="email"> // โ ๏ธ not connected to the form // โ ๏ธ
// Don't assign to control.value directly
email.value = 'x'; // โ ๏ธ use setValue // โ ๏ธ
// Don't use patchValue for full loads
form.patchValue(fullData); // โ ๏ธ may miss fields // โ ๏ธ
// Don't forget the module
@Component({ imports: [] }) // โ ๏ธ formControlName not found // โ ๏ธ
// Don't read .value to check user input
// Subscribe to valueChanges or read after change detection // โ ๏ธ
// Don't skip validation
new FormControl('') // โ ๏ธ no validators // โ ๏ธ
// Don't put form logic in the template
@if (form.controls.email.errors?.['required'] && form.dirty) { } // โ ๏ธ
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing ReactiveFormsModule | Directives not found | Import in component |
formControlName mismatch | Control not found | Match the name |
setValue missing key | Compile error | Use patchValue |
| Wrong module | FormsModule imported | Use ReactiveFormsModule |
| Value assignment | Doesn’t update state | Use setValue |
| Forgot getter | Verbose template | Add getters |
| Errors shown immediately | Bad UX | Guard with touched |
| Untyped control | any values | Use generics |
| Disabled excluded | Missing values | Use getRawValue() |
| Not testing | Silent bugs | Unit test forms |
Real-World Examples
1. Single control
const email = new FormControl('');
2. Typed control
const email = new FormControl<string>('', { nonNullable: true });
3. Basic group
const form = new FormGroup({
name: new FormControl(''),
email: new FormControl('')
});
4. Nested group
const form = new FormGroup({
user: new FormGroup({
name: new FormControl('')
})
});
5. FormBuilder group
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
6. With validators
password: ['', [Validators.required, Validators.minLength(8)]]
7. Template binding
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email">
</form>
8. Nested template
<div formGroupName="user">
<input formControlName="name">
</div>
9. Read value
this.form.value.email;
10. Patch value
this.form.patchValue({ email: 'a@b.c' });
11. Set value
this.form.setValue({ email: 'a@b.c', password: 'secret' });
12. Reset
this.form.reset();
13. Disable submit
<button [disabled]="form.invalid">Submit</button>
14. Show errors conditionally
@if (email.invalid && email.touched) {
<span>Error</span>
}
15. Subscribe to changes
this.form.valueChanges.subscribe(v => console.log(v));
16. Getter for control
get email() { return this.form.controls.email; }
17. Validate on submit
onSubmit(): void {
if (this.form.invalid) return;
// submit
}
18. Access specific control
this.form.get('user.email');
19. Typed value
const { email, password } = this.form.value;
// email: string, password: string
20. Test a form
const form = new FormGroup({ email: new FormControl('') });
form.setValue({ email: 'a@b.c' });
expect(form.valid).toBe(true);
Visual: Reactive Form Model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FormGroup (form) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ FormControl: email โ โ
โ โ FormControl: password โ โ
โ โ FormGroup: address โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ FormControl: street โ โ โ
โ โ โ FormControl: city โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Template โ Model Binding
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Template โ
โ โ
โ <form [formGroup]="form"> โ
โ <input formControlName="email"> โ
โ </form> โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ directives connect
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Model โ
โ โ
โ form = fb.group({ โโ email: [''] โ
โ }); โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DOM change โ model update โ
โ Model change โ DOM update โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Value vs Status
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ form.value โ
โ { โ
โ email: 'a@b.c', โ
โ password: 'secret' โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ form.status โ
โ 'VALID' โ
โ โ
โ form.valid โ true โ
โ form.invalid โ false โ
โ form.dirty โ true โ
โ form.touched โ true โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Control State
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Initial: โ
โ value: '' โ
โ pristine: true โ
โ untouched: true โ
โ valid: true โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ user types
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ value: 'x' โ
โ pristine: false โ dirty โ
โ untouched: true โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ user blurs
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ value: 'x' โ
โ dirty: true โ
โ touched: true โ blurred โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: setValue vs patchValue
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Form: โ
โ { name: '', email: '' } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ setValue({ name: 'Alice', email: 'a@b.c' }) โ
โ โ
all fields provided โ
โ โ
โ setValue({ name: 'Alice' }) โ
โ โ missing email โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ patchValue({ name: 'Alice' }) โ
โ โ
partial update โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: FormBuilder Array Syntax
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ fb.group({ โ
โ email: ['', [Validators.required]], โ
โ // โ โ โ
โ // value sync validators โ
โ // โ
โ password: ['', Validators.required] โ
โ // โ โ โ
โ // initial validators โ
โ }) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Full form: โ
โ โ
โ fb.control( โ
โ initialValue, โ
โ syncValidators, โ
โ asyncValidators, โ
โ options โ
โ ) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Typed Controls
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Without type parameter: โ
โ โ
โ const email = new FormControl(''); โ
โ // value: string | null โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ With type parameter: โ
โ โ
โ const email = new FormControl<string>(''); โ
โ // value: string | null โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ With nonNullable: โ
โ โ
โ const email = new FormControl('', { โ
โ nonNullable: true โ
โ }); โ
โ // value: string โ never null โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Form State Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Created โ
โ pristine, untouched โ
โ โ โ
โ โผ โ
โ User edits โ
โ dirty, untouched โ
โ โ โ
โ โผ โ
โ User blurs โ
โ dirty, touched โ
โ โ โ
โ โผ โ
โ Submit or reset โ
โ back to initial โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Decision Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Building a form? โ
โ โ โ
โ โโโ Simple, few fields โโโบ template-drivenโ
โ โ โ
โ โโโ Complex, validation, dynamic โโโบ reactiveโ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Building a form model? โ
โ โ โ
โ โโโ One field โโโบ FormControl โ
โ โ โ
โ โโโ Related fields โโโบ FormGroup โ
โ โ โ
โ โโโ Repeatable fields โโโบ FormArray โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Boilerplate-heavy? โ
โ โ โ
โ โโโ Use FormBuilder โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Loading data? โ
โ โ โ
โ โโโ Full object โโโบ setValue โ
โ โ โ
โ โโโ Partial update โโโบ patchValue โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Reactive forms | Form model in the class |
FormControl | Single field |
FormGroup | Group of controls |
FormArray | Array of controls |
FormBuilder | Shorthand builder |
ReactiveFormsModule | Directives |
[formGroup] | Bind group to template |
formControlName | Bind control to input |
setValue | Set all values |
patchValue | Set partial values |
reset | Back to initial |
| Validators | Sync and async rules |
Key takeaways:
- Reactive forms are built in the class โ
FormControl,FormGroup,FormArray FormControlis one field with value, status, validators, and change streamsFormGroupis a group of controls whose value is an object- Groups nest โ the value is a nested object
FormBuilderis the shorthand โ array syntax for value + validatorsReactiveFormsModuleprovides the directives โ import it in the component[formGroup]binds a form to a group;formControlNamebinds an input to a controlform.valueis synchronous and, since Angular 14, typedsetValuerequires all fields;patchValueaccepts partialresetreturns the form to initial values- Validators are functions โ
required,email,minLength, and more - Form state โ valid, dirty, touched, pristine โ drives the template
- Errors are shown conditionally, usually after
touched - Test by constructing the form directly โ no TestBed needed for the logic
- Reactive forms scale to complex cases โ dynamic controls, custom controls, cross-field validation
Remember: A reactive form is a model โ a tree of controls that holds values, validation state, and change streams. The template binds to the model with [formGroup] and formControlName. Everything happens in the class. That’s why reactive forms are typed, testable, and predictable. Use FormBuilder to reduce boilerplate, getters to shorten the template, and patchValue for updates. Show errors after touched. Disable the submit button when invalid. The rest โ validation, submission, dynamic controls โ builds on this foundation.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!