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.