Reactive Form
About
The Form is created programmatically and syncronized with the DOM.
Some advantages are:
It is build around Observable streams.
It is more roboust, scalable, reusable and testable.
It uses synchronous data-flow between View and Data models, which makes creating large-scale forms easier.
(View-to-Model) and (Model-to-view) flow. (more info)
Keep the data model pure by providing it as an immutable data structure. So each time a change is triggered on the data model, the
FormControl
instance returns a new data model rather than updating the existing one.This gives the ability to track unique changes to the data model through the control's Observable.
Then change detection is more efficient because it only needs to update on unique changes.
Can have Custom validation functions.
Testing can be done without renderding the UI, in these tests, status and data are queried and manipulated through the control without interacting the the change detection cycle. (more info)
Examples
Strictly Typing Setup
You may strictly type the controls if needed with:
@Component({ ... })
export class FormPageComponent {
protected username = new FormControl<string | null>('');
}
Simple Setup - Just FormControl
without Services
FormControl
without Services
By default FormControl
create typed form controls based on the initial value.
To use untyped form controls you must use UntypedFormControl
.
@Component({
selector: "app-form-page",
import: [ReactiveFormsModule],
template: ` <input type="text" [formControl]="username" /> `,
})
export class FormPageComponent {
protected username = new FormControl("");
protected resetUsername() {
this.username.setValue("");
}
}
Simple Setup - FormGroup
without Services
FormGroup
without Services
To Submit forms, use ngSubmit
in the Template.
To programatically change form control values, you can use:
setValue
: Must update the holeFormGroup
object, even if only updating one of the form controls.patchValue
: Can update one or multiple form controls.
@Component({
seletor: "app-form-page",
import: [ReactiveFormModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="handleSubmit()">
<input type="text" formControlName="username" />
<input type="password" formControlName="password" />
</form>
`,
})
export class FormPageComponent {
protected loginForm = new FormGroup({
username: new FormControl(""),
password: new FormControl(""),
});
protected handleSubmit() { ... }
protected resetForm() {
this.loginForm.setValue({
username: '',
password: '',
});
// Or
this.loginForm.patchValue({
username: ''
});
}
}
Nested Setup - FormGroup
without Services
FormGroup
without Services
@Component({
seletor: "app-form-page",
import: [ReactiveFormModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="handleSubmit()">
<div formGroupName="user">
<input type="text" formControlName="username" />
<input type="password" formControlName="password" />
</div>
<div formGroupName="optional">
<input type="number" formControlName="age" />
</div>
</form>
`,
})
export class FormPageComponent {
protected loginForm = new FormGroup({
user = new FormGroup({
username: new FormControl(""),
password: new FormControl(""),
}),
optional = new FormGroup({
age: new FormControl(0),
}),
});
}
Simple Setup - FormGroup
with Service: FormBuilder
FormGroup
with Service: FormBuilder
Controls created with FormBuilder
will be automatically typed based on the initial value. (To use untyped forms you must explicitly use the UntypedFormBuilder
)
formBuilder
has 4 functions:
group()
: To createFormGroup
.First parameter, a collection of child controls
FormControl
.Second Parameter, object of options
AbstractControlOptions
containing:validators
: Synchronous validators that will be ran for the Group. (For multiple controls)asyncValidators
: Aynsc validators that will be ran for the Group. (For multiple controls)updateOn
: The event upon which the control should be updated. ('change' | 'blur' | 'submit'
)
record()
: To createFormRecord
.First paremeter, a collection of child controls
FormControl
.Second Parameter, object of options
AbstractControlOptions
containing:validators
: Synchronous validators that will be ran for the Group. (For multiple controls)asyncValidators
: Aynsc validators that will be ran for the Group. (For multiple controls)updateOn
: The event upon which the control should be updated. ('change' | 'blur' | 'submit'
)
control()
: To createFormControl
.First parameter, the initial value for the control,
Second parameter,
ValidatorFn
orValidatorFn[]
orFormControlOptions
.Third parameter,
AsyncValidatorFn
orAsyncValidatorFn[]
.
array()
: To createFormArray
.First parameter, an Array of child Controls.
Second parameter,
ValidatorFn
orValidatorFn[]
orFormControlOptions
.Third parameter,
AsyncValidatorFn
orAsyncValidatorFn[]
.
@Component({
seletor: "app-form-page",
import: [ReactiveFormModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="handleSubmit()">
<input type="text" formControlName="username" />
<input type="password" formControlName="password" />
</form>
`,
})
export class FormPageComponent {
private formBuilder = inject(FormBuilder);
protected loginForm = this.formBuilder.group({
username: [""],
password: [""],
});
}
Validation Setup - FormGroup
FormGroup
Validators are passed as second parameter, after the initial value.
Async Validators are passed as third parameter.
Form validation status
, can be accessed by formVariable.status
.
You can also access each form control status with:
formVariable.controls.<formControlName>.invalid
formVariable.controls.get('formControlName').invalid
@Component({
seletor: "app-form-page",
import: [ReactiveFormModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="handleSubmit()">
<input type="text" formControlName="username" />
<input type="password" formControlName="password" />
</form>
`,
})
export class FormPageComponent {
private formBuilder = inject(FormBuilder);
protected loginForm = this.formBuilder.group({
username: ["", Validators.required],
password: ["", [Validators.required]],
});
// Or
protected loginForm = this.formBuilder.group({
username: ["", { validators: Validators.required }],
password: ["", { validators: [Validators.required] }],
});
// Or
protected loginForm = new FormGroup({
username: new FormControl("", Validators.required),
password: new FormControl("", [Validators.required]),
});
}
Dynamic Setup - FormArray
with Service: FormBuilder
FormArray
with Service: FormBuilder
The FormArray
instance represents an undefined number of controls in an array.
Each FormControl
created will be referenced by its index
in the FormArray
.
It can be useful for creating dynamic form inputs or managing checkboxes or radio buttons.
@Component({
seletor: "app-form-page",
import: [ReactiveFormModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="handleSubmit()">
<input type="text" formControlName="username" />
<input type="password" formControlName="password" />
<button type="button" (click)="addAlias()">New Alias</button>
<div>
@for (alias of aliases; track alias.id; let idx = $index) {
<input type="text" [formControlName]="idx" />
}
</div>
<!-- OR -->
<div>
@for (alias of formData.get('aliases').controls; track alias.id; let idx = $index) {
<input type="text" [formControlName]="idx" />
}
</div>
</form>
`,
})
export class FormPageComponent {
private formBuilder = inject(FormBuilder);
protected loginForm = this.formBuilder.group({
username: ["", Validators.required],
password: ["", [Validators.required]],
aliases: this.formBuilder.array([this.formBuilder.control("")]),
});
// You may use getter syntax to create a class property to retrieve the Form Array control values from the group, instead of calling directly in the Template
protected get aliases() {
// By default the array control is return as AbstractControl, so explicitly type it as FormArray, so you can loop it in the Template
return this.loginForm.get("aliases") as FormArray;
// Or
return this.loginForm.get("aliases").controls;
}
/*
Define a method to dynamically insert new values into the created class property `aliases`.
*/
protected addAlias() {
this.aliases.push(this.formBuilder.control(""));
// Or
(<FormArray>this.formData.get("aliases")).push(this.formBuilder.control(""));
}
}
NonNullable Setup - FormControl
FormControl
When reseting the form, the initial
values will be used by default.
You may defined non-nullable form controls with:
@Component({ ... })
export class FormPageComponent {
protected username = new FormControl('', { nonNullable: true });
}
Or
@Component({ ... })
export class FormPageComponent {
private formBuilder = inject(NonNullableFormBuilder);
// The entire form will be Non-Nullable
protected loginForm = this.formBuilder.group({
...
});
}
Or
@Component({ ... })
export class FormPageComponent {
private formBuilder = inject(FormBuilder);
protected loginForm = this.formBuilder.group({
username: ['', Validators.required],
// You can define specific controls to be Non-Nullable
password: this.formBuilder.nonNullable.control('', {
validators: [Validators.required]
})
// or
password: this.formBuilder.control('', {
validators: [Validators.required],
nonNullable: true,
})
});
}
Subscribing to statusChanges
or valueChanges
Observables - Reactive value changes
statusChanges
or valueChanges
Observables - Reactive value changesFor instance saving the form values in the localStorage
after 500ms.
@Component({ ... })
export class FormPageComponent implements OnInit {
private formBuilder = inject(FormBuilder);
private valueChangesSubscription;
private statusChangesSubscription;
protected loginForm = this.formBuilder.group({
...
});
ngOnInit() {
const savedForm = window.localStorage.getItem('saved-form-data');
if (savedForm) {
const loadedForm = JSON.parse(savedForm);
this.loginForm.patchValue({
username: loadedForm.username
});
}
// Executed everytime a value changes in the form
this.valueChangesSubscription = this.loginForm.valueChanges.pipe(debouceTime(500)).subscribe((value) => {
window.localStorage.setItem('saved-form-data', JSON.stringify({ username: value.username }));
});
// Executed everytime the form status changes
this.statusChangesSubscription = this.loginForm.statusChanges.subscribe((status) => {
console.log(status);
});
}
ngOnDestroy() {
this.valueChangesSubscription.unsubscribe();
this.statusChangesSubscription.unsubscribe();
}
}
Custom Validators Setup - Synchronous
Synchronous
Functions that take a control instance and immediately return either a set of validation errors or null.
@Component({ ... })
export class FormPageComponent {
readonly formBuilder = inject(FormBuilder);
private forbiddenUsernames = ['dicken', 'balls'];
protected loginForm = this.formBuilder.group({
username: new FormControl('', {
// You must use 'bind' to pass the correct 'this' reference, which must be of the class
validators: [Validators.required, this.checkNames.bind(this), this.checkRegExp(/bob/i)]
})
});
// Or
protected loginForm = this.formBuilder.group({
// You must use 'bind' to pass the correct 'this' reference, which must be of the class
username: ['', [Validators.required, this.checkNames.bind(this), this.checkRegExp(/bob/i)]]
});
// The return is a key, with an error flagname if invalid
// Return nothing or null if it is valid
checkNames(control: FormControl): {[key: string]: boolean} {
if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
return { nameIsForbidden: true };
}
return null;
}
checkRegExp(re: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = re.test(control.value);
return forbidden ? { forbiddenName: { value: control.value } } : null;
}
}
}
Cross-field Validation
You add the validator method to the group.
@Component({ ... })
export class FormPageComponent {
readonly formBuilder = inject(FormBuilder);
protected loginForm = this.formBuilder.group({
username: ['', Validators.required],
passwords: this.formBuilder.group({
password: [''],
confirmPassword: ['']
}, {
validators: equalPasswords
})
});
protected equalPasswords: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const password = control.get('password')?.value;
const confirmPasswords = control.get('confirmPasswords')?.value;
return password && confirmPasswords && password === confirmPasswords ? null : { notEqual: true };
}
}
Custom Validators Setup - Async
Async
Functions that take a control instance and must return a Promise
or Observable
that later emits a set of validations errors or null.
For performance reasons, Angular only runs async validators after all sync validators pass.
The Validator functions takes the same control: AbstractControl
param.
For intance reaching to a webserver to validate:
@Component({ ... })
export class FormPageComponent {
readonly formBuilder = inject(FormBuilder);
protected loginForm = this.formBuilder.group({
username: new FormControl('', {
asyncValidators: [this.checkNames.bind(this)]
})
});
// Or
protected loginForm = this.formBuilder.group({
username: ['', [], [this.checkNames.bind(this)]]
});
// The return is a key, with an error flagname if invalid
// Return nothing or null if it is valid
checkNames(control: FormControl) {
// Will return a Promise of null or object
}
}
Last updated