angular-typesafe-reactive-forms-helper

Use angular reactive forms with type-safety.

Usage no npm install needed!

<script type="module">
  import angularTypesafeReactiveFormsHelper from 'https://cdn.skypack.dev/angular-typesafe-reactive-forms-helper';
</script>

README

angular-typesafe-reactive-forms-helper

GitHub Workflow Status (branch) GitHub package.json version GitHub commit activity GitHub npm GitHub forks

Quick Syntax

Instead of:

this.form.get('heroName').patchValue('He-Man');

angular-typesafe-reactive-forms-helper allows:

this.form.getSafe(x => x.heroName).patchValue('He-Man');

Why

  • Get intellisense
  • No more misspelled property names
  • Refactoring Reactive Forms is back to a trivial IDE rename task

Demo

In order to make this work as closely as possible to the Angular way, an abstract class FormGroupTypeSafe<T> was derived from Angular’s FormGroup with the intent not to break existing code.

Intellisense on FormGroupTypeSafe.value:

FormGroupTypeSafe.value Intellisense

Intellisense on FormGroupTypeSafe.getSafe and then patching the value:

FormGroupTypeSafe.getSafe Intellisense

How to use:

1. Define an interface of your form model.

//interface used with FormGroupTypeSafe<T>
interface IHeroFormModel {
  name: string;
  secretLairs: Array<Address>;
  power: string;
  sidekick: string
}

2. Declare your new FormGroupTypeSafe form with the help of TypeScript’s generics.

/* TypeSafe Reactive Forms Changes */
//old code
//heroForm: FormGroup;
heroForm: FormGroupTypeSafe<IHeroFormModel>;

3. Inject FormBuilderTypeSafe

constructor(
   /* TypeSafe Reactive Forms Changes */
   //old code - private fb: FormBuilder,
   private fb: FormBuilderTypeSafe,
   private heroService: HeroService) {

   this.createForm();
   this.logNameChange();
 }

4. Create your form group with Interfaces (contracts).

// old code
//    this.heroForm = this.fb.group({
//      name: '',
//      secretLairs: this.fb.array([]),
//      power: '',
//      sidekick: ''
//    });


 this.heroForm = this.fb.group<IHeroFormModel>({
      name: new FormControl(''),
      secretLairs: new FormControl([]),
      power: new FormControl(''),
      sidekick: new FormControl('')
    });

//***** Nested type sample *****
interface IAddressModel {
   suburb: string;
   postcode: string;
}

interface ICustomerModel {
  name: string;
  address: IAddressModel;
}

 this.form = this.fb.group<ICustomerModel>({
        name: new FormControl(null, [Validators.required]),
        address: this.formBuilder.group<IAddressModel>({
            suburb: new FormControl(''),
            postcode: new FormControl('', [Validators.required]),
      })
  });

Peer Dependencies

@angular/forms and all its peer dependencies.

This package has been tested with Angular 9, 10, 11.

(Should work with Angular 4, 5, 6, 7, 8 too)

I would encourage you to use versions Angular still support, see Angular's Support policy and schedule.

Blog

For a more in detail description of the benefits of this package, read my blog - Angular typesafe reactive forms.

When reading the blog, be mindful that it was written Oct-2017, before the angular-typesafe-reactive-forms-helper package existed. Back then, the idea was to copy the code and adjust as needed. Since then, there were a few requests, thus angular-typesafe-reactive-forms-helper was born.

Contributions

I only added features required by my projects, but I know more could be added with your help.

Create a PR to get the conversation started :smile:

Lastly

Use it…don’t use it :smile:


Release notes

The model used for all code samples:

interface HeroFormModel {
    heroName: string;
    weapons: WeaponModel[];
}
  
interface WeaponModel {
    name: string;
    damagePoints: number;
}

FormGroupTypeSafe<T> extends Angular's FormGroup class

V2.0.2 (2021-05-18)

  • Bump to Angular 11.
  • Stop integration tests for Angular 8.

V2.0.1 (2020-12-09)

Package the correct library files, instead of the repository - rookie mistake :)

V2.0.0 (2020-11-06)

  • use ng-packagr to fix bug - main.ts:15 Error: Angular JIT compilation failed
  • add end-to-end-tests Angular 8, 9, 10
  • removed Angular 7 integration tests from build pipeline as it is no longer supported by Angular team

New dist file structure:

./dist:
LICENSE
README.md
angular-typesafe-reactive-forms-helper.d.ts
angular-typesafe-reactive-forms-helper.metadata.json
bundles
esm2015
fesm2015
package.json
public_api.d.ts
src

./dist/bundles:
angular-typesafe-reactive-forms-helper.umd.js
angular-typesafe-reactive-forms-helper.umd.js.map
angular-typesafe-reactive-forms-helper.umd.min.js
angular-typesafe-reactive-forms-helper.umd.min.js.map

./dist/esm2015:
angular-typesafe-reactive-forms-helper.js
public_api.js
src

./dist/esm2015/src:
angularTypesafeReactiveFormsHelper.js
getPropertyName.js

./dist/fesm2015:
angular-typesafe-reactive-forms-helper.js
angular-typesafe-reactive-forms-helper.js.map

./dist/src:
angularTypesafeReactiveFormsHelper.d.ts
getPropertyName.d.ts

Old dist file structure:

./dist:
LICENSE
README.md
lib
package.json

./lib:
angularTypesafeReactiveFormsHelper.d.ts
angularTypesafeReactiveFormsHelper.js
getPropertyName.d.ts
getPropertyName.js

V1.8.2 (2020-09-04)

  • Fix bug - getSafe() call fails and returns null when compiled to ES5.

V1.8.1 (2020-06-26)

  • Bump to Angular 10.
  • Stop integration tests for Angular 6.

V1.8.0 (2020-06-16)

  • added removeControlSafe

Sample:

 let sut: FormGroupTypeSafe<HeroFormModel> = createGroup();
 sut.removeControlSafe(x => x.heroName);

The bottom code was avoided simply because in a variable rename scenario, the IDE should rename all the references instead of just informing one where the errors are.

removeControl(name: keyof T): void;
removeControl(name: string): void;

V1.7.0 (2020-05-14)

  • added controls

Angular's forms.d.ts:

controls: { [key: string]: AbstractControl; };

angular-typesafe-reactive-forms-helper:

controls: { [P in keyof T]: AbstractControlTypeSafe<T[P]> };

Code samples:

let sut: FormGroupTypeSafe<HeroFormModel> = createGroup();
// $ExpectType { heroName: AbstractControlTypeSafe<string>; weapons: AbstractControlTypeSafe<WeaponModel[]>; }
 sut.controls;

 // $ExpectType AbstractControlTypeSafe<string>
 sut.controls.heroName;
 // $ExpectType AbstractControlTypeSafe<WeaponModel[]>
 sut.controls.weapons;

 // $ExpectType string
 sut.controls.heroName.value;
 // $ExpectType WeaponModel[]
 sut.controls.weapons.value;

V1.6.0 (2020-04-22)

  • added statusChanges and status

Angular's forms.d.ts:

/**
 * The validation status of the control. There are four possible
 * validation status values:
 *
 * * **VALID**: This control has passed all validation checks.
 * * **INVALID**: This control has failed at least one validation check.
 * * **PENDING**: This control is in the midst of conducting a validation check.
 * * **DISABLED**: This control is exempt from validation checks.
 *
 * These status values are mutually exclusive, so a control cannot be
 * both valid AND invalid or invalid AND disabled.
 */
readonly status: string;

angular-typesafe-reactive-forms-helper:

 export type ControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';

 export interface FormGroupTypeSafe<T> extends FormGroup {
  readonly status: ControlStatus;
  readonly statusChanges: Observable<ControlStatus>;
}

Code samples:

 let sut: FormGroupTypeSafe<HeroFormModel> = createGroup();

 // $ExpectType ControlStatus
 sut.status;

 sut.statusChanges.subscribe(val => {
     // $ExpectType ControlStatus
     val;
 });

 // $ExpectType string | undefined
 sut.getSafe(x => x.heroName)?.status; // unfortunately this is still string ¯\_(ツ)_/¯

 sut.getSafe(x => x.heroName)?.statusChanges.subscribe(val => {
     // $ExpectType ControlStatus
     val;
 });
 

V1.5.1 (2020-04-17)

Had this error in Angular 9.1.2 when executing ng serve. The app would show a blank page with an error in browser's devtools console:

main.ts:15 Error: Angular JIT compilation failed: '@angular/compiler' not loaded!
  - JIT compilation is discouraged for production use-cases! Consider AOT mode instead.
  - Did you bootstrap using '@angular/platform-browser-dynamic' or '@angular/platform-server'?
  - Alternatively provide the compiler with 'import "@angular/compiler";' before bootstrapping.
    at getCompilerFacade (core.js:643)
    at Function.get (core.js:16349)
    at getFactoryDef (core.js:2200)
    at providerToFactory (core.js:17183)
    at providerToRecord (core.js:17165)
    at R3Injector.processProvider (core.js:16981)
    at core.js:16960
    at core.js:1400
    at Array.forEach (<anonymous>)
    at deepForEach (core.js:1400)

This is fixed.

More info on the error from StackOverflow.


V1.5.0 (2020-04-15)

Extend AbstractControlTypeSafe<P> with:

  readonly valueChanges: Observable<T>;
  get(path: Array<string> | string): AbstractControl | null; 
  get(path: number[]): AbstractControlTypeSafe<T extends (infer R)[] ? R : T> | null;
  • Samples readonly valueChanges: Observable<T>;:
let sut: FormGroupTypeSafe<HeroFormModel> = createGroup();
sut.valueChanges.subscribe(val => {
    // $ExpectType HeroFormModel
    val;
});

sut.getSafe(x => x.heroName).valueChanges.subscribe(val => {
    // $ExpectType string
    val;
});
  • Split Angular's get into two functions based on the path: Array<string | number> | string parameter.

Angular's forms.d.ts:

      get(path: Array<string | number> | string): AbstractControl | null;

angular-typesafe-reactive-forms-helper:

 get(path: Array<string> | string): AbstractControl | null;
 get(path: number[]): AbstractControlTypeSafe<T extends (infer R)[] ? R : T> | null;

This allows type safety when working with arrays.

sut.getSafe(x => x.weapons).get([0]).valueChanges.subscribe(val => {
    // $ExpectType WeaponModel
    val;
});

// the angular way - .get('person.name')
sut.getSafe(x => x.weapons).get('person.name').valueChanges.subscribe(val => {
    // $ExpectType any
    val;
});

// the angular way - .get(['person', 'name'])
sut.getSafe(x => x.weapons).get(['person', 'name']).valueChanges.subscribe(val => {
    // $ExpectType any
    val;
});

V1.4.0 (2020-04-14)

  • new interface AbstractControlTypeSafe<P> which extends from Angular's AbstractControl and will, over time, contain the common properties to Angular's FormGroup, FormControl and FormArray. Currently it only returns readonly value: T.

  • enhanced getSafe to return AbstractControlTypeSafe<P>

  getSafe<P>(propertyFunction: (typeVal: T) => P): AbstractControlTypeSafe<P> | null;

Code example:

 // heroName: string
 sut.getSafe(x => x.heroName)?.value; // value's ExpectType => string | undefined
  • add new type RecursivePartial<T>
  • enhanced patchValue to use RecursivePartial<T> so one is not forced by the compiler to complete mandatory properties on a nested types.
patchValue(value: RecursivePartial<T>, options?: Object): void;

Code Example:

let sut: FormGroupTypeSafe<HeroFormModel> = formBuilderTypeSafe.group<HeroFormModel>({...}) // let's pretend a valid FormGroupTypeSafe object was created here  
// Looking at the line below... 
// Before V1.4.0, Typescript would have complained about missing property damagePoints.
// This is not the case anymore as now all nested types will be Partial properties. 
sut.patchValue({ weapons: [{ name: "Head" }]});

V1.3.0 (2020-04-06)

  • patchValue

Angular's forms.d.ts:

patchValue(value: any, options?: Object): void;

angular-typesafe-reactive-forms-helper:

patchValue(value: Partial<T>, options?: Object): void;
  • formBuilderTypeSafe.group<T> supports FormArray
 sut = formBuilderTypeSafe.group<HeroFormModel>({
      heroName: new FormControl('He-Man', Validators.required),
      weapons: new FormArray([formBuilderTypeSafe.group<WeaponModel>({
            name: new FormControl('Sword', Validators.required),
            damagePoints: new FormControl(50, Validators.required)
        }),
        formBuilderTypeSafe.group<WeaponModel>({
            name: new FormControl('Shield', Validators.required),
            damagePoints: new FormControl(0, Validators.required)
        }),
      ])
    });

V1.2.0 (2020-04-02)

  • valueChanges, function returns Observable<T>

Angular's forms.d.ts:

valueChanges: Observable<any>;

angular-typesafe-reactive-forms-helper:

valueChanges: Observable<T>;

V1.1.0 (2020-03-31)

  • setValue, just a function signature update.

Angular's forms.d.ts function signature:

    setValue(value: {
        [key: string]: any;
    }, options?: {
        onlySelf?: boolean;
        emitEvent?: boolean;
    }): void;

angular-typesafe-reactive-forms-helper signature:

    setValue(value: T, 
            options?: { 
              onlySelf?: boolean; 
              emitEvent?: boolean 
    }): void;

V1.0.0 (2020-03-29)

angular-typesafe-reactive-forms-helper has these extra functions:

  • getSafe
  • setControlSafe