Angular strongly typed reactive forms
Asked Answered
P

9

35

I'm looking to refactor a large set of components in my Angular project to have strongly typed FormGroups, FormArrays, and FormControls.

I'm just looking for a good way to implement strongly typed reactive forms. Could anyone provide suggestions/recommendations from their own experiences?

Thank you.

EDIT:

To clarify, by strongly typed I mean currently when I create a FormGroup or FormArray I have no way to specify the structure of the actual form inside it. When I pass this form around to various components in my app, I then feel I am making it more difficult to maintain.

Plugboard answered 7/3, 2019 at 21:59 Comment(9)
Your meaning of strongly-typed isn't really clear here. Can you please give us an example to elaborate as to what exactly are you looking for?Disproportionation
@SiddAmjera To clarify, by strongly typed I mean currently when I create a FormGroup or FormArray I have no way to specify the structure of the actual form inside it. When I pass this form around to various components in my app, I then feel I am making it more difficult to maintain.Plugboard
Would it be possible for you to create a Sample StackBlitz, minimally replicating this your scenario?Disproportionation
@SiddAmjera Sorry maybe I'm not explaining it clearly enough. It's not a specific piece of code. But hopefully this explains the issue: stackblitz.com/edit/angular-tlfrgtPlugboard
Ahhhh. Got it. I don't really think this is something that's baked right into Angular's Reactive Forms. You'll probably have to extend the FormGroup and make something on your own. I'm also not sure if there's a library that does something like this.Disproportionation
If it's just the value that you're concerned about, I think you can strongly type the parameters of the methods that are receiving these values. Just create interfaces for Data Models and type the parameters with these interface types.Disproportionation
Yeh I need to do more than just access the values, I need to be able to set them and update them programatically as well. I've been googling it a bit, I just thought I'd post a question here to see if anyone else had any experience doing it themselves before I gave it a go. Thank you for your help anyway!Plugboard
@TomVinnicombe I was having the similar concern in my application and I resolved the same. Before going to give answer to your question would like to understand little bit more about your statement is "I need to be able to set them and update them programatically as well.". Can you please provide code sample which kind of set and update operation you are performing? I also update and set the value based on some operation, But would be good if you share code snippetChapin
Angular team is currently working on it, they have MVP of generic/typed form! github.com/angular/angular/issues/13721#issuecomment-942910823Rheumatic
N
23

The most elegant solution is leveraging TypeScript declaration files (*.d.ts) to introduce generic interfaces extending the standard form classes like AbstractControl, FormControl, etc. It doesn’t introduce any new functionality and has no footprint in the compiled JavaScript, but at the same time enforcing strong type checking.

It was suggested by Daniele Morosinotto in March this year and there are talks now to include it in Angular 9.

Adopting the solution is straightforward:

  1. Download TypedForms.d.ts from this gist and save it as src/typings.d.ts in your project (Angular 6+ already knows how to use this file).
  2. Start using the new types (FormGroupTyped<T>, FormControlTyped<T>, etc.) whenever you need a strong type validation (see examples in that gist or stackblitz).

For more information, check out a blog post analysing available solutions for strongly typed forms.

Noel answered 6/6, 2019 at 11:9 Comment(2)
From where do you import FormGroupTyped?Teratogenic
Nevermind, I was using a Lib and you have to copy the TypedForms.d.ts file into each library as well...Teratogenic
C
21

For ones who want another solution. I found this article talking about strong type for angular form. Below is my sumary.

interface Person {
  name: string;
  email: string
}

// Controls in a form group that would emit a Person as it's value
type PersonControls = { [key in keyof Person]: AbstractControl };
type PersonFormGroup = FormGroup & { value: Person, controls: PersonControls };

export class MyFormComponent {
  form = new FormGroup({
    name: new FormControl(),
    email: new FormControl()
  } as PersonControls) as PersonFormGroup;

  init() {
    const name = this.form.controls.name; // strong typed!
  }
}
Circular answered 19/3, 2021 at 23:41 Comment(2)
I like this much more. Thanks!Passably
This is good; but I think I'm missing something - as is I can change the name declaration to const name = this.form.controls.HA and it complies; if I remove as PersonFormGroup it correctly errors...??Cuprite
B
19

2022 Update (Angular 14): Typed Reactive Forms

With the latest update of Angular, strict types for reactive forms have been implemented! and there is no need for a workaround.

If you are using formBuilder service for creating your forms you can assign a strict type to it like the following:

type myType = 'typeA' | 'typeB' | 'typeC';

public myForm = this.fb.control<myType>('typeA');

Ang if you prefer to create a formGroup regardless of formBuilder service, this is how you can achieve that:

interface User {
    name: FormControl<string>;
    email: FormControl<string>;
    isAdmin: FormControl<boolean>;
}

public user: FormGroup<User> = new FormGroup<User>({
    name: new FormControl('', {nonNullable: true}),
    email: new FormControl('', {nonNullable: true}),
    isAdmin: new FormControl(false, {nonNullable: true}),
});
Please keep in mind that if you do not specify the nonNullable attribute, you might get an error in your template about your value being null. especially when you want to bind your form control's value to something.

Angular will automatically check for type system even if you do not explicitly write down the type like the following:

const cat = new FormGroup({
   name: new FormGroup({
      first: new FormControl('Barb'),
      last: new FormControl('Smith'),
   }),
   lives: new FormControl(9),
});

// Type-checking for forms values!
// TS Error: Property 'substring' does not exist on type 'number'.
let remainingLives = cat.value.lives.substring(1);

// Optional and required controls are enforced!
// TS Error: No overload matches this call.
cat.removeControl('lives');

// FormGroups are aware of their child controls.
// name.middle is never on cat
let catMiddleName = cat.get('name.middle');

You can learn more about this at the Angular 14 release update on their blog here

Bilberry answered 12/6, 2022 at 5:2 Comment(1)
Fine. If you don't want to specify nonNullable attribute in component, you can specify correct data types in your model class as: interface User { name: FormControl<string | null>; }Seafarer
A
8

forms.ts

import { FormControl, FormGroup } from '@angular/forms';

export type ModelFormGroup<T> = FormGroup<{
  [K in keyof T]: FormControl<T[K] | null>;
}>;

login.ts

export interface Ilogin {
  username: string;
  password: string;
}

login.component.ts

loginForm!: ModelFormGroup<Ilogin>;

constructor() {
    this.loginForm = new FormGroup({
      username: new FormControl<string | null>(null, Validators.required),
      password: new FormControl<string | null>(null, Validators.required),
    });
}

Alike answered 30/3, 2023 at 1:46 Comment(0)
A
5

I had a similar issue and this was my solution. I really only cared about the type of the 'value' of the form not the form itself. It ended up looking something like this.

export interface UserFormValue {
  first_name: string
  last_name: string
  referral: string
  email: string
  password: string
}
...

ngOnInit() {
  this.userForm = this.fb.group({
    first_name: [ '', Validators.required ],
    last_name: [ '', Validators.required ],
    referral: [ '' ],
    email: [ '', [ Validators.required, Validators.email ] ],
    password: [ '', [ Validators.required, Validators.minLength(8) ] ],
  });
}

...

Then in the template submit the value

<form [formGroup]="userForm" (ngSubmit)="onSubmit(userForm.value)">
   ...
</form>

Now you can add a type to the submit function

onSubmit(userForm: UserFormValue) {
   ...
}

It's not perfect but has been good enough for my use cases. I really wish there was like this.

userForm: FormGroup<UserFormValue>

Asher answered 2/12, 2020 at 16:0 Comment(0)
O
4

Strictly typed forms are available as of Angular 14 (currently on the next channel)!

The FormGroup and FormArray classes accept a generic, which is the type of the inner controls. The FormControl class accepts a generic of its value type. There is also a new class called FormRecord, for dynamic groups of controls.

Here's an example:

const party = new FormGroup({
  address: new FormGroup({
    house: new FormControl(123, {initialValueIsDefault: true}),
    street: new FormControl('Powell St', {initialValueIsDefault: true}),
  }),
  formal: new FormControl(true),
  foodOptions: new FormArray([
    new FormControl('Soup'),
  ])
});

// whichHouse has type `number`
const whichHouse = party.get('address.house')!.value;

// Error: control "music" does not exist
const band = party.controls.music;
Obsessive answered 23/4, 2022 at 3:21 Comment(0)
L
2

if you have a nested type of groups then you can do sometihng like this:
**models.ts

export type TCreateUserFields = {
    first_name: string,
    last_name: string,
    accept_terms: boolean,
};
export type TPasswordsControls = {
    passwords: FormGroup & {
        password: AbstractControl,
        confirm_password: AbstractControl
    }
}
export type TPasswordsFields = {
    passwords: {
        password: string,
        confirm_password: string
    }
}
export type TAllFields = TCreateUserFields & TPasswordsFields;
export type TAllControls = TCreateUserControls & TPasswordsControls;
export type TCreateUserControls = {
    [key in keyof TCreateUserFields]: AbstractControl
};
export type TCreateUserFormGroup = FormGroup & {value: TAllFields, controls: TAllControls};

**component.ts
this.registerationForm = this.fb.group(
{
    first_name: new FormControl("", [Validators.required]),
    last_name: new FormControl("", [Validators.required]),
    accept_terms: new FormControl(false, [Validators.required]),
    passwords: new FormGroup(
        {
            password: new FormControl("", [Validators.required, Validators.pattern(/^[~`!@#$%^&*()_+=[\]\{}|;':",.\/<>?a-zA-Z0-9-]+$/)]),
            confirm_password: new FormControl("", [Validators.required, Validators.pattern(/^[~`!@#$%^&*()_+=[\]\{}|;':",.\/<>?a-zA-Z0-9-]+$/)]),
        }, { 
            validators: <ValidatorFn>pwdConfirming({key:'password', confirmationKey:'confirm_password'})
        }
    )
} as TCreateUserControls) as TCreateUserFormGroup;
Ligament answered 23/11, 2021 at 15:49 Comment(0)
P
1

The solution I ended up using was a library I found called ngx-strongly-typed-forms

It enables you to have strongly typed FormControls, FormGroups, and FormArrays. There are some limitations but it's definitely helped a lot in my project.

You can see the documentation at https://github.com/no0x9d/ngx-strongly-typed-forms

Plugboard answered 9/10, 2019 at 23:56 Comment(2)
I created an issue over a month ago and this doesn't seem to have gotten ANY response yet. From the looks of it, this project may have gone stale and I don't recommend anyone to use it!Sather
The (still unsolved) angular issue for missing type information is github.com/angular/angular/issues/13721. There are some stronger alternatives mentioned there (like @ng-stack/forms).Johann
M
0

As of Angular 14, reactive forms are strictly typed by default.

https://angular.io/guide/typed-forms

interface LoginForm {
    email: FormControl<string>;
    password: FormControl<string>;
}

const login = new FormGroup<LoginForm>({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});
Maziemazlack answered 11/6, 2022 at 22:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.