¿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 actualizar una aplicación Angular para agregar 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 cuatro campos:

Figura 1. Formulario de creación de autor

¿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 componente dentro del módulo

Vaya a la carpeta del módulo author y cree un nuevo componente con el nombre author-create

Asociar el componente con el módulo

En el módulo author agregue en el atributo declarations la referencia al nuevo componente:

declarations: [AuthorListComponent, AuthorDetailComponent, AuthorCreateComponent]

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 importar en el módulo donde se definen componentes que tendrán formularios. En este tutorial, nuestro módulo es AuthorModule.

imports: [
   CommonModule,
   RouterModule,
   AuthorRoutingModule,
   ReactiveFormsModule
 ]

Vamos a llamar el componente de crear desde el menú de navegación. En el archivo del componente principal app.component.html modifique el ítem de Authors para incluir un menú desplegable con dos opciones: list y create.

class="nav-item dropdown">

class="nav-link dropdown-toggle"

href="#"

id="navbarDropdown"

role="button"

data-bs-toggle="dropdown"

aria-expanded="false"

>

Authors

class="dropdown-menu" aria-labelledby="navbarDropdown">

  • class="dropdown-item" routerLink="/authors/list">List
  • class="dropdown-item" routerLink="/authors/create">Create
  • Figura 2. Componente principal

    Para que el menú desplegable funcione se debe agregar está linea en el atributo scripts dentro del archivo angular.json:

    "scripts": ["node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"]

    Luego de este cambio reinicie el servidor y deberá ver el nuevo menú.

    Ahora se debe ajustar las rutas para el módulo author e incluir la nueva ruta para la creación de un autor.

    const routes: Routes = [
      {
        path: 'create',
        component: AuthorCreateComponent
      },
      {
        path: 'list',
        component: AuthorListComponent
      },
      {
        path: ':id',
        component: AuthorDetailComponent
      },
    ];
    

    Ahora al hacer click en el menú Author > Create se debe mostrar el componente de crear

    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 autor. En el siguiente código del componente vamos a declarar una variable authorForm 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 } from '@angular/forms';
    import { ToastrService } from 'ngx-toastr';
    
    @Component({
     selector: 'app-author-create',
     templateUrl: './author-create.component.html',
     styleUrls: ['./author-create.component.css']
    })
    export class AuthorCreateComponent implements OnInit {
    
     authorForm!: FormGroup;
    
     constructor(
       private formBuilder: FormBuilder,
       private toastr: ToastrService
     ) { }
    
     ngOnInit() {
     }
    
    }

    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 a authorForm 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.authorForm = this.formBuilder.group({
         name: ["", [Validators.required, Validators.minLength(2)]],
         image: ["", Validators.required],
         birthDate: ["", Validators.required],
      description: ["", [Validators.required, Validators.maxLength(100)]]
       })
     }

    En el formulario authorForm, estamos definiendo cuatro campos:

    Las validaciones significan que si cualquiera de esas validaciones no se cumple 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 author-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 cuatro 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 autor, 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 authorForm 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 createAuthor(authorForm.value). Esta función debemos definirla en la clase del componente. El detalle de este código se muestra a continuación:

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

    Create y Cancel

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

    .

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

    Ese botón es de tipo type="submit

    " lo que implica que cuando el usuario hace clic en el botón se ejecutará el ngSubmit del formulario. En este caso se llamará al método que permite crear el author con los valores recolectados en los campos que se encuentran en authorForm.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.

    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.authorForm = 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="Author's name"
       />
       <div
         class="alert alert-danger alert-dismissible fade show"
         *ngIf="
           authorForm.get('name')!.hasError('required') &&
           authorForm.get('name')!.touched
         "
       >
         Name required
       </div>
       <div
         class="alert alert-danger alert-dismissible fade show"
         *ngIf="authorForm.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 authorForm definido en el componente

    placeholder="Author's name"

    El valor que va a aparecer como ayuda en el campo

    Validaciones

    Procesar una validación consiste en definir un condicional utilizando la directiva de Angular *ngIf para consultar si hay un 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="authorForm.get('name')!.hasError('minlength')"
       >
         Name too short
    </div>

    También se agrega authorForm.get('name').touched para saber si el campo recibió un clic por parte del usuario.

    De nuestro formulario las consultas al error para el campo name son:

    Definición en el componente

    *ngIf en el HTML

    Validators.required

    authorForm.get('name').hasError('required')

    Validators.minLength(2)

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

    Código completo de formulario

    <div class="container mt-4">
     <h2 class="text-center">Create a new author</h2>
     <form
       [formGroup]="authorForm"
       (ngSubmit)="!authorForm.invalid && createAuthor(authorForm.value)"
     >
       <!--Name-->
       <div class="form-group mx-sm-3 mb-2">
         <label for="name">Name</label>
         <input
           novalidate
           id="name"
           class="form-control"
           formControlName="name"
           placeholder="Author's name"
         />
         <div
           class="alert alert-danger alert-dismissible fade show"
           *ngIf="
             authorForm.get('name')!.hasError('required') &&
             authorForm.get('name')!.touched
           "
         >
           Name required
         </div>
         <div
           class="alert alert-danger alert-dismissible fade show"
           *ngIf="authorForm.get('name')!.hasError('minlength')"
         >
           Name too short
         </div>
       </div>
    
       <!--Image-->
       <div class="form-group mx-sm-3 mb-2">
         <label for="image">Image</label>
         <input
           novalidate
           id="image"
           class="form-control"
           formControlName="image"
           placeholder="URL with Author's image"
         />
         <div
           class="alert alert-danger alert-dismissible fade show"
           *ngIf="
             authorForm.get('image')!.hasError('required') &&
             authorForm.get('image')!.touched
           "
         >
           Image required
         </div>
       </div>
    
       <!--Birth Date-->
       <div class="form-group mx-sm-3 mb-2">
         <label for="birthDate">Birth date</label>
         <input
           novalidate
           id="birthDate"
           class="form-control"
           formControlName="birthDate"
           placeholder="Author's birth date"
         />
         <div
           class="alert alert-danger alert-dismissible fade show"
           *ngIf="
             authorForm.get('birthDate')!.hasError('required') &&
             authorForm.get('birthDate')!.touched
           "
         >
           Birth date required
         </div>
       </div>
    
       <!--Description-->
       <div class="form-group mx-sm-3 mb-2">
         <label for="description">Description</label>
         <input
           novalidate
           id="description"
           class="form-control"
           formControlName="description"
           placeholder="Author's short description"
         />
         <div
           class="alert alert-danger alert-dismissible fade show"
           *ngIf="
             authorForm.get('description')!.hasError('required') &&
             authorForm.get('description')!.touched
           "
         >
           Description required
         </div>
         <div
           class="alert alert-danger alert-dismissible fade show"
           *ngIf="authorForm.get('description')!.hasError('maxlength')"
         >
           Description too long
         </div>
       </div>
    
       <button
         type="submit"
         class="btn btn-primary"
         [disabled]="!authorForm.valid"
       >
         Create
       </button>
       <button
         type="button"
         class="btn btn-danger ml-3"
         (click)="cancelCreation()"
       >
         Cancel
       </button>
     </form>
    </div>

    Cuando el formulario es válido y el usuario hace clic en el botón Create, estamos invocando el método createAuthor(authorForm.value) que debe estar definido en la clase del componente. En nuestro tutorial tenemos el siguiente código, que imprime por consola los datos del autor que se va a crear en la consola, muestra un toastr con un mensaje de confirmación y resetea el formulario.

    createAuthor(author: Author){
       console.info("The author was created: ", author)
       this.toastr.success("Confirmation", "Author created")
       this.authorForm.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 la creación del autor. Por simplicidad lo que haremos en este ejemplo es resetear los campos del formulario.

    Otras acciones pueden ser solicitar la confirmación del usuario y en caso afirmativo, redirección a la lista de autores.

    cancelCreation(){
       this.authorForm.reset();
    }

    Hasta ahora se estaba simulando el post mediante una limpieza de los campos, el console log de los datos y el aviso de éxito. A continuación, verá cómo se puede conectar el resultado del formulario con un backend.

    Atención: Tenga en cuenta que para este ejemplo el post no funcionara pero puede ver como se hace para su propio backend. Su backend tendrá la responsabilidad de responder esta petición con éxito o no.

    Para poder crear el nuevo author se requiere actualizar el servicio AuthorService. En este ejemplo se incluye el método createAuthor.

    createAuthor(author: Author): Observable<Author> {
       return this.http.post<Author>(this.apiUrl, author);
    }

    El método recibe como parámetro el autor que se va a crear y se envía el request de tipo POST a la url definida en el servicio junto con el nuevo autor.

    Ahora entonces se debe actualizar el método createAuthor en el componente CreateAuthorComponent el cual quedará así:

    constructor(
       private formBuilder: FormBuilder,
       private toastr: ToastrService,
       private authorService: AuthorService
     ) { }
    
     createAuthor(author: Author){
       this.authorService.createAuthor(author).subscribe(author=>{
         console.info("The author was created: ", author)
         this.toastr.success("Confirmation", "Author created")
         this.authorForm.reset();
       })
     }
    

    Note que se requiere inyectar el servicio AuthorService en el constructor del componente.

    Al ejecutar las pruebas por defecto vamos a obtener varios errores. Estos ocurren porque el componente de crear tiene referencias a los módulos ReactiveForms y a Toastr. Para que la prueba por defecto funcione agregue las siguientes referencias al atributo imports de la prueba:

    imports: [ReactiveFormsModule, ToastrModule.forRoot(), HttpClientModule],

    Como ejercicio se propone la creación de varios spec para verificar que el formulario se ha desplegado correctamente.