Angular forms: construcción y validaciones
Una de las herramientas más potentes del ecosistema de Angular son los formularios, en especial el módulo de Formularios Reactivos que nos permiten crear formularios de una forma simple pero bastante potente y personalizada.
En este artículo vamos a aprender a construir formularios reactivos y validaciones usando Angular en su versión 15, todo el código se encontrará en el siguiente repositorio de github y aquí nos centraremos en lo más importante:
Bien, para comenzar es necesario que importemos el ReactiveFormsModule en el módulo que estemos trabajando, en este caso lo importamos en el app.module.ts en la sección de imports, ya que este es el módulo que nos permite usar los reactive forms.
// Any imports
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReactiveFormsModule <--- Here
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Una vez hecha la importación nos movemos al componente en el que construiremos el formulario, para este ejemplo crearemos un formulario con los campos full name, email, phone y age; en nuestro app.component.ts escribiremos el siguiente código que explicaré más adelante:
// Any imports
import { FormGroup, FormBuilder} from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
// Declare a form
form!: FormGroup;
constructor(
// import the form builder
private formBuilder: FormBuilder
) {
// Build the form
this.buildForm();
}
// declare getters for each field
get fullNameField() {
return this.form?.get('fullName');
}
get emailField() {
return this.form?.get('email');
}
get phoneField() {
return this.form?.get('phone');
}
get ageField() {
return this.form?.get('age');
}
// create the form
private buildForm() {
this.form = this.formBuilder.group({
fullName: [''],
email: [''],
phone: [''],
age: [18],
});
}
saveUser(event: any) {
console.log(this.form.value);
}
}
Hemos declarado una variable llamada Form en donde se guardarán todos los datos del formulario y su respectivo estado, en el constructor inyectamos el form builder que nos ayudará más adelante en la construcción del formulario, luego declaramos los getters que contendrán el valor y estado de cada campo, la función buildForm que como su nombre lo indica asigna el formulario haciendo uso del formBuilder el cuál crea un form builder group con los campos declarados y por último la función saveUser que imprime los valores del formulario en la consola, se lee un poco enredado pero pienso que el código se explica solo.
Después de construir el formulario vamos a escribir la parte visual en nuestro app.component.html sin usar css ya que no es el objetivo de esta publicación.
<form [formGroup]="form" (ngSubmit)="saveUser($event)">
<div class="fullName">
<label> Full Name: {{ fullNameField?.value }} </label>
<div class="form-group">
<input formControlName="fullName" placeholder="Kevin Bueno" type="text" />
</div>
</div>
<div class="email">
<label> Email: {{ emailField?.value }} </label>
<div class="form-group">
<input
formControlName="email"
placeholder="k@kevinbueno.dev"
type="email"
/>
</div>
</div>
<div class="phone">
<label> Phone: {{ phoneField?.value }} </label>
<div class="form-group">
<input formControlName="phone" placeholder="3212986763" type="tel" />
</div>
</div>
<div class="age">
<label> Age: {{ ageField?.value }} </label>
<div class="form-group">
<input formControlName="age" placeholder="23" type="number" />
</div>
</div>
<div>
<br>
<button type="submit">Save user</button>
</div>
</form>
Una vez más pienso que el código se explica solo pero sin embargo vamos a profundizar un poco más en este bloque de código, enlazamos nuestro formulario de html con nuestro reactive form con [formGroup]=”form” y enviamos el evento de guardar con (ngSubmit)=”saveUser($event)” (el parámetro event no se está usando pero ahí está para que lo tengan en cuenta), luego declaramos cada input y hacemos la técnica two-way data binding para mostrar en tiempo real lo que se está escribiendo, para eso declaramos los getters en el ts así que enlazamos ese getter <label> Full Name: {{ fullNameField?.value }} </label>, posteriormente declaramos el input y lo conectamos al reactive form mediante el formControlName=”fullName”, luego repetimos para cada campo, con la diferencia de que en el campo Age hemos puesto 18 como valor por defecto en el component.ts; para finalizar creamos un botón de tipo submit (es importante agregar el tipo y colocarlo dentro de la etiqueta form) que hará la impresión de los datos en consola.
Toma un break de 5 minutos para revisar instagram y ver que ella/él no te ha escrito. A continuación realizaremos la validaciones, el gran equipo de angular nos dejó listas para usar las validaciones más comunes como min — max para valores numéricos, max-min length para strings, email para … y un etcétera que podrá consultar en la documentación de la API. Sin más agregamos las validaciones a nuestro formulario y creamos nuevos getters que nos permitirán controlar si un input fue tocado o modificado.
// IMPORT VALIDATORS
import { Validators, FormGroup, FormBuilder, } from '@angular/forms';
export class AppComponent {
// get state of a field if was touched or modified
get fullNameFieldDirty() {
return this.fullNameField?.dirty || this.fullNameField?.touched;
}
get emailField() {
return this.form?.get('email');
}
get emailFieldDirty() {
return this.emailField?.dirty || this.emailField?.touched;
}
get phoneField() {
return this.form?.get('phone');
}
get phoneFieldDirty() {
return this.phoneField?.dirty || this.phoneField?.touched;
}
get ageField() {
return this.form?.get('age');
}
get ageFieldDirty() {
return this.ageField?.dirty || this.ageField?.touched;
}
// create the form
private buildForm() {
this.form = this.formBuilder.group({
fullName: ['', [Validators.required, Validators.minLength(10), Validators.pattern(/^[a-zA-Z ]+$/)]],
email: ['', [Validators.required, Validators.email]],
phone: ['', Validators.required],
age: [18, [Validators.required, Validators.min(18), Validators.max(100)]],
});
}
}
He quitado partes del código que para este caso hacen ruido y he dejado lo más importante, primero se importa la clase Validators, segundo creamos los getters que me estarán mostrando los cambios de los inputs reactivamente y agregamos un array de validaciones en el form.builder.group (si es una sola validación no es necesario agregar un array), cada campo tiene la siguiente estructura:
Por ahora no trabajaremos con validaciones asíncronas, así que declaramos nuestros valores por defecto y el array con validaciones síncronas. Esto es para nuestro component.ts, ahora vamos a refactorizar nuestra vista component.html.
<form [formGroup]="form" (ngSubmit)="saveUser($event)">
<div class="fullName">
<label> Full Name: {{ fullNameField?.value }} </label>
<div class="form-group">
<input formControlName="fullName" placeholder="Kevin Bueno" type="text" />
</div>
<ng-container
*ngFor="let validation of utilsService.validationMessages.full_name"
>
<small
*ngIf="fullNameField?.hasError(validation.type) && fullNameFieldDirty"
>
{{ validation.message }}
</small>
</ng-container>
</div>
<div class="email">
<label> Email: {{ emailField?.value }} </label>
<div class="form-group">
<input
formControlName="email"
placeholder="k@kevinbueno.dev"
type="email"
/>
</div>
<ng-container
*ngFor="let validation of utilsService.validationMessages.email"
>
<small *ngIf="emailField?.hasError(validation.type) && emailFieldDirty">
{{ validation.message }}
</small>
</ng-container>
</div>
<div class="phone">
<label> Phone: {{ phoneField?.value }} </label>
<div class="form-group">
<input formControlName="phone" placeholder="3212986763" type="tel" />
</div>
<ng-container
*ngFor="let validation of utilsService.validationMessages.phone"
>
<small *ngIf="phoneField?.hasError(validation.type) && phoneFieldDirty">
{{ validation.message }}
</small>
</ng-container>
</div>
<div class="age">
<label> Age: {{ ageField?.value }} </label>
<div class="form-group">
<input formControlName="age" placeholder="23" type="number" />
</div>
<ng-container
*ngFor="let validation of utilsService.validationMessages.age"
>
<small
*ngIf="ageField?.hasError(validation.type) && ageFieldDirty"
>
{{ validation.message }}
</small>
</ng-container>
</div>
<div>
<br />
<button type="submit" [disabled]="form.invalid">Save user</button>
</div>
</form>
En nuestra vista hemos agregado un ng-container para cada input el cual renderiza los errores en tiempo real, primero buscamos los mensajes de errores que queremos mostrar let validation of utilsService.validationMessages.age traídos desde un servicio que explicaré enseguida, como segundo aspecto preguntamos al input si tiene algún error y si el formulario ha sido tocado o modificado, en caso de que ambas condiciones se cumplan mostramos el error *ngIf=”fullNameField?.hasError(validation.type) && fullNameFieldDirty”, personalmente no me gusta los mensajes de error en el html anidados de muchos ng-if por lo que yo creo un servicio aparte con esos errores y lo inyecto de la siguiente manera:
Utils.services.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UtilsService {
// create all validations messages
public validationMessages = {
// input name
full_name: [
// types must be in lowercase
{ type: 'required', message: 'The full name is required' },
{ type: 'minlength', message: 'The full name must be more than 10 characters' },
{ type: 'pattern', message: 'The full name format is invalid' }
],
email: [
{ type: 'required', message: 'The email is required' },
{ type: 'email', message: 'The email format is invalid' }
],
phone: [
{ type: 'required', message: 'The phone is required' },
],
age: [
{ type: 'min', message: 'The age must be more than 17' },
{ type: 'max', message: 'The age must be minor or equal than 100' },
],
}
constructor() { }
}
app.component.ts
...
import { UtilsService } from './services/utils.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
...
constructor(
// import the form builder
private formBuilder: FormBuilder,
public utilsService: UtilsService, <<<<-- Inject the services
) {
// build the form
this.buildForm();
}
...
}
Para concluir, hemos aprendido a usar los formularios reactivos de angular con sus respectivas validaciones, una de las apis más potentes de este gran framework, próximamente aprenderemos nuevos conceptos con este mismo proyecto.