Angular 14 - Typed Forms

Introduction

Angular 14 was released in the month of June 2022 and it has some cool enhancements. The features include typed forms, standalone components, page title handling using routes and related patterns, better diagnostics and error handling features, and CLI improvements. We are going to cover one of these cool features, Typed forms. This feature has been introduced to improve the developer experience and provide uniformity in the way we develop forms in Angular.

What are Typed Forms?

Typed forms enforce a model through which we can access the properties in a uniform way for Angular forms. In the concept of typed form we have the property name and its data type defined. This was not possible in older versions of angular. Prior to Angular 14, we used a loosely typed forms model. To access the forms property we have to pass the field name. If the field used to exist we used to get the response value. The forms model was of type ‘any’. We could pass any invalid form property name and it would throw any error after that when we invoked it. This concept is not complex, it simply means we know what we are dealing with like field names and their data types and that’s it. This feature has just been released in Angular 14 and is only limited to Reactive forms and we cannot control bindings using directives. It is expected to have some improvements in upcoming releases.

Setting up Project

Download Angular CLI version 14

npm install -g @angular/cli

Check the angular version to make sure it's Angular 14.

ng version

Create angular application

ng new ng14-demo

Select your preferences for routing and CSS

Create new component

cd ng14-demo/src/app
ng g c typed-contact-form

Code walkthrough

app.module.ts

@NgModule({
  declarations: [
    AppComponent,
    TypedContactFormComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule //Import reactive forms module
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

contactFormGroup.ts

import { FormControl } from "@angular/forms";

//Interface that will contain strongly typed definition for form 
export interface contactFormGroup
{
  name: FormControl<string>;
  email: FormControl<string>;
  contactNumber?: FormControl<number|null>; //? makes controls as optional
  query?: FormControl<string|null>;//? makes controls as optional
}

typed-contact-form.component.ts

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { contactFormGroup } from  './contactFormGroup';

@Component({
  selector: 'app-typed-contact-form',
  templateUrl: './typed-contact-form.component.html',
  styleUrls: ['./typed-contact-form.component.css']
})
export class TypedContactFormComponent implements OnInit {
  contactForm = new FormGroup<contactFormGroup>({
    name: new FormControl<string>('', { nonNullable: true }),
    email: new FormControl<string>('', { nonNullable: true}),
    contactNumber: new FormControl<number>(0, { nonNullable: false}),
    query:new FormControl<string>('I would like to connect!', { nonNullable: false })
  });
  dataOutput: string = '';
  constructor() {
   }
  ngOnInit(): void {
  }
  onSubmitContactForm(){
    this.dataOutput = `Name: ${this.contactForm.value.name},Query: ${this.contactForm.value.query},Contact Number: ${this.contactForm.value.contactNumber}, Email: ${this.contactForm.value.email} `;
  }
  resetSubmitContactForm(){
    this.contactForm.reset();
    this.dataOutput = '';
  }
  removeQuery(){
    this.contactForm.removeControl('query'); //This code removes the optional control from typed model
  }
}

typed-contact-form-component.html

<form (ngSubmit)="onSubmitContactForm()" [formGroup]="contactForm">
  <label for="name">Name: </label>
  <input id="name" type="text" formControlName="name">
  <br />
  <label for="email">Email: </label>
  <input id="email" type="text" formControlName="email">
  <br />
  <label for="contactNumber">Contact Number: </label>
  <input id="contactNumber" type="text" formControlName="contactNumber">
  <br />
  <label for="query">Query: </label>
  <textarea id="query" type="text" formControlName="query"></textarea>
  <br />
  <button type="submit">Submit</button>
  <button (click)="resetSubmitContactForm()" type="reset">Reset</button>
</form>
<button (click)="removeQuery()">Remove Query</button>
<p [innerHTML]="dataOutput"></p>

On Submit,

On Remove Query and Submit,

Concept of Nullable feature

When we will reset the form, it will either assign null value or default value based on our nonNullable option value, 

Code Snippet On <formGroup>.reset()
/<formControl>.reset()
new FormControl<string>('I would like to connect!', { nonNullable: false }) Input will become null
new FormControl<string>('I would like to connect!', { nonNullable: true}) Input will become ‘I would like to connect!’

Removing Controls

We can control the behavior of typed form controls such as which part of the model to remove from the formGroup model. Any field marked as ? interface can be removed using “this.contactForm.removeControl('<controlName>');”. This is enforced by typescript. Below is the table for more details.

Code Snippet Can I remove? Outcome
query?: FormControl<string|null>; Yes Value becomes undefined
email: FormControl<string>; No Exception at compile time
No overload matches this call.
 Overload 1 of 2, '(this: FormGroup<{ [key: string]: AbstractControl<any, any>; }>, name: string, options?: { emitEvent?: boolean | undefined; } | undefined): void', gave the following error.
The 'this' context of type 'FormGroup<contactFormGroup>' is not assignable to method's 'this' of type 'FormGroup<{ [key: string]: AbstractControl<any, any>; }>'.
Types of property 'controls' are incompatible.
Type 'contactFormGroup' is not assignable to type '{ [key: string]: AbstractControl<any, any>; }'.
 Overload 2 of 2, '(name: never, options?: { emitEvent?: boolean | undefined; } | undefined): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'never'.

To explore more please refer to Angular official docs, https://angular.io/guide/typed-forms

For more details about Goals and Non-Goals of this feature refer to Angular RFC at Github, https://github.com/angular/angular/discussions/44513

Thanks for reading. I hope you found this useful and will be using it in new angular applications or will soon migrate to this new feature if used in older applications. I have uploaded code for reference please feel free to explore at your own pace.


Similar Articles