DEV Community

Joaquin Cid
Joaquin Cid

Posted on

Angular Reactive Forms typed!

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 value
Type-check form value

Type-check form model when instantiating FormGroup
Type-check form model when instantiating FormGroup

Type-check form model when patching value
Type-check form model when patching value

Type-check field value 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.

FormControl typed
FormControl typed

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)