Notes & options from AI
- Scope control
- Per-component instance (recommended): keep providers: [PersonFormModel] in the component as shown.
- Singleton: use @Injectable({ providedIn: ‘root’ }) on the model and remove the providers array (but beware of shared form state across pages).
- Type of birthday:
- Using native
<input type="date">
→ keep as string (YYYY-MM-DD).
- Using MatDatepicker + MatNativeDateModule → change the control to FormControl.
- Unit tests:
- This refactor makes testing easier—instantiate PersonFormModel directly (no TestBed/DI required) for pure form logic, or use component provider for integration tests.
Here are the practical benefits of using the FormGroup
/ FormControl
constructors instead of FormBuilder
—especially with typed forms in Angular 16+:
Less coupling to Angular DI (simpler construction & testing)
-
No service injection required (
FormBuilder
is a service). You can create forms in plain TypeScript classes, helper functions, or unit tests without TestBed
or inject(FormBuilder)
.
- This makes library/util code cleaner and makes unit tests faster and more focused (no DI setup for the form model).
More explicit & readable control definitions
- Constructors make the shape obvious: the control value, validators, and options are separated and self-explanatory—no need to remember tuple positions like
[value, validators, asyncValidators]
.
- Async validators and options are clearer
- With constructors, you’re nudged to declare generic types explicitly, which guards you against accidental
null
or wrong shapes.
- At the group level you can fully type the form for end-to-end IntelliSense:
Fewer hidden defaults & clearer nullability
- With
FormBuilder
, devs often rely on the tuple shorthand and unintentionally end up with string | null
controls.
- Constructors make nullability an explicit choice via
nonNullable: true
or by typing string | null
.
Easier reuse & composition
- You can compose controls before grouping, reuse validators/configuration, and split large forms into smaller, reusable blocks without always threading a
FormBuilder
reference around.
Clearer migration path & consistency
- Using constructors aligns with the underlying API that
FormBuilder
wraps. If the API evolves, you’re closest to the source of truth.
- You avoid team-level ambiguity between tuple shorthand vs. object/constructor style—one consistent pattern.
import {Injectable} from '@angular/core';
import {FormControl, FormGroup, Validators,} from '@angular/forms';
@Injectable({
providedIn: 'root'
})
export class PersonFormModel {
readonly form = new FormGroup({
firstName: new FormControl<string>('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(2)],
}),
lastName: new FormControl<string>('', {
nonNullable: true,
validators: [Validators.required],
}),
birthday: new FormControl<Date | null>(null, {
nonNullable: true,
validators: [Validators.required],
}),
});
markAllAsTouched(): void {
this.form.markAllAsTouched();
}
reset(): void {
this.form.reset();
}
}
export type PersonFormGroup = PersonFormModel['form'];
export type PersonControlName = keyof PersonFormGroup['controls']