One of the most complex and repetitive task you will encounter when you're developing a Single Page Application is the creation of forms.
I love and use React, Svelte, SolidJS and several other modern JS frameworks but IMHO Angular is by far the best framework for managing several types of forms.
So, when you're choosing the front-end technology to use in your next project keep this in mind 😅
Angular offers two approaches:
• Template driven forms: based on template directives. It requires more or less zero JavaScript code.
• Reactive Forms: defined programmatically at the level of component class.
Although Reactive Forms represent the best choice for most applications, as they are more powerful and strongly typed, we'll use template driven forms because they are very simple to use and allow us to become familiar with the framework and its template system
In this recipe you will create a template-driven Angular form in order to add new users to the list.
FormsModule
In order to work with template-driven forms, you need to import FormsModule
in AppModule
(src/app/app.module.ts
):
FormsModule
is the collection of "utilities" and directives you need to import in order use Template Driven Forms in your components.
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms'; // NEW
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule // NEW
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Add a form just above the list (below the <h1>Users</h1>
.
I'll explain later how it works step by step:
app.component.ts
(in HTML template)<!--
<div class="container">
<h1>Users</h1>
-->
<form
class="card card-body mt-3"
#f="ngForm"
(submit)="saveHandler(f)"
>
<input
type="text"
ngModel
name="label"
placeholder="Add user name"
class="form-control"
required
>
<select
ngModel
name="gender"
class="form-control"
required
>
<option value="">Select option</option>
<option value="M">M</option>
<option value="F">F</option>
</select>
<button
class="btn btn-dark"
[disabled]="f.invalid"
>Save</button>
</form>
<hr>
<!--LIST BELOW: NO CHANGES HERE-->
<form #f="ngForm">
: this syntax creates the f
variable that is available in any part of the template. It holds the instance of the form that you can use to get its content (f.value
) and its status: f.valid
, f.dirty
, f.touched
and more...
In fact, it will be used to automatically disable the submit <button>
when form is invalid (since both, button
and select
, have a required
attribute
<button [disabled]="f.invalid">Save</button>
<input ngModel name="label"
:
ngModel
allow you to create an instance of the "form control" that will be available in the main form instance.
dirty
, touched
and so on.ngModel
is a directive and can be used with or without square brackets.
We usually use brackets when we want bind a state to a form control, i.e. [ngModel]="value"
but this is not the case.
We only want that form controls are recognized by Angular so we don't need them.
Anyway there is an important different: the default value of a form control is an empty string when we don't use brackets, while is null
when we use them.
(submit)="saveHandler(f)"
: invoke the method when the form is submitted passing the reference to the form as paremeter.
You can then get the value of each control using f.value
that returns an object whose keys are the name
attributes of each control:f.value
{
name: 'anything',
gender: 'M'
}
the text
input and the select
works more or less in the same way. The only difference is that the select
contains some default values.
<option value="">
: associate an empty string to the default select
option.
Anyway your code still doesn't work.
You still need to create the saveHandler
method in your app.component
class that will be invoked when the form is submited.
app.component.ts
saveHandler(f: NgForm) {
const user = f.value as User;
user.id = Date.now(); // create a fake ID (i.e. a timestamp)
this.users = [...this.users, user]
f.reset({gender: ''});
}
In the previous snippet we assigned a fake ID by using a timestamp with Date.now()
.
In the next recipes it will be generated by the server but it's temporarily useful to assign a different ID to each element (otherwise you cannot delete them since this operation needs an ID).
The default value of each form control is an empty string since we have used ngModel
without brackets.
However the reset
form method set all form control values to null
and there are no <option>
elements that handle it. So we must manually set the gender
value to an empty string in order to display the default <option>
when form is reset.
<input>
s form controls don't suffer of this issue because even when they are set to null
they simply don't display anything.
You also need to import NgForm
in AppComponent
in order to work:
app.component.ts
import { NgForm } from '@angular/forms';
Instead of using array spread operator [...this.users, value]
you may also push the new value this.users.push(user)
. See the previous chapter to know more about Immutability
Refresh the browser and try it on `http://localhost:4200``.
Here the completed source code of app.component
:
app.component.ts
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { User } from './model/user';
@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>Users</h1>
<form
class="card card-body mt-3"
#f="ngForm"
(submit)="saveHandler(f)"
>
<input
type="text"
ngModel
name="label"
placeholder="Add user name"
class="form-control" required>
<select
ngModel
name="gender"
class="form-control"
required
>
<option value="">Select option</option>
<option value="M">M</option>
<option value="F">F</option>
</select>
<button
class="btn btn-dark"
[disabled]="f.invalid"
>Save</button>
</form>
<hr>
<!--LIST BELOW: NO CHANGES HERE-->
<hr>
<ul class="list-group">
<li
*ngFor="let u of users" class="list-group-item"
[ngClass]="{
'male': u.gender === 'M',
'female': u.gender === 'F'
}"
>
<i
class="fa fa-3x"
[ngClass]="{
'fa-mars': u.gender === 'M',
'fa-venus': u.gender === 'F'
}"
></i>
{{u.label}}
<i class="fa fa-trash fa-2x pull-right" (click)="deleteHandler(u)"></i>
</li>
</ul>
</div>
`,
styles: [`
.male { background-color: #36caff; }
.female { background-color: pink; }
`]
})
export class AppComponent {
users: User[] = [
{ id: 1, label: 'Fabio', gender: 'M', age: 20 },
{ id: 2, label: 'Lorenzo', gender: 'M', age: 37 },
{ id: 3, label: 'Silvia', gender: 'F', age: 70 },
];
deleteHandler(userToRemove: User) {
this.users = this.users.filter(u => u.id !== userToRemove.id);
}
saveHandler(f: NgForm) {
const user = f.value as User;
user.id = Date.now(); // create a fake ID
this.users = [...this.users, user]
f.reset({gender: ''});
}
}
We simply want to apply a different background color to the form itself when user selects the gender from the select
:
Now we use ngClass
to display a blue or a pink background to the form in according to the gender selection:
So, update the following previous code:
app.component.ts
(in HTML template)<form
class="card card-body mt-2"
#f="ngForm"
(submit)="saveHandler(f)"
>
To:
app.component.ts
(in HTML template)<form
class="card card-body mt-2"
#f="ngForm"
(submit)="saveHandler(f)"
[ngClass]="{
'male': f.value.gender === 'M',
'female': f.value.gender === 'F'
}"
>
It should already works but we also want add a nice transition between colors.
So we can simple override the Bootstrap .card
CSS class adding the transition
property:
app.component.ts
(in styles
property)styles: [`
.male { background-color: #36caff; }
.female { background-color: pink; }
.card { transition: all 0.5s }
`]
We also want to apply a different color to the Submit Button:
valid
: the button should be greeninvalid
: the button should be redUse the ngClass
directive to apply a different CSS class to the "save" button when form is valid
or invalid:
From:
app.component.ts
(in HTML template)<button
class="btn"
[disabled]="f.invalid"
>Save</button>
To:
app.component.ts
(in HTML template)<button
class="btn"
[disabled]="f.invalid"
[ngClass]="{
'btn-success': f.valid,
'btn-danger': f.invalid
}"
>Save</button>
Currently we only know if the form itself is invalid
but we also would like to know which
of the form controls are invalid in order to display a different message for each one or,
just like in the following example, apply an error CSS class to them.
So, replace the following code:
app.component.ts
(in HTML template)<input
type="text"
ngModel
name="label"
placeholder="Add user name"
class="form-control" required>
<select
ngModel
name="gender"
class="form-control"
required
>
To the following one:
is-invalid
CSS class is provided by Bootstrap
app.component.ts
(in HTML template)<input
type="text"
ngModel
name="label"
placeholder="Add user name"
class="form-control"
required
#labelInput="ngModel"
[ngClass]="{'is-invalid': labelInput.invalid && f.dirty}"
>
<select
ngModel
name="gender"
class="form-control"
required
#genderInput="ngModel"
[ngClass]="{'is-invalid': genderInput.invalid && f.dirty}"
>
We assigned each ngModel
to a template reference variable that can be used to know if a form control is invalid
, dirty
, touched
and we can also know which errors
it contains
In the snippet below we apply the is-invalid
Bootstrap CSS class to the input
and the select
when they are invalid
and, at the same time, the whole form is dirty
.
In fact, I would see all the controls colored by red when the page is loaded if I had only checked if a form control is invalid .
But I want to show errors only when users starts to interact with the form (so it's dirty
)!
Refresh the browser and try it on http://localhost:4200
.
app.component.ts
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { User } from './model/user';
@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>Users</h1>
<form
class="card card-body mt-3"
#f="ngForm"
(submit)="saveHandler(f)"
[ngClass]="{
'male': f.value.gender === 'M',
'female': f.value.gender === 'F'
}"
>
<input
type="text"
ngModel
name="label"
placeholder="Add user name"
class="form-control"
required
#labelInput="ngModel"
[ngClass]="{'is-invalid': labelInput.invalid && f.dirty}"
>
<select
ngModel
name="gender"
class="form-control"
required
#genderInput="ngModel"
[ngClass]="{'is-invalid': genderInput.invalid && f.dirty}"
>
<option value="">Select option</option>
<option value="M">M</option>
<option value="F">F</option>
</select>
<button
class="btn"
[disabled]="f.invalid"
[ngClass]="{
'btn-success': f.valid,
'btn-danger': f.invalid
}"
>Save</button>
</form>
<hr>
<!--LIST BELOW: NO CHANGES HERE-->
<hr>
<ul class="list-group">
<li
*ngFor="let u of users" class="list-group-item"
[ngClass]="{
'male': u.gender === 'M',
'female': u.gender === 'F'
}"
>
<i
class="fa fa-3x"
[ngClass]="{
'fa-mars': u.gender === 'M',
'fa-venus': u.gender === 'F'
}"
></i>
{{u.label}}
<i class="fa fa-trash fa-2x pull-right" (click)="deleteHandler(u)"></i>
</li>
</ul>
</div>
`,
styles: [`
.male { background-color: #36caff; }
.female { background-color: pink; }
.card { transition: all 0.5s }
`]
})
export class AppComponent {
users: User[] = [
{ id: 1, label: 'Fabio', gender: 'M', age: 20 },
{ id: 2, label: 'Lorenzo', gender: 'M', age: 37 },
{ id: 3, label: 'Silvia', gender: 'F', age: 70 },
];
deleteHandler(userToRemove: User) {
this.users = this.users.filter(u => u.id !== userToRemove.id);
}
saveHandler(f: NgForm) {
const user = f.value as User;
user.id = Date.now(); // create a fake ID
this.users = [...this.users, user]
f.reset({gender: ''});
}
}