| |

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:

PropertyMeaning
valueCurrent value
valueChangesObservable of value changes
status'VALID', 'INVALID', 'PENDING', 'DISABLED'
statusChangesObservable of status changes
valid / invalidBoolean shortcuts
pristine / dirtyTouched by user?
touched / untouchedBlurred at least once?
errorsValidation errors or null
pendingAsync 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, a FormControl can be reset to null. If the field should always have a value of the declared type, nonNullable: true keeps 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 setValue vs patchValue: setValue enforces that you provide every field โ€” good for full resets. patchValue accepts partial updates โ€” good for updates. The distinction prevents accidental missing fields in setValue and allows flexible updates in patchValue.


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:

  • FormControl directive ([formControl])
  • FormGroup directive ([formGroup])
  • FormGroupName directive
  • FormControlName directive
  • FormArrayName directive
  • FormBuilder service
  • 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. FormsModule for ngModel; ReactiveFormsModule for [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 children
  • formControlName="x" finds the control named x in 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:

FormMeaning
'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 fb not formBuilder: Convention. The shorthand fb is 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:

MethodIncludes 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.email is string, not any. No casts, no as โ€” 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:

MethodUse
setValueFull reset or load
patchValuePartial 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. setValue and patchValue run 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:

  • FormBuilder builds the form with validation
  • formControlName wires each input
  • form.invalid disables the submit button
  • email.invalid && email.touched shows errors only after interaction
  • email.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.email is verbose in the template. A getter get email() shortens it to email. The template reads cleanly.


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

ClassPurpose
FormControlSingle value
FormGroupObject of controls
FormArrayArray of controls

Import

NeedImport
Reactive directivesReactiveFormsModule
Classes@angular/forms
Validators@angular/forms

Template Directives

DirectiveApplied to
[formGroup]<form> or <div>
formControlName="x"<input>
[formControl]="ctrl"<input>
formGroupName="x"Nested group
formArrayName="x"Array

FormControl Properties

PropertyMeaning
valueCurrent value
valueChangesObservable
statusVALID / INVALID / PENDING / DISABLED
statusChangesObservable
valid / invalidBooleans
pristine / dirtyUser-modified?
touched / untouchedBlurred?
errorsError object or null
pendingAsync validation in progress

FormGroup Methods

MethodPurpose
valueGroup value
getRawValue()Include disabled
setValue(obj)All values
patchValue(obj)Partial update
reset(value?)Reset form
get(path)Access a control
addControl / removeControlDynamic controls

FormBuilder Syntax

FormMeaning
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

MethodDisabled controls
valueExcluded
getRawValue()Included

Setting Values

MethodRequirement
setValueAll keys required
patchValuePartial allowed

Typed Controls

FormType
FormControl<string>string | null
new FormControl('', { nonNullable: true })string
FormControl<number | null>number | null

Common Validators

ValidatorMeaning
Validators.requiredNon-empty
Validators.emailEmail 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

ExpressionMeaning
form.validAll controls valid
form.valueAll values
form.statusVALID/INVALID/PENDING
form.dirtyUser changed something
form.touchedAt least one blur
control.errors?.['key']Specific error

Error Cases

ErrorCause
Cannot find controlformControlName mismatch
Must supply a value for form controlsetValue missing key
formGroup expects a FormGroupWrong binding
No value accessorMissing 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

PitfallProblemSolution
Missing ReactiveFormsModuleDirectives not foundImport in component
formControlName mismatchControl not foundMatch the name
setValue missing keyCompile errorUse patchValue
Wrong moduleFormsModule importedUse ReactiveFormsModule
Value assignmentDoesn’t update stateUse setValue
Forgot getterVerbose templateAdd getters
Errors shown immediatelyBad UXGuard with touched
Untyped controlany valuesUse generics
Disabled excludedMissing valuesUse getRawValue()
Not testingSilent bugsUnit 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

ConceptMeaning
Reactive formsForm model in the class
FormControlSingle field
FormGroupGroup of controls
FormArrayArray of controls
FormBuilderShorthand builder
ReactiveFormsModuleDirectives
[formGroup]Bind group to template
formControlNameBind control to input
setValueSet all values
patchValueSet partial values
resetBack to initial
ValidatorsSync and async rules

Key takeaways:

  • Reactive forms are built in the class โ€” FormControl, FormGroup, FormArray
  • FormControl is one field with value, status, validators, and change streams
  • FormGroup is a group of controls whose value is an object
  • Groups nest โ€” the value is a nested object
  • FormBuilder is the shorthand โ€” array syntax for value + validators
  • ReactiveFormsModule provides the directives โ€” import it in the component
  • [formGroup] binds a form to a group; formControlName binds an input to a control
  • form.value is synchronous and, since Angular 14, typed
  • setValue requires all fields; patchValue accepts partial
  • reset returns 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!