Skip to content

User Input and Forms in Angular

At some point, you’re going to want to collect some data from your users. That might be some text for a status update, their name and shipping address, a search term, a title for their todo list item, or anything else.

Whatever the data is, the user is going to be entering it into one of the templates in your application. We’ve already looked at a simple example of this using [(ngModel)], let’s revisit that now:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-home',
template: `
<p>I am the home page</p>
<input [(ngModel)]="firstName" (ngModelChange)="logChange()" />
`,
imports: [FormsModule],
})
export class HomeComponent {
firstName = 'Josh';
logChange() {
console.log(this.firstName);
}
}

I’ve extended this into a complete example now. I’ve also set up an event binding for (ngModelChange) so that you can see every time the user changes the input, that change is reflected on this.firstName.

Now we have access to some data that the user has supplied, and we can do whatever we like with it. If we wanted, we could bind to values in an object like this to get multiple values:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-home',
template: `
<p>I am the home page</p>
<input [(ngModel)]="formData.firstName" (ngModelChange)="logChange()" />
<input [(ngModel)]="formData.lastName" (ngModelChange)="logChange()" />
`,
imports: [FormsModule],
})
export class HomeComponent {
formData = {
firstName: '',
lastName: '',
};
logChange() {
console.log(this.formData);
}
}

This approach works quite well for simple forms, but can become more difficult for larger/more complex forms if implemented naively. We might want to include validations on the forms to enforce certain values, react to value changes, dynamically add/remove fields, and more. The more popular approach generally is to use ReactiveForms.

I emphasised generally because there are proponents of template driven forms that rely on using ngModel even for more complex form situations. Proponents of this approach include industry veterans who have a great deal of knowledge and experience. This is not an approach I have ever used for any substantial amount of time so I can’t speak too much to its benefits or downsides, but it is certainly a valid architectural approach and definitely not something that should be dismissed out of hand as just something for “simple” forms.

Nonetheless, we will be focusing on just one approach primarily: ReactiveForms.

Reactive Forms

To use ReactiveForms we will need to replace the FormsModule in our imports with ReactiveFormsModule. The FormsModule is what provides us with ngModel and it is possible to include both but, generally, if we are using ReactiveForms we won’t be using ngModel.

The general idea behind a reactive form is that it is made up of a FormGroup (or multiple) and within that group will be multiple FormControl instances. The group collects things together, and the individual controls are tied to a specific input. In this way, we could apply validations to a specific control and check if it is valid, and we could also apply validations to an entire group and check if the entire group is valid.

We might create a reactive form like this:

import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-home',
template: `
<form [formGroup]="myForm">
<input formControlName="firstName" type="text" />
<input formControlName="lastName" type="text" />
</form>
`,
imports: [ReactiveFormsModule],
})
export class HomeComponent {
myForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
});
}

Notice that we give names to our group and controls, and then we reference those in the template with the formGroup and formControlName properties.

What we have just done is fine, although you may find it weird we are manually creating new instances of FormGroup and FormControl with the new keyword whereas we would typically use dependency injection. There isn’t anything wrong with creating a form this way, but it does lead to quite a bit of unnecessary typing, which is where the FormBuilder comes into the picture.

Form Builder

We are going to do exactly what we just did above, except we are going to use the FormBuilder this time. The ReactiveFormsModule provides this for us, and we can inject it into our component:

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-home',
template: `
<form [formGroup]="myForm">
<input formControlName="firstName" type="text" />
<input formControlName="lastName" type="text" />
</form>
`,
imports: [ReactiveFormsModule],
})
export class HomeComponent {
private fb = inject(FormBuilder);
myForm = this.fb.group({
firstName: [''],
lastName: [''],
});
}

It’s the same general idea, except we are using the methods of the FormBuilder instance to create our groups and controls for us. There is one method of FormBuilder that we have not mentioned and that is array (we can also manually create a form array with new FormArray() instead of using the array method). This is great for situations with dynamic forms as we can easily add or remove controls from the array, but this is a slightly more advanced use case.

Submitting a Form

We have our form, the user can supply it with data, but we still want to be able to do something with that data. There are multiple ways we can go about this, but the most standard way is to use a standard submit button inside of our form and bind to the (ngSubmit) event:

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-home',
template: `
<form [formGroup]="myForm" (ngSubmit)="handleSubmit()">
<input formControlName="firstName" type="text" />
<input formControlName="lastName" type="text" />
<button type="submit">Submit</button>
</form>
`,
imports: [ReactiveFormsModule],
})
export class HomeComponent {
private fb = inject(FormBuilder);
myForm = this.fb.group({
firstName: [''],
lastName: [''],
});
handleSubmit() {
console.log(this.myForm.value);
}
}

Now when the submit button is clicked it will trigger out handleSubmit method, and we can get the value of the form using our myForm class member.

Adding Validations

This is a topic we will explore in more depth later, but let’s just see how to add some basic validations and check if a form is valid. We can supply some additional configurations to our FormGroup to specify the validators we want to use:

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-home',
template: `
<form [formGroup]="myForm" (ngSubmit)="handleSubmit()">
<input formControlName="firstName" type="text" />
<input formControlName="lastName" type="text" />
<button type="submit">Submit</button>
</form>
`,
imports: [ReactiveFormsModule],
})
export class HomeComponent {
private fb = inject(FormBuilder);
myForm = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', [Validators.minLength(20), Validators.required]],
});
handleSubmit() {
console.log(this.myForm.value);
console.log(this.myForm.valid);
}
}

We can supply either a single validator, or an array of validators. In this case, we are making both fields required and our lastName field also has a minimum length of 20 characters (an absurd validation obviously).

We can use any of the default validators Angular supplies through Validators, but we can also write our own custom validators if we want. This is more advanced but it is something we do in the chat application later in this course.

When the form is submitted we will now be able to see the value as well as if the form is valid or not. If any of the validators fail in our form, then this.myForm.valid will be false. We can then do whatever we like with this information — generally, we would display error messages and not process the data if it is invalid.

Forms can get quite tricky to work with quite quickly — making the form manageable from a code perspective, and also have it provide a nice user experience is a challenge. We explore this area a lot more in the chat application.

Recap

    What are the two form modules we can import from Angular?

    The three main types of form controls we can create are…

    What is the correct way to make a field required using FormBuilder?

    What property of a form group can we use to check if all the validators have passed?

    What event can we bind to on a form element to handle a submission?