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.
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 |
Para realizar este taller Ud. debe tener claro:
Vaya a la carpeta del módulo author
y cree un nuevo componente con el nombre author-create
En el módulo author agregue en el atributo declarations la referencia al nuevo componente:
declarations: [AuthorListComponent, AuthorDetailComponent, AuthorCreateComponent]
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.
|
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:
name
, su valor inicial es vacío y tiene dos validaciones: la primera dice que el campo es obligatorio y la segunda dice que el valor debe tener una longitud mínima de 2 caracteres. image
, su valor inicial es vacío y tiene una validación que indica que el campo es obligatorio.birthDate
, su valor inicial es vacío y tiene una validación que indica que el campo es obligatorio.description
, su valor inicial es vacío y tiene dos validaciones que indican que el campo es requerido y que su longitud máxima será de 100 caracteres. 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 |
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>
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>
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>
Para la etiqueta input
, tenemos que el significado de cada atributo es el siguiente:
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
<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.