Manejo de Formularios Reactivos con Angular
Hola! Hoy vamos a ver "Manejo de Formularios Reactivos con Angular".
Muchas veces nos hemos topado con la desgracia de tener que crear un formulario enorme para envío de datos (ya sea un alta de usuario, un ABM de productos o un largo etcétera) y al intentar realizarlo con el típico Angular Forms (ó también llamado Template-Driven Forms) debemos usar NgModal para poder asignarle valores a nuestros campos.
El problema, quizás, no es escribir el código del lado de la vista, ya que está diseñado para esto, pero al intentar realizar las validaciones se pone un poco engorroso y el código termina siendo bastante horrible. Además, complicamos la creación de nuestros Test Unitarios. Otro punto interesante, es que Angular Forms es Sincrónico.
Reactive Forms al rescate
Angular nos ofrece una solución a todo esto, llamada Reactive Forms. Esto es un modulo que nos permite realizar formularios reactivos y asincrónicos.
¿Qué es algo reactivo?
Este término significa que nuestro form va a "Reaccionar" ante ciertos eventos o cambios.
Para ver como implementar esto en nuestro form, primero vamos a crear uno de ejemplo.
En este caso, lo que hice yo (para simplificar el diseño) fue incluir Bulma como Framework CSS (pueden encontrar toda la documentación en su web oficial). Bulma, nos va a proveer de un style lindo para el form.
Para incluirlo lo único que tenemos que hacer es agregarlo a nuestro index.html en la etiqueta head
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
Ahora, vamos a nuestro app.component.html e insertamos el siguiente html:
<div class="columns">
<div class="column is-three-fifths is-offset-one-fifth">
<form action="">
<div class="field">
<label class="label">Usuario</label>
<div class="control has-icons-left has-icons-right">
<input
class="input is-success"
type="text"
placeholder="Ingrese Usuario"
/>
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
<span class="icon is-small is-right">
<i class="fas fa-check"></i>
</span>
</div>
<p class="help is-success">Este usuario esta disponible</p>
<p class="help is-danger">Este no está disponible</p>
<p class="help is-danger">El usuario es requerido</p>
</div>
<div class="field">
<label class="label">Email</label>
<div class="control has-icons-left has-icons-right">
<input
class="input is-danger"
type="email"
placeholder="Ingrese Email"
/>
<span class="icon is-small is-left">
<i class="fas fa-envelope"></i>
</span>
<span class="icon is-small is-right">
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<p class="help is-success">Este email es válido</p>
<p class="help is-danger">Este email es inválido</p>
<p class="help is-danger">El email es requerido</p>
</div>
<div class="field">
<label class="label">Password</label>
<div class="control has-icons-left has-icons-right">
<input
class="input is-danger"
type="password"
placeholder="Password"
/>
<span class="icon is-small is-left">
<i class="fas fa-envelope"></i>
</span>
<span class="icon is-small is-right">
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<p class="help is-success">Este password es válido</p>
<p class="help is-danger">El password debe ser mayor a 6 digitos</p>
<p class="help is-danger">El password es requerido</p>
</div>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-link">Enviar</button>
</div>
<div class="control">
<button class="button is-link is-light">Cancelar</button>
</div>
</div>
</form>
</div>
</div>
Como podrán ver, ya tenemos nuestra plantilla html del formulario para que vayamos modificando e implementar reactive forms.
¿Cómo se utiliza reactive forms?
Lo primero que tenemos que hacer es incluir en nuestro module, los módulos correspondientes a Forms y ReactiveForms. En mi caso lo vamos a hacer dentro de app.module.ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Lo segundo que vamos a hacer es modificar nuestro formulario en html para definir que controles van a estar en el mismo y así seguirlo trabajando en el ts.
Pero antes, voy a definir algunas cosas:
formGroup: Va a ser nuestro formulario, la ventaja que tiene es que nos permite hacer un "seguimiento" a los valores que posee y tiene propiedades interesantes como valid (nos permite verificar si un formulario es válido). Dentro, van a estar sus "hijos" que van a ser los inputs que tenga el mismo (los definimos con formControlName)
formControlName: Con esto definimos cuales van a ser los inputs que van a estar incluidos en nuestro form y a los cuales vamos a aplicar los métodos necesarios para validarlos, enviarlos, etc.
onSubmit: Es el evento de envío de formulario. Funciona al hacer click en el botón del tipo Submit (en nuestro caso "Enviar")
Agregando el form en nuestra vista
Vamos a agregar a nuestro tag <form> las propiedades formGroup y el onSubmit
<form [formGroup]="loginForm" (onSubmit)="sendLogin()">
<!-- contenido de nuestro form -->
</form>
Ahora, nos toca definir los controles que pertenecen a ese formGroup.
<!-- cosas del form -->
<input class="input is-success" type="text" placeholder="Ingrese Usuario" formControlName="usuario"/>
<!-- mas cosas del form -->
<!-- cosas del form -->
<input class="input is-danger" type="email" placeholder="Ingrese Email" formControlName="email"/>
<!-- mas cosas del form -->
<!-- cosas del form -->
<input class="input is-danger"type="password" placeholder="Password" formControlName="password"/>
<!-- mas cosas del form -->
Como tercer paso, vamos a definir nuestro form en el componente ts para poder agregar las validaciones y hacer controles desde allí.
Definimos nuestro form (el nombre debe coincidir con el que pusimos en el html)
public loginForm:FormGroup;
Luego, tambien declaramos el FormBuilder en el constructor
constructor(private fb: FormBuilder)
Lo siguiente es "construir" nuestro form con sus controles (o hijos) correspondientes, lo haremos en el ngOnInit().
public ngOnInit(){
this.loginForm = this.fb.group({
usuario: new FormControl(''),
email: new FormControl(''),
password: new FormControl(''),
})
}
Los nombres de los controles también deben coincidir en el html.
Perfecto, ahora solo nos queda agregarle las validaciones correspondientes a cada input.
Validaciones
Existen distintos tipos de validaciones que podemos utilizar, yo nombraré solo las que voy a usar (la lista completa esta acá)
Required (Validators.required): Esta validación nos dice cual input es requerido para completar el form.
Email (Validators.email): Indica si el input ingresado es del tipo email o no.
MinLength (Validators.minLength(minimo)): Indica el minimo de caracteres que puede tener un input.
También podemos hacer validaciones customizables y/o asíncronas (en este caso voy a combinar las dos).
¿Cómo esta compuesto un objeto del tipo FormControl?
El objeto tiene la siguiente naturaleza (pueden ver la documentación acá)
new FormControl('valorInicial',[Validaciones Sincronas (Array)],[Validaciones Asincronas(Array)]);
Esto puede variar ya que se puede agregar un compose (pero es más avanzado)
Para utilizar las validaciones, las podemos incluir en el objeto FormControl de cada input al momento de construir nuestro Form.
Modifiquemos nuestro "constructor" del objeto Form para agregarlas.
this.loginForm = this.fb.group({
usuario: new FormControl("", [Validators.required]),
email: new FormControl("",[Validators.required,Validators.email]),
password: new FormControl("",[Validators.required,Validators.minLength(6)])
});
Podemos observar que agregamos las validaciones nombradas arriba.
Ahora supongamos lo siguiente: Necesitamos que cada vez que se escriba en nuestro input se chequee si el usuario ingresado ya existe o no.. ¿Cómo lo hacemos?
Fácil, podemos utilizar una validación asíncrona.
¿Cómo se crea un validación ásincrónica?
Primero, como nuestra validación va a ser propia (custom), debemos crear una directiva y allí ponemos que es lo que hace:
ng g d ValidatorsForms/Usuario
El código sería el siguiente:
import { UserService } from '../Services/user.service';
import { timer } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';
import { FormControl } from '@angular/forms';
export const userAsyncValidator =
(userService: UserService, time: number = 500) => {
return (input: FormControl) => {
return timer(time).pipe(
switchMap(() => userService.checkUser(input.value)),
map(res => {
return res.isUserAvailable ? null : {userAvailable: false}
})
);
};
};
Como podemos ver, usamos rxjs (pueden ver algo en esta entrada del blog donde creamos un carrito con rxjs)
Voy a explicar a grandes rasgos que hacemos acá:
Timer: Esta función nos permite crear un observable que se emite cada cierto tiempo (para la simulación del servicio)
Pipe: El pipe de rxjs lo utilizamos para concatenar funciones.
SwitchMap: Se utiliza para cancelar llamadas anteriores realizadas al servicio y así evitar errores.
Map: Modificamos el valor de salida dependiendo de si el usuario existe o no, retornamos un valor.
El servicio (simulado) que cree para esto es el siguiente:
import { Injectable } from '@angular/core';
import { of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
public checkUser(usuario: string) {
// simulate http.get()
return of({ isUserAvailable: usuario !== 'prueba'});
}
}
Ahora solo nos queda agregarlo a nuestro Objeto FormControl como validación ásincronica. En mi caso lo agregaré solo al usuario, quedando así la construccion del form:
this.loginForm = this.fb.group({
usuario: new FormControl("", [Validators.required], [userAsyncValidator(this.userService)]),
email: new FormControl("",[Validators.required,Validators.email]),
password: new FormControl("",[Validators.required,Validators.minLength(6)])
});
Nuestro componente finalizado debería quedar así:
import { Component, OnInit } from "@angular/core";
import {
FormGroup,
FormBuilder,
FormControl,
Validators
} from "@angular/forms";
import { userAsyncValidator } from './ValidatorsForms/Usuario.directive';
import { UserService } from './Services/user.service';
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
title = "Formularios";
public loginForm: FormGroup;
constructor(private fb: FormBuilder, private userService:UserService) {}
public ngOnInit() {
this.loginForm = this.fb.group({
usuario: new FormControl("", [Validators.required], [userAsyncValidator(this.userService)]),
email: new FormControl("",[Validators.required,Validators.email]),
password: new FormControl("",[Validators.required,Validators.minLength(6)])
});
}
public sendLogin() {}
}
Ahora debemos ir al html para modificar la vista de acuerdo a las validaciones.
¿Cómo deshabilitar un botón con Reactive Forms?
Para eso podemos utilizar la propiedad valid de nuestro FormGroup:
<button type="submit" class="button is-link" [disabled]="!loginForm.valid">Enviar</button>
¿Cómo mostrar un mensaje de error si el input no pasa una validación?
Podemos agregar en nuestro mensaje un ngIf para fijarnos si hay un error o no en ese input especifico.
<p
class="help is-danger walkP"
*ngIf="
(loginForm.controls['usuario'].dirty ||
loginForm.controls['usuario'].touched) &&
loginForm.controls['usuario'].errors && !loginForm.controls['usuario'].errors.userAvailable
">
Este usuario no está disponible
</p>
Paso a explicar que es cada propiedad:
dirty: Chequea si el usuario ya escribio algo en el input
touched: Chequea si el usuario ya entro o hizo click en el input
errors: Se fija si ese input incumplió alguna validación
errors.userAvailable: "userAvailable" es nuestra validación custom, es para fijarnos si incumplió con esa validación especifica (se puede reemplazar por la validación que queramos)
También podemos jugar con el ngClass para mostrar mensajes con un estilo o no.
Voy a dejar como me quedo finalmente mi Formulario con los estilos:
HTML:
<div class="columns">
<div class="column is-three-fifths is-offset-one-fifth">
<form [formGroup]="loginForm" (onSubmit)="sendLogin()">
<div class="field">
<label class="label">Usuario</label>
<div class="control has-icons-left has-icons-right">
<input
class="input"
type="text"
placeholder="Ingrese Usuario"
formControlName="usuario"
[ngClass]="{
'is-danger':
loginForm.controls['usuario'].dirty &&
loginForm.controls['usuario'].errors,
'is-success':
loginForm.controls['usuario'].dirty &&
!loginForm.controls['usuario'].errors
}"
/>
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
<span
class="icon is-small is-right iconP"
*ngIf="
(loginForm.controls['usuario'].dirty ||
loginForm.controls['usuario'].touched) &&
!loginForm.controls['usuario'].errors
"
>
<i class="fas fa-check"></i>
</span>
<span
class="icon is-small is-right iconP"
*ngIf="
(loginForm.controls['usuario'].dirty ||
loginForm.controls['usuario'].touched) &&
loginForm.controls['usuario'].errors
"
>
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<p
class="help is-success walkP"
*ngIf="
(loginForm.controls['usuario'].dirty ||
loginForm.controls['usuario'].touched) &&
!loginForm.controls['usuario'].errors
"
>
Este usuario esta disponible
</p>
<p
class="help is-danger walkP"
*ngIf="
(loginForm.controls['usuario'].dirty ||
loginForm.controls['usuario'].touched) &&
loginForm.controls['usuario'].errors &&
!loginForm.controls['usuario'].errors.userAvailable
"
>
Este usuario no está disponible
</p>
<p
class="help is-danger walkP"
*ngIf="
(loginForm.controls['usuario'].dirty ||
loginForm.controls['usuario'].touched) &&
loginForm.controls['usuario'].errors &&
loginForm.controls['usuario'].errors.required
"
>
El usuario es requerido
</p>
</div>
<div class="field">
<label class="label">Email</label>
<div class="control has-icons-left has-icons-right">
<input
class="input"
type="email"
placeholder="Ingrese Email"
formControlName="email"
[ngClass]="{
'is-danger':
loginForm.controls['email'].dirty &&
loginForm.controls['email'].errors,
'is-success':
loginForm.controls['email'].dirty &&
!loginForm.controls['email'].errors
}"
/>
<span class="icon is-small is-left">
<i class="fas fa-envelope"></i>
</span>
<span
class="icon is-small is-right iconP"
*ngIf="
(loginForm.controls['email'].dirty ||
loginForm.controls['email'].touched) &&
!loginForm.controls['email'].errors
"
>
<i class="fas fa-check"></i>
</span>
<span
class="icon is-small is-right iconP"
*ngIf="
(loginForm.controls['email'].dirty ||
loginForm.controls['email'].touched) &&
loginForm.controls['email'].errors
"
>
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<p
class="help is-success walkP"
*ngIf="
(loginForm.controls['email'].dirty ||
loginForm.controls['email'].touched) &&
!loginForm.controls['email'].errors
"
>
El email es válido
</p>
<p
class="help is-danger walkP"
*ngIf="
(loginForm.controls['email'].dirty ||
loginForm.controls['email'].touched) &&
loginForm.controls['email'].errors &&
loginForm.controls['email'].errors.email
"
>
El email es inválido
</p>
<p
class="help is-danger walkP"
*ngIf="
(loginForm.controls['email'].dirty ||
loginForm.controls['email'].touched) &&
loginForm.controls['email'].errors &&
loginForm.controls['email'].errors.required
"
>
El email es requerido
</p>
</div>
<div class="field">
<label class="label">Password</label>
<div class="control has-icons-left has-icons-right">
<input
class="input is-danger"
type="password"
placeholder="Password"
formControlName="password"
[ngClass]="{
'is-danger':
loginForm.controls['password'].dirty &&
loginForm.controls['password'].errors,
'is-success':
loginForm.controls['password'].dirty &&
!loginForm.controls['password'].errors
}"
/>
<span class="icon is-small is-left">
<i class="fas fa-key"></i>
</span>
<span
class="icon is-small is-right iconP"
*ngIf="
(loginForm.controls['password'].dirty ||
loginForm.controls['password'].touched) &&
!loginForm.controls['password'].errors
"
>
<i class="fas fa-check"></i>
</span>
<span
class="icon is-small is-right iconP"
*ngIf="
(loginForm.controls['password'].dirty ||
loginForm.controls['password'].touched) &&
loginForm.controls['password'].errors
"
>
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<p
class="help is-success walkP"
*ngIf="
(loginForm.controls['password'].dirty ||
loginForm.controls['password'].touched) &&
!loginForm.controls['password'].errors
"
>
Este password es válido
</p>
<p
class="help is-danger walkP"
*ngIf="
(loginForm.controls['password'].dirty ||
loginForm.controls['password'].touched) &&
loginForm.controls['password'].errors &&
loginForm.controls['password'].errors.minlength
"
>
El password debe ser mayor a 6 digitos
</p>
<p
class="help is-danger walkP"
*ngIf="
(loginForm.controls['password'].dirty ||
loginForm.controls['password'].touched) &&
loginForm.controls['password'].errors &&
loginForm.controls['password'].errors.required
"
>
El password es requerido
</p>
</div>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-link" [disabled]="!loginForm.valid">Enviar</button>
</div>
<div class="control">
<button class="button is-link is-light">Cancelar</button>
</div>
</div>
</form>
</div>
</div>
CSS:
.walkP {
animation-name: walk;
animation-duration: 1s;
margin-left: 0%;
opacity: 1;
}
.iconP {
opacity: 1;
animation-name: op;
animation-duration: 1s;
}
@keyframes walk {
from {
margin-left: 30%;
opacity: 0;
}
to {
margin-left: 0%;
opacity: 1;
}
}
@keyframes op {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
Como pueden ver le agregué algunas animaciones.
Pueden encontrar el código completo en mi GitHub
Así finalizamos esta entrada sobre "Manejo de formularios Reactivos con Angular"