Formularios en Angular#
Los formularios son una parte fundamental de cualquier aplicación web, y Angular ofrece dos enfoques principales para manejarlos: formularios reactivos y formularios basados en plantillas.
Característica |
Formularios Reactivos |
Formularios Basados en Plantillas |
---|---|---|
Configuración del modelo de formulario |
Explícita, creada en la clase del componente |
Implícita, creada por directivas |
Modelo de datos |
Estructurado e inmutable |
No estructurado y mutable |
Flujo de datos |
Sincrónico |
Asincrónico |
Validación del formulario |
Funciones |
Directivas |
Formularios de Plantilla en Angular#
Usaremos los formularios de plantilla para aquellos que sean muy simples y no requieran una validación o manejo de los datos sofisticados.
Configuración de Formularios Basados en Plantillas#
Para empezar a trabajar con formularios basados en plantillas, necesitamos importar el módulo FormsModule
en nuestro componente:
import { FormsModule } from '@angular/forms';
...
imports: [
FormsModule,
// otros módulos
],
...
Uso de ngModel y Validadores HTML5#
Los formularios basados en plantillas en Angular permiten utilizar validadores HTML5 directamente en los elementos input
. Estos validadores aplican automáticamente clases de estilo dependiendo del estado del formulario.
<input
type="text"
class="form-control"
[(ngModel)]="product.description"
minlength="5"
maxlength="600"
required
/>
Modificar Estilos al Validar#
Podemos cambiar los estilos de los elementos del formulario en función de su estado de validación utilizando clases CSS personalizadas y la referencia a ngModel
.
<input
type="text"
name="description"
class="form-control"
[(ngModel)]="product.description"
minlength="5"
maxlength="600"
required
#descriptionModel="ngModel"
[ngClass]="{
'is-valid': descriptionModel.touched && descriptionModel.valid,
'is-invalid': descriptionModel.touched && !descriptionModel.valid
}"
/>
<div>
<div>{{ product | json }}</div>
<div>Dirty: {{ descriptionModel.dirty }}</div>
<div>Valid: {{ descriptionModel.valid }}</div>
<div>Value: {{ descriptionModel.value }}</div>
<div>Errors: {{ descriptionModel.errors | json }}</div>
</div>
Manipular la Entrada del Usuario en Tiempo Real#
Podemos separar la vinculación del modelo y el evento ngModelChange
para manipular la entrada del usuario en tiempo real.
<input
type="text"
class="form-control"
[ngModel]="product.description"
(ngModelChange)="product.description = $event.toUpperCase()"
/>
ngForm y Directiva noValidate#
Por defecto, todos los formularios en Angular tienen la directiva ngForm
. Podemos hacer una referencia a esta directiva para observar las propiedades generales del formulario. Es recomendable usar la directiva novalidate
para desactivar la validación del navegador y permitir que Angular gestione la validación.
<form #productForm="ngForm" novalidate>
<input type="text" name="description" ... />
</form>
<div>
<div>Touched: {{ productForm.touched }}</div>
<div>Valid: {{ productForm.valid }}</div>
<div>Value: {{ productForm.value | json }}</div>
<div>Descripción: {{productForm.control.get('description').value | json}}</div>
</div>
Mostrar los Errores de Validación#
Podemos combinar estilos de Bootstrap, validación HTML5, ngIf
y referencias a ngModel
para mostrar mensajes de error de validación.
<form #productForm="ngForm" novalidate>
<input
type="text"
name="description"
class="form-control"
[(ngModel)]="product.description"
minlength="5"
maxlength="600"
required
#descriptionModel="ngModel"
[ngClass]="{
'is-valid': descriptionModel.touched && descriptionModel.valid,
'is-invalid': descriptionModel.touched && !descriptionModel.valid
}"
/>
<div *ngIf="descriptionModel.touched && descriptionModel.invalid" class="alert alert-danger">
Descripción requerida (entre 5 y 60 caracteres)
</div>
</form>
Creación de Validadores Personalizados#
Angular permite la creación de validadores personalizados mediante directivas. Estos validadores se registran como validadores de Angular utilizando el proveedor NG_VALIDATORS
.
import { Directive, Input } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
@Directive({
selector: '[appMinPrice]',
providers: [{ provide: NG_VALIDATORS, useExisting: MinPriceDirective, multi: true }]
})
export class MinPriceDirective implements Validator {
@Input('appMinPrice') minPrice: number;
constructor() { }
validate(c: AbstractControl): { [key: string]: any } | null {
if (this.minPrice && c.value) {
if (this.minPrice > c.value) {
return { minPrice: true };
}
}
return null;
}
}
Enviar el Formulario#
Podemos enviar el formulario y realizar validaciones adicionales antes de enviarlo. Además, podemos desactivar el botón de envío hasta que el formulario sea válido.
<form #productForm="ngForm" (ngSubmit)="editar(productForm)" novalidate>
<input type="text" name="description" ... />
<button type="submit" class="btn btn-primary" [disabled]="productForm.invalid">Submit</button>
</form>
En el componente, obtenemos el formulario utilizando @ViewChild
y la variable de referencia:
@ViewChild('editForm', { static: true }) editForm: NgForm;
editar(productForm: NgForm) {
if (this.editForm.valid) {
this.productService.editProduct(this.product).subscribe(
ok => this.router.navigate(['/product/', this.product.id])
);
}
}
Formularios Reactivos#
En Angular, los formularios reactivos representan un conjunto de técnicas que permiten controlar mejor el comportamiento de los formularios desde el código del componente en lugar de desde la plantilla. Aunque el uso de [(ngModel)]
en formularios basados en plantillas ya proporciona cierto comportamiento reactivo, los formularios reactivos ofrecen una manera más estructurada y potente de manejar formularios complejos. Es recomendable usar formularios reactivos, especialmente en formularios más avanzados que van más allá de simples búsquedas o inicios de sesión.
Para trabajar con formularios reactivos, debemos importar el módulo ReactiveFormsModule
import { ReactiveFormsModule } from '@angular/forms';
...
imports: [
ReactiveFormsModule,
// otros módulos
],
...
En los formularios reactivos, declaramos directamente en el código los objetos FormControl
, FormGroup
y FormArray
. A continuación, se muestra un ejemplo básico de cómo configurar un formulario reactivo:
Definir el Formulario en el Componente
En el componente, necesitamos una variable de tipo FormGroup
inicializada en el constructor. Utilizaremos un servicio inyectado llamado FormBuilder
para construir el formulario.
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-product-form',
templateUrl: './product-form.component.html'
})
export class ProductFormComponent {
formulario: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.crearFormulario();
}
crearFormulario() {
this.formulario = this.formBuilder.group({
name: [''],
price: [0],
description: [''],
});
}
crear() {
// Lógica para crear el producto
}
}
HTML del Formulario
En el HTML del formulario, solo necesitamos agregar [formGroup]
y formControlName
en cada input
.
<form [formGroup]="formulario" (ngSubmit)="crear()">
<div class="form-group">
<label for="inputName">Name</label>
<input type="text" class="form-control" id="inputName" formControlName="name">
</div>
<div class="form-group">
<label for="inputPrice">Price</label>
<input type="number" class="form-control" id="inputPrice" formControlName="price">
</div>
<div class="form-group">
<label for="inputDescription">Description</label>
<input type="text" class="form-control" id="inputDescription" formControlName="description">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Validaciones Síncronas#
En los formularios reactivos, las validaciones son más sencillas y se pueden declarar directamente en el código del componente usando Validators
.
import { Validators } from '@angular/forms';
crearFormulario() {
this.formulario = this.formBuilder.group({
name: ['', [Validators.required, Validators.minLength(5), Validators.pattern('.*[a-zA-Z].*')]],
price: [0, Validators.min(0.01)],
description: [''],
});
}
get nameNotValid() {
return this.formulario.get('name').invalid && this.formulario.get('name').touched;
}
Para aplicar las validaciones visualmente en el HTML, podemos utilizar getters:
<input type="text" class="form-control" id="inputName" formControlName="name"
[ngClass]="nameNotValid ? 'is-invalid' : 'is-valid'">
Creación de Validadores Personalizados#
Los validadores personalizados en formularios reactivos son funciones que devuelven una ValidatorFn
. Aquí mostramos cómo crear un validador personalizado para fechas mínimas:
import { AbstractControl, ValidatorFn } from '@angular/forms';
function minDateValidator(minInputDate: string): ValidatorFn {
return (c: AbstractControl): { [key: string]: any } | null => {
if (c.value) {
const minDate = new Date(minInputDate);
const inputDate = new Date(c.value);
return minDate <= inputDate ? null : { 'minDate': minDate.toLocaleDateString() };
}
return null;
};
}
Validación de Campos Cruzados#
Podemos crear validadores personalizados para evaluar un campo en función de otros campos en el formulario. Aquí un ejemplo para validar que dos campos de contraseña coincidan:
this.registerForm = this.formBuilder.group({
password: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(10)]],
confirm_password: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(10)]]
}, {
validators: passwordValidator
});
const passwordValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const password = control.get('password');
const confirmPassword = control.get('confirm_password');
return password && confirmPassword && password.value === confirmPassword.value ? null : { passwordValidator: true };
};
Agrupar Campos#
Podemos agrupar campos en un FormGroup
dentro de otro FormGroup
:
this.formulario = this.formBuilder.group({
name: ['', [Validators.required, Validators.minLength(5), Validators.pattern('.*[a-zA-Z].*')]],
price: [0, Validators.min(0.01)],
description: [''],
address: this.formBuilder.group({
street: [''],
city: ['']
})
});
Y en el HTML:
<div class="form-group row mb-3" formGroupName="address">
<label for="inputStreet" class="col-sm-2 col-form-label">Street</label>
<div class="form-row col">
<div class="col">
<input class="form-control" id="inputStreet" placeholder="Street" formControlName="street">
</div>
<div class="col">
<input class="form-control" id="inputCity" placeholder="City" formControlName="city">
</div>
</div>
</div>
Cargar Datos en el Formulario#
Si estamos trabajando en un formulario de edición, podemos cargar los valores en los controles de formulario utilizando setValue()
, patchValue()
o reset()
.
this.formulario.setValue(this.product); // Necesita todos los campos
// Para resetear con valores por defecto
this.formulario.reset({
name: 'Default Name',
price: 0,
description: 'Default Description'
});
// Para actualizar solo algunos campos
this.formulario.patchValue({
name: 'Updated Name'
});
Detección de Cambios#
Los FormGroup
y FormControl
contienen un Observable llamado valueChanges
que emite el valor actual del control cada vez que cambia. Podemos suscribirnos a este Observable para realizar acciones en respuesta a los cambios.
export class AppComponent implements OnInit {
formulario: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.formulario = this.formBuilder.group({
notifications: [false]
});
this.formulario.get('notifications').valueChanges
.subscribe(value => this.updateNotificationMethod(value));
}
updateNotificationMethod(value: boolean) {
// Lógica para manejar cambios en las notificaciones
}
}
Formularios Dinámicos#
Los FormArray
permiten crear formularios dinámicos donde los controles pueden añadirse o eliminarse en tiempo de ejecución.
Para funcionar necesitamos crear un formArray
dentro del formGroup
del formulario. En el ejemplo lo hacemos con formBuilder.array
. Para generar nuevos formControl
dentro de ese array, en este caso hemos creado dos funciones getIngredientControl
y generateIngredientControl
que crean un control de forma menos concisa que con los formBuilder
.
Hemos hecho funciones para crear nuevos controles y borrarlos, así como la función que rellena el formulario y crea tantos controles como elementos tenga el array. Puesto que un formArray
es un tipo especial de formControl
, hay que castearlo cuando se obtiene por get
y tiene funciones útiles como push
o removeAt
. Observemos cómo las usamos en el ejemplo.
@Input('id') recipeID?: string;
mealForm: FormGroup;
constructor(
private supaService: SupabaseService,
private formBuilder: FormBuilder
) {
this.mealForm = this.formBuilder.group({
strMeal: ['', [Validators.required]],
strInstructions: ['', [Validators.required]],
ingredients: this.formBuilder.array([]),
});
}
ngOnInit(): void {
if (this.recipeID) {
// suponiendo que tenemos supabase como backend
this.supaService.getMeals(this.recipeID).subscribe({
next: (meals) => {
this.mealForm.reset(meals[0]);
meals[0].idIngredients.forEach(i=>{ // idIngredients es un array de ids
if(i){
(<FormArray>this.mealForm.get('ingredients')).push(
this.generateIngredientControl(i)
)
}
})
},
error: (err) => console.log(err),
complete: () => console.log('Received'),
});
}
}
getIngredientControl(): FormControl {
const control = this.formBuilder.control('');
control.setValidators(Validators.required);
return control;
}
generateIngredientControl(id: string): FormControl {
const control = this.formBuilder.control(id);
control.setValidators(Validators.required);
return control;
}
get IngredientsArray(): FormArray {
return <FormArray>this.mealForm.get('ingredients');
}
addIngredient() {
(<FormArray>this.mealForm.get('ingredients')).push(
this.getIngredientControl()
);
}
delIngredient(i: number) {
(<FormArray>this.mealForm.get('ingredients')).removeAt(i);
}
Y en el HTML hay que crear un contenedor con el atributo formArrayName
en el que podemos hacer un @for
para recorrer el formArray
obtenido por el getter
creado anteriormente. Este tiene un interable .controls
que contiene la información del control. El input debe tener formControName
al índice del @for
.
En el ejemplo tenemos botones para añadir o borrar elementos del formArray
.
<div formArrayName="ingredients">
@for (ingredient of IngredientsArray.controls; track $index) {
<div class="input-group mb-3">
<input
type="text"
class="form-control"
id="ingredient{{ $index }}"
name="ingredient{{ $index }}"
[formControlName]="$index"
placeholder="ingredient"
[ngClass]="{
'is-valid': ingredient.valid && ingredient.touched,
'is-invalid': ingredient.invalid && ingredient.touched
}"
/>
<button class="btn btn-outline-danger" (click)="delIngredient($index)">
Borrar
</button>
</div>
<button class="btn btn-primary" (click)="addIngredient()">
Añadir ingrediente
</button>
Ficheros en Formularios#
Si ponemos un input de tipo file
, lo podemos tratar como en el ejemplo:
selectedFile: File | null = null;
onFileSelected(event: any) {
this.selectedFile = event.target.files[0];
}
onSubmit() {
if (this.selectedFile && this.nombre && this.email) {
this.uploadService.uploadForm(this.selectedFile, this.nombre, this.email).subscribe({
next: (response) => console.log('Subida exitosa:', response),
error: (error) => console.error('Error al subir:', error)
});
} else {
console.error('Todos los campos son obligatorios');
}
}
Siendo onFileSelected y onSubmit los manejadores de los eventos <form (submit)="onSubmit()">
y <input type="file" (change)="onFileSelected($event)" required />
Cuando hay ficheros, no importa que se trate de formularios reactivos o de plantilla, ya que los
FormControl
no manejar ficheros. Hay que atender al evento de subida del fichero y tratarlo por separado delFormGroup
.