Angular Reactive Forms are great, they come with two way data-binding between the model and view, support for sync/async validation, event handling, and status; but they don't come with typing support.
Lucky for us, adding this feature to Reactive Forms is very easy, and we'll see how we can do that in this tutorial.
First, we'll create our own generic versions of FormGroup<T>
and AbstractControl<T>
class. We'll just override the minimum required methods and properties to make our forms typed. Let's create a file called forms.ts
and the following code.
import {
FormGroup as NgFormGroup,
AbstractControl as NgAbstractControl,
ValidatorFn,
AbstractControlOptions,
AsyncValidatorFn
} from '@angular/forms';
import { Observable } from 'rxjs';
export abstract class AbstractControl<T = any> extends NgAbstractControl {
readonly value: T;
readonly valueChanges: Observable<T>;
abstract setValue(value: Partial<T> | T, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void
abstract patchValue(value: Partial<T> | T, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void
}
export class FormGroup<T = any> extends NgFormGroup {
readonly value: T;
readonly valueChanges: Observable<T>;
constructor(controls: { [key in keyof T]?: AbstractControl; },
validatorOrOpts?: ValidatorFn | Array<ValidatorFn> | AbstractControlOptions | null,
asyncValidator?: AsyncValidatorFn | Array<AsyncValidatorFn> | null) {
super(controls, validatorOrOpts, asyncValidator);
}
patchValue(value: Partial<T> | T, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void {
super.patchValue(value, options);
}
get(path: Array<Extract<keyof T, string>> | Extract<keyof T, string> | string): AbstractControl | never {
return super.get(path);
}
controls: {
[key in keyof T]: AbstractControl<T[key]>;
};
}
Next, we'll create the form model, in this case a simple sign-up. Let's create a file called sign-up-form.ts
.
export type SignUpForm = {
email: string,
password: string,
optInNewsletter: boolean
}
Finally, we'll implement the generic FormGroup
in our component. We instantiate the generic FormGroup
with the SignUpForm
type. Notice we import our FormGroup
and not Angular's.
//...
import { FormGroup } from './forms';
export class AppComponent implements OnInit {
form = new FormGroup<SignUpForm>({
email: new FormControl(''),
password: new FormControl(''),
optInNewsletter: new FormControl('')
})
ngOnInit() {
this.form.patchValue({
email: 'joaqcid@gmail.com',
optInNewsletter: true
})
}
submit() {
console.log("typed forms", this.form.value)
}
}
Here we can see, how Typescript picks the Type used, and type-checks when we create the FormGroup, or we patchValue
and returns the Type defined when we get the value
Type-check form model when instantiating FormGroup
Type-check form model when patching value
Type-check field value when patching value
Cool! Isn't this great? Forms on steroids! 💉💊💉💊
Finally let's extend the FormControl<T>
class in case we want to need to work with them as well.
On forms.ts
let's add the followring code.
import {
FormControl as NgFormControl,
//...
} from '@angular/forms';
//...
export class FormControl<T = any> extends NgFormControl {
readonly value: T;
readonly valueChanges: Observable<T>;
setValue(value: Partial<T> | T, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void {
super.setValue(value, options);
}
patchValue(value: Partial<T> | T, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void {
super.patchValue(value, options);
}
}
On our component lets import our own FormControl implementation and build one. Notice I'm using a custom type NgbDate
from ng-bootstrap.
import { ..., FormControl } from './forms';
import { NgbDate } from '@ng-bootstrap/ng-bootstrap';
export class AppComponent implements OnInit {
//...
formControl = new FormControl<NgbDate>('')
ngOnInit() {
//...
this.formControl.patchValue(new NgbDate(2019, 9, 11))
}
}
Again, by using the typed version of our FormControl, we can type-check the FormControl when we patch, set or get the value.
Conclusion
With these few lines of code we were able to extend Angular's Reactive Forms to make them typed, this can help us to type our form and add other custom functionality if we need too. Be careful when modifying behaviour as it may impact the current Form behaivour.
You can check the full example in this StackBlitz.
This post have been written after reading different approaches in an open Angular's github issue, there are also libraries such as ngx-typed-forms, that also provide similar functionality.
Hope you enjoyed this post, and if you did, please show some ❤️. I often write about Angular, Firebase and Google Cloud platform services. If you’re interested on more content, you can follow me on dev.to, medium and twitter.
Top comments (0)