Create a simple CRUD app

Angular 15

Template Driven Forms

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 😅

2022-12-28
11 min read
Difficulty
angular
typescript

Template Driven Forms vs Reactive Forms

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

TOPICS

Goal

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.

AngularTypeScript
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 { }

Create the Form

Add a form just above the list (below the <h1>Users</h1>.
I'll explain later how it works step by step:

AngularHTML
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-->

How it works

  • <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.
    In this way, the form can know that these fields exists, get its values and know if they are dirty, touched and so on.

ngModel vs [ngModel]

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.

Add new User

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.

AngularTypeScript
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: ''});
}

Why a fake ID?

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).

Why `reset({gender: ''})` ?

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:

AngularTypeScript
app.component.ts
import { NgForm } from '@angular/forms';

JAVASCRIPT TIP

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

PREVIEW

Refresh the browser and try it on `http://localhost:4200``.

SOURCE CODE

Here the completed source code of app.component:

AngularTypeScript
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: ''});
  }
}

Play with CSS and Transitions

We simply want to apply a different background color to the form itself when user selects the gender from the select:

Dynamic Form Background

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:

AngularHTML
app.component.ts (in HTML template)
<form
  class="card card-body mt-2"
  #f="ngForm" 
  (submit)="saveHandler(f)"
>

To:

AngularHTML
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:

AngularCSS
app.component.ts (in styles property)
styles: [`
  .male { background-color: #36caff; }
  .female { background-color: pink; }
  .card { transition: all 0.5s }
`]

"SAVE" Button Color

We also want to apply a different color to the Submit Button:

  • When the form is valid: the button should be green
  • When the form is invalid: the button should be red

Use the ngClass directive to apply a different CSS class to the "save" button when form is valid or invalid:

From:

AngularHTML
app.component.ts (in HTML template)
<button
  class="btn"
  [disabled]="f.invalid"
>Save</button>

To:

AngularHTML
app.component.ts (in HTML template)
<button
  class="btn"
  [disabled]="f.invalid"
  [ngClass]="{
    'btn-success': f.valid,
    'btn-danger': f.invalid
  }"
>Save</button>

Input validations

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:

AngularHTML
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:

The is-invalid CSS class is provided by Bootstrap
AngularHTML
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}"
>

What do we did?

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

Why check if form is `dirty`?

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)!

TRY IT

Refresh the browser and try it on http://localhost:4200.

Final Source Code

AngularTypeScript
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: ''});
  }
}
WHAT'S NEXT

ADS: MY LATEST VIDEO COURSE <br />(italian only)ADS: MY LATEST VIDEO COURSE <br />(italian only)
ADS: MY LATEST VIDEO COURSE
(italian only)

Keep updated about latest content
videos, articles, tips and news
BETA