Editor’s note: This article was last updated on 6 December 2023 to include sections about handling asynchronous operations with form controls, validating form controls, and creating custom form controls.
If you’re making a web app, sooner or later, you’re going to need to capture information from your user. Fortunately, reactive forms in Angular allow you to create clean forms without using too many directives. They also reduce the need for end-to-end testing as it’s very easy to validate your forms.
Put simply, form controls in Angular give the developer all the control, and nothing is implicit anymore — every choice about inputs and controls must be made intentionally and explicitly.
In this tutorial, we’ll show you how to divide form controls by form groups to create clusters that provide a platform to easily access the template element as groups. To illustrate the concept of form groups in Angular, we’ll walk through the process of building a reactive form so that you can fully understand how to set it up.
To follow along, download the starter project on GitHub and open it in VS Code. If you haven’t already, you’ll want to update to the most recent version, Angular 17.
In Angular, form controls are classes that can hold both the data values and the validation information of any form element. Every form input you have in a reactive form should be bound by a form control. These are the basic units that make up reactive forms.
Form groups wrap a collection of form controls. Just as the control gives you access to the state of an element, the group gives you the same access but to the state of the wrapped controls. Every single form control in the form group is connected to the appropriate form control in the component code.
FormControl
and FormGroup
in AngularFormControl
is a class in Angular that tracks the value and validation status of an individual form control. One of the three essential building blocks in Angular forms — along with FormGroup
and FormArray
— FormControl
extends the AbstractControl
class, which enables it to access the value, validation status, user interactions, and events.
FormGroup
is used with FormControl
to track the value and validate the state of form control. In practice, FormGroup
aggregates the values of each child FormControl
into a single object, using each control name as the key. It calculates its status by reducing the status values of its children so that if one control in a group is invalid, the entire group is rendered invalid.
Before Angular 14, whenever you had a FormControl
, you weren’t able to specify the type for the control. This could lead to confusion at runtime over what type was expected to be in a form.
However, since Angular 14, FormControl
s are strongly typed by default. That means when you are declaring a FormControl
, you can specify the type that you expect in the given FormControl
. We’ll demonstrate this when we create our forms in a moment.
The first step in registering form groups is to tell Angular that you want to use the form group by importing it inside the appropriate component.
To see how this works, navigate to the employee.component.ts
file and paste in the code block below:
import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms' @Component({ selector: 'app-employee', templateUrl: './employee.component.html', styleUrls: ['./employee.component.css'] }) export class EmployeeComponent implements OnInit { bioSection = new FormGroup({ firstName: new FormControl<string>(''), lastName: new FormControl<string>(''), age: new FormControl<number>(null) }); constructor() { } ngOnInit() { } }
Here, the form group was both imported and initialized to group together some form controls that compose the bio section of the form. To reflect this group, you have to associate the model to the view with the form group name, like this:
<form [formGroup]="bioSection"> <label> First Name: <input type="text" [formControl]="bioSection.controls.firstName" > </label> <label> Last Name: <input type="text" [formControl]="bioSection.controls.lastName"> </label> <label> Age: <input type="text" [formControl]="bioSection.controls.age"> </label> <button type="submit">Submit Application</button> </form>
We refer to the FormGroup
that we created in code, and then directly specify what control to use in the FormControl
directive.
Your app.component.html
file should look like this:
<div style="text-align:center"> <h2>Angular Job Board </h2> <app-employee></app-employee> </div>
Now run your application in development with this command:
ng serve
It should look like this:
Having a form is useful, but if we can’t be sure of the quality of the data that we’ll receive, we open ourselves up to possibly having entered data that doesn’t make sense or isn’t valid given the context.
Angular comes with an array of validators, so we don’t have to make our own for many common scenarios. In this situation, let’s validate that the entered first name and last name are at least three characters long, and the entered age is greater than 18:
bioSection = new FormGroup({ firstName: new FormControl<string>('', [ Validators.minLength(3), Validators.required ]), lastName: new FormControl<string>('', [Validators.minLength(3), Validators.required]), age: new FormControl<number>(null, [Validators.min(18), Validators.required]) });
Now, we need to update our HTML template to show what validation errors are on the form, and we also want to prevent submission of the form unless the validation issues are resolved. Our updated form disables the submission button unless the form is valid, and gives feedback to the user to explain what is wrong with the form:
<form [formGroup]="bioSection" style="display: flex; flex-direction: column"> <label> First Name: <input type="text" [formControl]="bioSection.controls.firstName"> </label> <label> Last Name: <input type="text" [formControl]="bioSection.controls.lastName"> </label> <label> Age: <input type="number" [formControl]="bioSection.controls.age"> </label> <!-- The disabled attribute controls prevents submission if the form is not valid --> <button type="submit" [disabled]="!bioSection.valid">Submit Application</button> </form> <div style="width: 100%;"> @if (!bioSection.valid){ <!-- If the form is not valid... --> <div style="width: 300px; margin: auto; padding: 20px; background-color: palegoldenrod"> <b>Validation errors</b> <ol> <!-- For each control, check that the given parameters have been met--> @if (bioSection.controls.firstName.errors?.['minlength'] ?? bioSection.controls.firstName.errors?.['required']){ <li>The first name is not long enough, or not specified.</li> } @if (bioSection.controls.lastName.errors?.['minlength'] ?? bioSection.controls.lastName.errors?.['required']){ <li>The last name is not long enough, or not specified.</li> } @if (bioSection.controls.age.errors?.['min'] ?? bioSection.controls.age.errors?.['required']){ <li>You must be over 18 to sign up</li> } </ol> </div> } </div>
The result of this is a form that only accepts valid input and rejects invalid input:
Angular has many useful controls that are easily added to forms via FormControl
. But, sometimes you need to use a specific type of control in your form that isn’t included out of the box in Angular.
Fortunately, it’s quite simple to create our own form control that we can then re-use throughout the rest of our application. For our purposes, let’s create a stepper FormControl
.
First, let’s create the custom-stepper
component. Execute ng generate component CustomStepper
to have the Angular CLI generate the appropriate component.
Next, let’s design a simple stepper in the HTML markup. At its core, we just want an input box, with a plus and minus button on either side for the user to increase or decrease the number in the box. Also, if the user directly enters the value into the box, we want to notify the form that the value has updated:
<div class="stepper"> <button (click)="decrement()">-</button> <input type="number" [(ngModel)]="value" (input)="updateValue($event)" /> <button (click)="increment()">+</button> </div>
Let’s quickly review what’s happening here:
input
element is bound to the value
property in the component. When it changes, updateValue
is called with the new valueNow, let’s dive into the component. First, we need to tell Angular that this component provides a ControlValueAccessor
so it can appropriately use and register the form control within this component. Update the providers
array to include this information:
@Component({ selector: 'app-custom-stepper', templateUrl: './custom-stepper.component.html', styleUrls: ['./custom-stepper.component.css'], providers: [ // Include the NG_VALUE_ACCESSOR { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomStepperComponent), multi: true, }, ], })
Next, we need to implement ControlValueAccessor
on this component:
export class CustomStepperComponent implements ControlValueAccessor { // ...component code... }
This requires the implementation of three functions:
writeValue(value)
: Writes a new value to the form control, which is controlled by the current form control. If the form is updated programmatically, this function will be executed to make the custom field reflect the value in the modelregisterOnChange(function)
: Sets up a callback function that is called when the value changes within the UI. This function will ultimately propagate the new value within the form to the underlying data model for the formregisterOnTouched(function)
: Sets up a function that is called when the form control loses focus. Again, will propagate the new value to the underlying data model for the formsetDisabledState(boolean)
(Optional): Will be called when the control is disabled in the form itself. If false
is passed, the control should be made inoperable to the user, while true
should make the control active againAs well as this functionality, we also need to implement the code that is native to the stepper itself, such as increasing and decreasing numbers when the plus or minus buttons are pressed. Once we have done this, our component code will look like the following:
import { Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-custom-stepper', templateUrl: './custom-stepper.component.html', styleUrls: ['./custom-stepper.component.css'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomStepperComponent), multi: true, }, ], }) export class CustomStepperComponent implements ControlValueAccessor { value: number | null = null; onChange: any = () => {}; onTouch: any = () => {}; // -- ControlValueAccessor implementation -- // Called when the form data model is updated programmatically writeValue(value: number): void { this.value = value; } // Called when the form control is updated by the user registerOnChange(fn: any): void { this.onChange = fn; } // Called when the form control loses focus registerOnTouched(fn: any): void { this.onTouch = fn; } // Called when the form control is disabled in code setDisabledState(isDisabled: boolean): void { // You can implement this method if your control needs to be disabled } // -- Internal Stepper Implementation -- // Increase stepper by one increment(): void { // Increments the value by 1 if it is not null, otherwise sets the value to 1. this.updateValue(this.value !== null ? this.value + 1 : 1); } // Decrease stepper by one decrement(): void { this.updateValue(this.value !== null ? this.value - 1 : -1); } // Handles setting the new value and notifying the form of the change updateValue(newValue: number | null): void { if (newValue !== this.value) { this.value = newValue; this.onChange(newValue); this.onTouch(); } } }
Finally, within employee.component.html
, we can add a reference to our custom control. We’ll also need to add a yearsExperience
property to our FormGroup
in the component code:
<label> Years experience: <app-custom-stepper [formControl]="bioSection.controls.yearsExperience"></app-custom-stepper> </label>
The result is our brand new stepper control being available in the UI:
Now would be a good time to apply some styling so our UI doesn’t look so chaotic. Apply the following rules in the employee.component.css
file:
You can style your component with the style instructions below: input[type=text] { width: 30%; padding: 8px 14px; margin: 2px; box-sizing: border-box; } button { font-size: 12px; margin: 2px; padding: 8px 14px; }
The Angular reactive forms API makes it possible to nest a form group inside another form group. Copy the code block below into the employee.component.ts
file:
import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms' @Component({ selector: 'app-employee', templateUrl: './employee.component.html', styleUrls: ['./employee.component.css'] }) export class EmployeeComponent implements OnInit { bioSection = new FormGroup({ firstName: new FormControl<string>('', [ Validators.minLength(3), Validators.required ]), lastName: new FormControl<string>('', [Validators.minLength(3), Validators.required]), age: new FormControl<number>(null, [Validators.min(18), Validators.required]), yearsExperience: new FormControl<number>(0), stackDetails: new FormGroup({ stack: new FormControl<string>(''), experience: new FormControl<string>('') }), address: new FormGroup({ country: new FormControl<string>(''), city: new FormControl<string>('') }) }); constructor() { } ngOnInit() { } callingFunction() { console.log(this.bioSection.value); } }
The main form group wrapper is the bio section, inside of which both the stack details group and the address group are nested. As you can see, nested form groups are not defined by the assignment statement, but rather with the colon, just like a form control.
Reflecting this in the view will look like this:
// copy inside the employee.component.html file <form [formGroup]="bioSection" (ngSubmit)="callingFunction()"> <h3>Bio Details </h3> <label> First Name: <input type="text" formControlName="firstName"> </label> <br> <label> Last Name: <input type="text" formControlName="lastName"> </label> <br> <label> Age: <input type="text" formControlName="age"> </label> <div [formGroup]="bioSection.controls.stackDetails"> <h3>Stack Details</h3> <label> Stack: <input type="text" formControlName="stack" [formControl]="bioSection.controls.stackDetails.controls.stack"> </label> <br> <label> Experience: <input type="text" formControlName="experience" [formControl]="bioSection.controls.stackDetails.controls.experience"> </label> </div> <div [formGroup]="bioSection.controls.address"> <h3>Address</h3> <label> Country: <input type="text" [formControl]="bioSection.controls.address.controls.country"> </label> <br> <label> City: <input type="text" formControlName="city" [formControl]="bioSection.controls.address.controls.city"> </label> </div> <button type="submit">Submit Application</button> </form>
If you run the application, you should see something like this in your browser:
Occasionally, you may not know all of the data that you want to present to the user when you build the application. You might want to perform an asynchronous operation to retrieve the data, such as retrieving it from an API.
Fortunately, that’s a fairly easy thing to do within Angular forms. But first, let’s set up some data for us to use within our sample app. Within our employee.component.ts
file, let’s specify a long list of programming languages:
programmingLanguages = [ 'JavaScript', 'Python', 'Java', 'C#', 'C++', 'Ruby', 'Swift', 'Kotlin', 'TypeScript', 'HTML', 'CSS', 'PHP', 'Go', 'Rust', 'Objective-C', 'Scala', 'Shell', 'PowerShell', 'Perl', 'Lua', 'Haskell', 'Dart', 'Groovy', 'R', 'MATLAB', 'CoffeeScript', 'F#', 'Clojure', 'Elixir', 'Julia', 'Haxe', 'Fortran', 'Ada', 'COBOL', 'Lisp', 'Scheme', 'Prolog', 'Bash', 'Assembly', 'Smalltalk', 'Erlang', 'OCaml', 'VHDL', 'Verilog', 'PL/I', 'Ada', 'ABAP', 'ActionScript' ];
Next, we need to specify a couple of interfaces. One is to capture the current “state” that our query is in, and also to contain a list of languages:
export interface LanguageResponse { state: LanguageResponseState, languages: Array<string> } export enum LanguageResponseState { EMPTY, LOADING, LOADED, }
Now, let’s use a signal to handle the asynchronous data within our template. When we initialize the signal, it will be in the EMPTY
state as no data would have been loaded into it at that time:
filteredLanguages = signal<LanguageResponse>({state: LanguageResponseState.EMPTY, languages: []});
Within our ngOnInit()
function, we want to subscribe to the values that the FormGroup
emits. However, we don’t want to hit our API every single time someone enters a new key in the form.
Instead, we only want to run the search when the form has settled down. We can achieve that by subscribing to the FormGroup
, but piping it through a debounce
to ensure that the search runs half a second after the form finishes being updated:
ngOnInit() { this.bioSection.valueChanges.pipe(debounceTime(500)).subscribe(async x => { if (x.languageSearch){ // if a language is specified await this.searchForLanguage(x.languageSearch) // search for language } else{ // if no language is being searched for, reset back to "empty" state this.filteredLanguages.set({ state: LanguageResponseState.EMPTY, languages: [] }) } }) } async searchForLanguage(search: string) { // set signal to "loading" state this.filteredLanguages.set({state: LanguageResponseState.LOADING, languages: []}); // wait 2 seconds to simulate a web API call await new Promise(resolve => setTimeout(resolve, 2000)); // set results to matching languages this.filteredLanguages.set({ state: LanguageResponseState.LOADED, languages: this.programmingLanguages.filter(x => x.toLowerCase().indexOf(search.toLowerCase()) !== -1) }); } }
Now, we’ll need a search box and a list to select languages from in our HTML document. Fortunately, the new control flow syntax in Angular 17 makes this very easy:
<label> Language: <input [formControl]="bioSection.controls.languageSearch" > </label> @if (filteredLanguages().state === response.EMPTY){ Please specify a language to search for. } @if (filteredLanguages().state === response.LOADING){ Loading languages... } @else if (filteredLanguages().state === response.LOADED){ <select size="5"> @for (language of filteredLanguages().languages; track language) { <option>{{language}}</option> } @empty { <p>No languages match...</p> } </select> }
This is the result:
When you use the form and submit it, you will see your input results returned in the browser console. The code used in this tutorial is available on GitHub.
FormControl
to a FormGroup
To add, update, or remove controls in FormGroup
, use the following commands:
addControl()
: Adds a control and updates its value and validityremoveControl()
: Removes a controlsetControl()
: Replaces an existing controlcontains()
: Checks for enabled controls associated with a given nameregisterControl()
: Registers a control but, unlike the other methods, does not update its value and validityFormGroup
valueIn Angular, you can set values to individual form groups or set all FormGroup
values at once. Use patchValue
to set only some values:
this.myFormGroup.patchValue({ formControlName1: myValue1, // formControlName2: myValue2 });
You don’t need to supply all values here; fields whose values were not set will be unaffected. To set all FormGroup
values simultaneously, use setValue
:
this.myFormGroup.setValue({ formControlName1: myValue1, formControlName2: myValue2 });
FormBuilder
in Angular?Setting up form controls can be tedious, especially if you’re working with very long forms. Angular’s FormBuilder
helps you streamline the process of building advanced forms while avoiding repetition.
Put simply, FormBuilder
provides syntactic sugar that eases the burden of creating instances of FormControl
, FormGroup
, or FormArray
and reduces the amount of boilerplate required to build complex forms.
For a deeper dive and examples of how to build complex forms, check out this Angular FormBuilder
tutorial.
In this tutorial, we covered everything you need to know about form controls in Angular, including how to use FormControl
, how to group form controls with FormGroup
, and why it’s critical to capture controls’ collective instances at once.
Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.
Modernize how you debug your Angular apps — start monitoring for free.
Radix UI is quickly rising in popularity and has become an excellent go-to solution for building modern design systems and websites.
In this article, we’ll explore CSS cascade layers — and, specifically, the revert-layer
keyword — to help you refine your styling strategy.
Nushell is a modern, performant, extensible shell built with Rust. Explore its pros, cons, and how to install and get started with it.
The Zed code editor sets itself apart with its lightning-fast performance and cutting-edge collaborative features.
One Reply to "FormGroup and FormControl in Angular"
how to write error message for a control of nested form ?
*ngIf=”parentForm.controls.controlitem.errors”