Form State Flags
Form controls expose state flags that drive when and how validation messages appear.
State Flags
const control = this.form.get('email');
control?.valid; // All validators pass
control?.invalid; // At least one validator fails
control?.dirty; // User has changed the value
control?.pristine; // User has NOT changed the value
control?.touched; // User has focused and blurred the field
control?.untouched; // User has NOT focused the field
control?.pending; // Async validators are running
control?.errors; // { required: true, ... } or null
control?.value; // Current value
control?.status; // 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'
Using Flags for Validation Messages
<form [formGroup]="form">
<label>
Email
<input type="email" formControlName="email">
</label>
<!-- Show error only after user interaction -->
@if (form.get('email')?.invalid && form.get('email')?.touched) {
<p class="error">Please enter a valid email</p>
}
<!-- Show success indicator -->
@if (form.get('email')?.valid && form.get('email')?.dirty) {
<p class="success">Looks good!</p>
}
</form>
markAllAsTouched
When submitting an invalid form, mark all controls as touched to reveal all errors:
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
// Process valid form
console.log('Form value:', this.form.value);
}
Disabling Submit Button
<button type="submit" [disabled]="form.invalid || form.pending">
Submit
</button>
Observing valueChanges and statusChanges
FormGroup and FormControl expose Observables that emit on every value or status change.
valueChanges Observable
@Component({
selector: 'app-search',
template: `
<input formControlName="query" placeholder="Search...">
<p *ngIf="results$ | async as results">
{{ results.length }} results found
</p>
`,
})
export class SearchComponent implements OnInit, OnDestroy {
private fb = inject(FormBuilder);
private searchService = inject(SearchService);
private destroy$ = new Subject<void>();
form = this.fb.group({
query: [''],
});
results$: Observable<SearchResult[]> = this.form.get('query')!.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.searchService.search(query)),
takeUntil(this.destroy$)
);
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
statusChanges Observable
this.form.statusChanges.subscribe(status => {
console.log('Form status:', status); // 'VALID' | 'INVALID' | 'PENDING'
});
Autosave Pattern
@Component({
selector: 'app-profile-editor',
template: `...`,
})
export class ProfileEditorComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit(): void {
this.form.valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
takeUntil(this.destroy$)
).subscribe(values => {
this.saveToServer(values);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
setValue vs patchValue
// setValue — must provide ALL controls
this.form.setValue({
email: 'test@example.com',
password: 'secret123',
});
// patchValue — only update specified controls
this.form.patchValue({
email: 'new@example.com',
});
Quiz
1. What does the touched flag indicate?
2. What does markAllAsTouched() do?
3. What Observable does FormControl emit on every value change?
Flashcards
Question
What does the touched flag mean?
Click to reveal answer
Answer
User has focused and blurred the field. Use it to show errors only after interaction.
Question
What does markAllAsTouched() do?
Click to reveal answer
Answer
Marks all controls as touched, revealing all validation errors. Used on invalid submit.
Question
What Observable emits on value change?
Click to reveal answer
Answer
valueChanges — emits the new value on every change. Use for live search and autosave.
Revision Notes
Key Takeaways
- 1. touched flag controls when validation messages appear
- 2. markAllAsTouched() reveals all errors on invalid submit
- 3. valueChanges Observable enables live search and autosave
- 4. setValue requires all controls; patchValue updates selectively
Interview Tips
- • Explain the difference between dirty and touched
- • Be ready to describe the autosave pattern
- • Mention valueChanges for reactive form interactions
Cheat Sheet
Form State Cheat Sheet
State flags:
- valid / invalid
- dirty / pristine
- touched / untouched
- pending (async validators)
Show errors after interaction:
@if (form.get('email')?.invalid && form.get('email')?.touched) {
<p class="error">Error message</p>
}
Reveal all errors on submit:
this.form.markAllAsTouched();
Live search:
this.form.get('query')!.valueChanges.pipe(
debounceTime(300),
switchMap(q => this.searchService.search(q))
).subscribe();