Disclaimer: Esta tarea la hice originalmente en Angular 7 y estamos en proceso de actualizar a Angular 8 por esta razón puede que las cosas cambien en versiones más recientes.
Uno de los retos más raros que he tenido ha sido trabajar con formularios dinámicos en Angular.
¿Qué es un formulario dinámico?
Un formulario dinámico es cuando un formulario permite a un usuario generar campos con una acción. El formulario nunca va a saber cuantos campos quiere tener un usuario, por lo que el usuario tiene todo el control sobre la cantidad de campos que se generan. El usuario también puede quitar campos y eso es lo que hace la forma dinámica.
Un ejemplo es un típico formulario para hacer un currículum en internet. La parte de educación y experiencia laboral se ven algo como así:
Angular nos permite tener formularios dinámicos con su módulo de formularios reactivos o ReactiveFormsModule
. Un formulario reactivo es un tipo de formulario que se crea desde el controlador. En lugar de tener el formulario 100% en la plantilla, desde el controlador se crean los campos y se añaden características como las validaciones.
Hacer formularios directo en la plantilla parece ser más fácil al principio, pero si se tiene un formulario muy complejo es mejor hacerlo con formularios reactivos. Además parece que los formularios de plantilla serán deprecados en versiones futuras de Angular.
¿Cómo se crea un formulario reactivo?
Lo primero que hay que hacer es añadir el módulo de formularios reactivos a nuestro módulo.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule, ReactiveFormsModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Ya que tenemos el módulo importado, tendremos todas las herramientas para nuestro formulario dinámico disponibles en nuestra aplicación.
Ahora en nuestro controlador, lo primero que hay que hacer es definir dos cosas:
- Un
FormBuilder
que nos va a ayudar a construir el formulario - Un
FormGroup
que va a ser nuestro formulario
Para el FormBuilder
, tenemos que definirlo en el constructor del componente.
constructor(private fb: FormBuilder) {}
Y el FormGroup
sólo lo definimos como una variable en el componente.
formulario: FormGroup;
Después, usando el método de ngOnInit()
, mandamos llamar a la función que va a crear el formulario. Esta función se puede llamar como sea, yo la llamé crearFormulario
porque soy mega original.
crearFormulario() {
this.formulario = this.fb.group({
experienciaLaboral: new FormControl('')
});
}
Si este fuera un formulario normal, con esto ya tendríamos un formulario con un campo, pero queremos un formulario dinámico así que hay que seguir.
¿Cómo se hace un formulario dinámico?
Ahora es tiempo de introducir otra herramienta del módulo de formularios reactivos, se llama FormArray
. Un FormArray
es un arreglo de FormControl
. Se comporta muy parecido a un arreglo normal y tiene ciertos métodos propios que hace muy sencillo agregar y quitar elementos del mismo. Para usarlo vamos a modificar nuestro método crearFormulario
.
crearFormulario() {
this.formulario = this.fb.group({
experienciaLaboral: this.fb.array([])
});
}
Como FormBuilder
ya incluye todas estas herramientas, no necesitamos definir un nuevo FormArray
con new FormArray([])
. Los dos métodos son válidos, pero si ya tenemos el FormBuilder definido, hay que usarlo.
A mi gusto personal, prefiero crear una función para añadir campos al FormArray
que definimos en el formulario inicial. En el caso de este ejemplo, voy a crear un FormGroup
con 3 campos, y cada uno de estos grupos será añadido al arreglo llamado experiencia laboral.
Ahora les voy a dar un consejo buenísimo que aprendí en mi nuevo trabajo: hay que crear una función de get
para conseguir el arreglo y así no hacernos mil bolas a la hora de manipularlo.
get experienciaLaboral(): FormArray {
return this.formulario.get('experienciaLaboral') as FormArray;
}
Este get
nos va a permitir manejar el arreglo de manera mucho más sencilla que escribir todo cada vez y además hará que el código sea mucho más legible para otros.
Ya que tenemos nuestro get
, podemos proseguir a crear el grupo que le vamos a poner:
anadirExperienciaLaboral() {
const trabajo = this.fb.group({
empresa: new FormControl(''),
puesto: new FormControl(''),
descripcion: new FormControl('')
});
this.experienciaLaboral.push(trabajo);
}
Cada vez que llamemos a anadirExperienciaLaboral
vamos a generar un FormGroup
con 3 campos: empresa, puesto y descripción. Ya que esté creado, se va a añadir al arreglo de experienciaLaboral
. Todo de una manera muy sencilla por todo el código que ya escribimos.
No se te olvide añadir esta función a ngOnInit
justo abajo de la de crearFormulario
para añadir campos la primera vez.
Ahora, ¿cómo lo vemos en la plantilla?
¿Cómo es la plantilla de un formulario dinámico?
La plantilla no es tan diferente que la de un formulario hecho con base en plantillas. Sin embargo, hay detalles que tenemos que tener en cuenta a la hora de generarlo.
Una cosa que me tuvo atorada mucho tiempo, fue olvidar poner el nombre del arreglo en la plantilla antes de hacer un ciclo for
para generar los campos.
<form [formGroup]="formulario">
<div formArrayName="experienciaLaboral">
<div *ngFor="let trabajo of experienciaLaboral.controls">
<input>
</div>
</div>
</form>
El atributo de formArrayName
es fundamental para que nuestra plantilla sepa que va a recibir un arreglo dentro del formulario. Si esto no se define, los campos no aparecerán en la plantilla.
Otra cosa importante es que aunque tenemos nuestro get
para experienciaLaboral
, el FormArray
no funciona como un arreglo normal por el que podemos iterar. Por eso es importante definir que vamos a iterar por los controles del arreglo. Los controles son todos los FormGroups
que se han añadido al FormArray
.
Para mostrar los campos que queremos es necesario añadir el código para mostrarlos:
<form [formGroup]="formulario">
<h3>
Experiencia Laboral:
</h3>
<div formArrayName="experienciaLaboral">
<div *ngFor="let trabajo of experienciaLaboral.controls">
<div>
<label>Empresa:</label><br />
<input type="text" />
</div>
<div>
<label>Puesto:</label><br />
<input type="text" />
</div>
<div>
<label>Descripción:</label><br />
<textarea></textarea>
</div>
<hr />
</div>
</div>
</form>
¿Cómo se vuelve este show dinámico?
Como lo mencioné al principio, el chiste de los formularios dinámicos es que el usuario pueda añadir y quitar campos a voluntad. En el caso de este formulario, no se van a poder añadir o quitar campos individuales, sino todo un grupo completo a la vez.
Para añadir más campos hay que crear un botón en la plantilla que llame a la función de anadirExperienciaLaboral
, algo así:
<button (click)="anadirExperienciaLaboral()">
+ Añadir
</button>
Este botón se puede poner afuera del *ngFor
para que aparezca una vez en la plantilla.
En el caso de quitar campos del formulario, hay que agregar un botón para borrar dentro de cada grupo que añadimos. Pero primero hay que crear la función que se va a encargar de borrar en nuestro controlador.
borrarTrabajo(indice: number) {
this.experienciaLaboral.removeAt(indice);
}
Se podrán dar cuenta que la función que cree se llama borrarTrabajo
y que recibe un índice. Este índice es el mismo índice que tiene el grupo en el arreglo y para conseguirlo desde la plantilla hay que modificar nuestro *ngFor
de la siguiente manera:
<div *ngFor="let trabajo of experienciaLaboral.controls; let i = index;">
Esa variable i
va a contener el índice, lo que le va a decir al método removeAt
cual elemento del arreglo tiene que borrar.
En la plantilla el botón para borrar va de la siguiente manera:
<button (click)="borrarTrabajo(i)">Borrar</button>
Y así es como ya se pueden añadir y borrar campos a nuestro formulario de manera dinámica.
El código de este ejemplo se ve muy sencillo, pero la primera vez que lo tuve que hacer se me complicó muchísimo. En la documentación no estaba muy bien explicado como funcionan los FormArray
, tampoco como se muestran en la plantilla. Mucho menos había explicaciones sobre como validar todo esto para ver si el formulario estaba correcto antes de mandarlo. Este post sólo es la primera parte de una serie que puede ser enorme y que estaré escribiendo poco a poco.
Aquí abajo les dejo el proyecto final en Glitch y lo estaré usando como base para los siguientes posts.
Espero que este pequeño ejemplo les sirva para entender mejor los formularios reactivos y dinámicos. Son herramientas muy útiles para hacer aplicaciones con funcionalidades más complejas. En la siguiente parte escribiré sobre validación y como crear validaciones dinámicas y como aplicarlas a diferentes partes del formulario.
Top comments (8)
core.mjs:7753 ERROR Error: Cannot find control with path: 'row -> check'
at _throwError (forms.mjs:1778:11)
at setUpControl (forms.mjs:1567:13)
at FormGroupDirective.addControl (forms.mjs:5337:9)
at FormControlName._setUpControl (forms.mjs:5893:43)
at FormControlName.ngOnChanges (forms.mjs:5838:18)
at FormControlName.rememberChangeHistoryAndInvokeOnChangesHook (core.mjs:1515:14)
at callHook (core.mjs:2568:18)
at callHooks (core.mjs:2527:17)
at executeInitAndCheckHooks (core.mjs:2478:9)
at selectIndexInternal (core.mjs:8515:17)
A mi me da este error
Hola,
Como puse al principio del post, este código fue hecho en Angular 7. Seguramente como ya vamos en el 15, las cosas cambiaron y hay partes de este código que ya no aplican. No he actualizado el post porque ya no hago Angular.
Gracias me ayudo mucho pero como se leerían los datos en el componente para mandarlo al back ?
Hola,
Para mandarlo al backend puedes usar
this.form.getRawValue()
y eso te da un objeto con campo: valor que puedes manipular de la forma que necesites para tu sistema.Pero siempre se debe agregar el formcontrolname en los inputs?
Sí! Así la forma sabrá de donde vienen los datos.
Muy buena explicación, Gracias!
me ayudo de mucho