¿Qué aprenderá?

Al finalizar este tutorial el estudiante estará en capacidad de implementar un formulario básico en Angular para crear un recurso de la aplicación.

¿Qué construirá?

En este tutorial vamos a crear una aplicación Angular con un componente para crear un formulario.

Específicamente, utilizará la librería Angular de formularios reactivos (Reactive Forms) para definir un formulario en un componente de creación de un recurso. Este formulario tendrá la definición de los elementos que se quieren capturar en la interacción con el usuario así como sus validaciones básicas. Igualmente, utilizará el formulario en el template HTML y desplegará mensajes de error y mensaje de confirmación cuando el usuario hace un submit del formulario.

El resultado del tutorial es una aplicación que tiene un formulario básico (ver Figura 1). Hay tres campos:

Figura 1. Formulario de creación de cliente

¿Qué necesita?

Para realizar este taller Ud. debe tener claro:

  1. La estructura de un proyecto de Angular: módulos, componentes, servicios.
  2. La funcionalidad del Angular-Cli.

Crear el proyecto

Utilizando Angular-cli desde VSCode cree un nuevo proyecto Angular.

Crear el módulo nuevo

Sobre la carpeta app de su proyecto seleccione la opción de Angular Generator (angular-cli) y allí Módulo. Defina el nombre de su módulo como client.

Asociar el nuevo módulo en la aplicación principal

Vaya al archivo app.module.ts y agregue el nuevo módulo. Debe hacer dos cosas:

Crear el componente dentro del módulo

Vaya a la carpeta del módulo client, clic derecho Angular Generator/Componente.

Dele el nombre client-create.

Asociar el componente con el módulo

Angular-cli crea la declaración del componente en el módulo. Sin embargo, se debe agregar una línea con el atributo exports para que este componente se pueda ver desde la aplicación principal.

Importar en el módulo la librería de formularios

La librería que vamos a utilizar para manejar los formularios se llama ReactiveFormsModule y se debe incluir e importar en el módulo donde se definen componentes que tendrán formularios. En este tutorial, nuestro módulo es ClientModule.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { ClientCreateComponent } from './client-create/client-create.component';

@NgModule({
  imports: [
    CommonModule, ReactiveFormsModule
  ],
  declarations: [ClientCreateComponent],
  exports:[ClientCreateComponent]
})
export class ClientModule { }

Vamos a llamar el componente de crear un cliente utilizando su selector desde el componente principal.

En el archivo del componente principal app.component.html borre lo que hay y pegue el selector entre tags de apertura y cierre, tal como se ve en la Figura 2.

Figura 2. Componente principal

Angular cuenta con librerías para manejar los formularios. Estas librerías permiten crear objetos que contienen los campos en el formulario y que serán desplegados en la vista. La librería se encarga de mantener la relación entre los campos de entrada (input) que despliega la vista y los datos en el componente.

Estas librerías debemos importarlas en el componente de creación del recurso.

import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
...

En nuestro ejemplo, vamos a implementar un formulario para crear un nuevo cliente. En el siguiente código del componente vamos a declarar una variable clientForm de tipo FormGroup. También vamos a incluir en el constructor una variable formBuilder de tipo FormBuilder y una variable toastr de tipo ToastrService.

import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ToastrService } from "ngx-toastr";
import { Client } from '../client';


@Component({
  selector: "app-client-create",
  templateUrl: "./client-create.component.html",
  styleUrls: ["./client-create.component.css"]
})
export class ClientCreateComponent implements OnInit {
  clientForm: FormGroup;
  constructor(private formBuilder: FormBuilder,
    private toastr: ToastrService 
) {}
...
}

Un objeto formulario es de tipo FormGroup y contiene un conjunto de objetos FormControl donde cada uno representa un campo del formulario para que el usuario ingrese valores.

Vamos a inicializar la forma clientForm con los elementos que la componen. Esto lo hacemos en el método ngOnInit del componente. Por cada campo de entrada, definimos un nombre y sus propiedades de validación si es que las tiene:

 ngOnInit() {
    this.clientForm = this.formBuilder.group({
      name: ["", [Validators.required, Validators.minLength(2)]],
      address: ["", Validators.required],
      email: ["", [Validators.required, Validators.email]]
    });

En el formulario clientForm, estamos definiendo tres campos:

La validaciones significan que si cualquiera de esas validaciones no se cumple, por ejemplo, el campo address no tiene valor, el formulario es inválido. En este ejemplo solo estamos utilizando validaciones predefinidas (ver enlace https://angular.io/api/forms/Validators) . También podemos definir nuestras propias validaciones (ver enlace https://angular.io/guide/form-validation#custom-validators) .

La vista del componente, es decir el código HTML, va en el archivo client-create.component.html.

El formulario que vamos a construir es el que se detalla en la Figura 3.

Figura 3. Ejemplo de formulario que se va a construir

Este formulario es un elemento HTML form que contiene los tres elementos inputs con sus respectivos label y el código para desplegar los mensajes de error debajo de cada elemento, si no se cumplen las reglas de validación. Antes de cerrar la etiqueta form, se incluye tanto el botón de submit para crear el cliente, como el botón de cancel para cancelar el formulario. Queremos, además, que el botón de submit esté activo sólo si el formulario es válido.

Vamos a explicar el HTML gradualmente sin detenernos en el detalle de los estilos. La Figura 4 muestra la organización del HTML. Existe una etiqueta form que contiene, por cada campo en el formulario, un grupo de etiquetas para el despliegue y las validaciones. Al final está el código para mostrar los botones.

Figura 4. Organización del HTML

Etiqueta form

La etiqueta form contiene la referencia al modelo del formulario, es decir, a la variable clientForm que se creó en la clase del componente. También contiene la directiva Angular ngSubmit que tiene por valor la expresión que será evaluada cuando el usuario hace clic en el botón de submit. En este caso, preguntamos si el formulario es válido y se llama la función createClient(clientForm.value). Esta función debemos definirla en la clase del componente. El detalle de este código se muestra a continuación:

<form [formGroup]="clientForm" (ngSubmit)="!clientForm.invalid && createClient(clientForm.value)">
...  
</form>

Create y Cancel

Veamos ahora el código asociado con los botones que van antes de cerrar la etiqueta

</form>.

El botón para crear el cliente, está deshabilitado si el formulario no es válido, lo que se representa con el código [disabled]="!clientForm.valid".

Ese botón es de tipo type="submit" lo que implica que cuando el usuario le hace clic se ejecutará el ngSubmit del formulario. En este caso se llamará a crear el cliente con los valores recolectados en los campos que se encuentran en clientForm.value.

El botón de cancelar tiene una directiva denominada click que invoca la función cancelCreation() que también debe estar definida en la clase del componente.

<button type="submit" class="btn btn-primary" [disabled]="!clientForm.valid">Create</button>                
                <button type="button" class="btn btn-danger ml-3" (click)="cancelCreation()">Cancel</button>

Campos en el formulario

Por cada campo tenemos un label, una etiqueta input y una etiqueta por cada validación que fue definida en la configuración del formulario en la clase del componente.

La siguiente figura muestra lo que se despliega si el usuario ingresa mal la información en todos los campos de acuerdo con las reglas definidas. El nombre tiene menos de 2 letras, la dirección no tiene valor y el email no tiene el formato correcto. Note que en la Figura 5, Create no está habilitado porque el formulario no es válido.

Figura 5. Formulario inválido

Veamos un ejemplo con el campo name que fue definido de la siguiente forma en el componente, con dos validaciones:

this.clientForm = this.formBuilder.group({
      name: ["", [Validators.required, Validators.minLength(2)]],
     ...
    });

El HTML correspondiente es el siguiente: el label, el input y las dos validaciones:

<div class="form-group mx-sm-3 mb-2">
                <label for="name">
                    Name
                </label>
                <input novalidate id="name" class="form-control" formControlName="name" placeholder="Your Full Name">
                <div class="alert alert-danger alert-dismissible fade show"
                    *ngIf="clientForm.get('name').hasError('required') && clientForm.get('name').touched">
                    Name required
                </div>
                <div class="alert alert-danger alert-dismissible fade show"
                    *ngIf="clientForm.get('name').hasError('minlength')">
                    Name too short
                </div>
            </div>

Input

Para la etiqueta input, tenemos que el significado de cada atributo es el siguiente:

novalidate

Se define para impedir que se ejecuten validaciones de html normales dado que las validaciones se definen en la forma.

id="name"

Identifica el campo

class="form-control"

Formato de bootstrap para que quede el label y el input uno debajo del otro

formControlName="name"

El nombre del campo en el objeto clientForm definido en el componente

placeholder="Your Full Name"

El valor que va a aparecer por defecto

Validaciones

Procesar una validación consiste en definir un condicional utilizando la directiva de Angular *ngIf para consultar si hay el error y en ese caso, desplegar el mensaje al usuario.

En este ejemplo queremos saber si hay error en la validación sobre la longitud del texto.:

*ngIf="clientForm.get('name').hasError('minlength')

Veamos el código:

<div class="alert alert-danger alert-dismissible fade show"
                    *ngIf="clientForm.get('name').hasError('minlength') && clientForm.get('name').touched">
                    Name too short
                </div>

También se agrega clientForm.get('name').touched para saber si el campo fue efectivamente tocado por el usuario.

De nuestro formulario las consultas al error son:

Definición en el componente

*ngIf en el HTML

Validators.required

clientForm.get('address').hasError('required')

Validators.minLength(2)

clientForm.get('name').hasError('minlength')

Validators.email

clientForm.get('email').hasError('email')

Código completo de formulario

<div class="container-fluid">
 <div class="col-4 p-3">
   <div class="h4" i18n>Customer Creation</div>
   <form
     [formGroup]="clientForm"
     (ngSubmit)="!clientForm.invalid && createClient(clientForm.value)"
   >
     <div class="form-group mx-sm-3 mb-2">
       <label for="name"> Name* </label>
       <input
         novalidate
         id="name"
         class="form-control"
         formControlName="name"
         placeholder="Your Full Name"
       />
       <div
         class="alert alert-danger alert-dismissible fade show"
         *ngIf="
           clientForm.get('name').hasError('required') &&
           clientForm.get('name').touched
         "
       >
         Name required
       </div>
       <div
         class="alert alert-danger alert-dismissible fade show"
         *ngIf="
           clientForm.get('name').hasError('minlength') &&
           clientForm.get('name').touched
         "
       >
         Name too short. It should be at least 2 characters
       </div>
     </div>

     <div class="form-group mx-sm-3 mb-2">
       <label for="address"> Address* </label>
       <input
         id="address"
         type="text"
         class="form-control"
         formControlName="address"
         placeholder="Your Address"
       />

       <div
         class="alert alert-danger alert-dismissible fade show"
         *ngIf="
           clientForm.get('address').hasError('required') &&
           clientForm.get('address').touched
         "
       >
         Address required
       </div>
     </div>

     <div class="form-group mx-sm-3 mb-2">
       <label for="email"> Email* </label>
       <input
         id="email"
         type="text"
         class="form-control"
         formControlName="email"
         placeholder="Your Email"
       />
       <div
         class="alert alert-danger alert-dismissible fade show"
         *ngIf="
           clientForm.get('email').hasError('required') &&
           clientForm.get('email').touched
         "
       >
         Email required
       </div>
       <div
         class="alert alert-danger alert-dismissible fade show"
         *ngIf="
           clientForm.get('email').touched &&
           clientForm.get('email').hasError('email')
         "
       >
         Incorrect email format. It must include @
       </div>
     </div>

     <div class="row m-3 small">
       <p>* Required fields</p>
     </div>

     <div class="row m-3">
       <button
         type="submit"
         class="btn btn-success"
         [disabled]="!clientForm.valid"
       >
         Create
       </button>
       <button
         type="button"
         class="btn btn-danger ml-3"
         (click)="cancelCreation()"
       >
         Cancel
       </button>
     </div>
   </form>
 </div>
</div>

Cuando el formulario es válido y el usuario hace clic en el botón Create, estamos invocando el método createClient(clientForm.value) que debe estar definido en la clase del componente. En nuestro tutorial tenemos el siguiente código, que imprime el cliente que se va a crear en la consola, llama a showSucess() que utiliza el toastr y resetea el formulario. Entre comentarios lo que sería el código completo si llamamos el servicio que invoca el HttpClient de Angular.

createClient(newClient: Client) {
    // Process checkout data here
    console.warn("el cliente fue creado", newClient);
    this.showSuccess(newClient);

    //-----------------------------------------------------------------
    // this.clientService.createClient(newClient).subscribe(client => {
    //   this.clientes.push(client);
    //  this.showSuccess(newClient);
    // });
    //------------------------------------------------------------------
    this.clientForm.reset();

  }

El resultado final utilizando el toastr es el que se presenta en la Figura 6.

Figura 6. Ejemplo del toastr

Cuando el usuario cancela debemos decidir si queremos enviarle un mensaje al usuario utilizando el toastr, debemos resetear el formulario y decidir a dónde queremos navegar.

En este tutorial que solo tiene el formulario no navegamos a ningún lado, solo reseteamos el formulario.

cancelCreation() {
    console.log("Cancelando ...");
    this.clientForm.reset();
  }