Reactive Forms
Mientras que los formularios de plantilla son más sencillos de implementar a priori y requieren menos código (en la clase del componente), los formularios reactivos son más flexibles, dinámicos y más fáciles de probar (pruebas unitarias). Además, el código HTML de la plantilla queda más limpia. Un formulario se representa internamente mediante un objeto FormGroup, mientras que los campos se representan como un objeto FormControl.
Para empezar a usar formularios reactivos, en lugar de importar FormsModule, necesitaremos importar ReactiveFormsModule:
Crear un formulario reactivo
Lo primero que debemos hacer es crear un objeto FormGroup que representará todo el formulario. Podemos crear más FormGroups dentro de ese objeto que representen secciones (grupos de campos) de ese formulario. Cada elemento de entrada que necesitemos integrar (validar, obtener su valor, etc.) debe estar representado por un objeto FormControl.
No necesitaríamos tener un objeto que represente el producto a enviar en este caso (newProduct), ya que los valores los obtendremos del formulario a la hora de enviarlo. Lo único necesario será un string externo al formulario (imageBase64) para almacenar la imagen en formato base64, ya que el control del formulario que representa la imagen está vinculado al input de tipo archivo.
Como se puede observar, el constructor de FormControl puede recibir el valor por defecto. Si no establecemos ninguno, este valor sería null. Por ello, todos los campos están tipados con el tipo del valor establecido. Si no establecemos un valor por defecto lo podríamos tipar de la siguiente manera: new FormControl<number>(), por ejemplo.
Los valores de los objetos FormControl se tipan también con null. Es decir, si el campo almacena un string, el tipo será string | null. Esto es así porque cuando se resetea el formulario, por defecto, todos los valores se asignan a null. Si queremos que esto no sea así, y que se asigne el valor inicial al resetear, debemos añadir la opción nonNullable.
FormBuilder
El servicio FormBuilder permite construir formularios reactivos con una sintáxis más simple. A cada propiedad solamente se le asigna el valor, y automáticamente se construye un objeto de tipo FormControl con dicho valor. Si queremos que el formulario no tenga valores nulos, usaremos el servicio NonNullableFormBuilder en su lugar.
En los siguientes ejemplos utilizaremos este servicio para la construcción de formularios reactivos.
Vincular formulario en la plantilla
Para vincular el formulario en la plantilla usaremos la directiva formGroup en el formulario vinculada al objeto FormGroup en el componente. Los campos se vinculan con la directiva formControlName. No se ponen los validadores de los campos en la plantilla (veremos más adelante como ponerlos). El atributo name puede omitirse al contrario que con los formularios de plantilla.
Existe también la directiva formControl. La diferencia con formControlName, es que esta última recibe un string con el nombre de la propiedad del objeto FormGroup que representa el valor del campo, mientras que formControl requiere una referencia al objeto FormControl directamente.
Si estamos adaptando el código anterior con formularios de plantilla, debemos tener en cuenta que los validadores en la plantilla no funcionarán y debemos quitarlos. El código como el de enviar el formulario, por ahora lo dejaremos comentado.
Modificar valores desde código
Mientras que en los formularios de plantilla, cambiando el valor de la propiedad asociada con ngModel a un campo, cambia el valor del campo, en el caso de los formularios reactivos es un poco diferente la metodología.
También se puede modificar el valor de un campo accediendo a él (objeto FormGroup) y llamando al método setValue.
Resetear formulario
Resetear un formulario implica volver a los valores iniciales de los campos y además reiniciar los campos a pristine y untouched para eliminar los estilos de validación. Se le puede pasar un objeto con los valores de los campos que queremos establecer. Si no pasamos ningún valor, el campo se reestablece al valor inicial.
Envío del formulario
EL envío del formulario se gestiona exactamente igual que para los formularios de plantilla, mediante el evento ngSubmit. Los valores los tendremos que obtener del formulario (FormGroup) y construir el objeto tipo Product que enviaremos al servidor.
El método getRawValue nos devuelve un objeto con los campos del fomrulario (propiedades) y su valor. Tenemos que añadirle la puntuación (rating) y sustituir el valor de la propiedad imagen (imageUrl) por la imagen en base64 que hemos generado.
Validar formulario
Angular tiene validadores equivalentes a los que se utilizan en HTML para los formularios reactivos. Los validadores son métodos estáticos de la clase Validators (@angular/forms).
Para asignar un validador a un campo del formulario, en nuestro caso que estamos usando FormBuilder, declaramos el campo como un array de 2 valores. El primer valor será el valor inicial del campo, y el segundo un array con los validadores a aplicar. Si solo hay un validador, no haría falta especificar un array.
La directiva validationClasses la creamos en la sección anterior de Formularios de Plantilla, y contemplaba que el input tuviera el atributo y por tanto la directiva ngModel. Vamos a hacerla un poco más flexible, para poder utilizarla también con formularios reactivos.
Ya podemos añadirla a la plantilla del formulario
Mostrar mensajes de validación
Para acceder a las propiedades de validación de los campos el fomulario se puede acceder a partir del objeto del formulario, con la colección controls, a los objetos FormControl internos. Las propiedades para la validación son las mismas que usando NgModel con los formularios de plantilla. Vamos a ver un ejemplo con el campo de la descripción:
Crear validadores personalizados
Los validadores en formularios reactivos son funciones (del tipo ValidatorFn) en lugar de directivas. Estas funciones las podemos crear en el propio archivo del componente (puede ser un método del mismo), o en un archivo aparte si queremos poder reutilizarlas fácilmente. Esta función debe devolver una función de validación, equivalente al método validate en las directivas para los formularios de plantilla. Esta función recibe el objeto que representa el campo del formulario, y devuelve un objeto con el error o null si no hay error.
Validadores de grupo (FormArray)
Para crear un validador y aplicarlo a un conjunto de campos, primero debemos agruparlos. Para esto tenemos los objetos FormGroup y FormArray.
En el caso del ejemplo que vimos con un formulario de plantilla, donde validabamos que al menos estuviera marcado un día de la semana, como son varios input del mismo tipo seguidos (que representan los días de la semana), la mejor solución es utilizar un objeto FormArray. Creamos un objeto de este tipo con el método array del objeto FormBuilder.
Cuando el validador no recibe parámetros, no hace falta crear una función del tipo ValidatorFn que devuelva otra función de validación. Basta con crear directamente la función de validación (que recibe el campo del formulario por parámetro), y en el array de validadores poner el nombre de la función sin llamarla.
Validadores de grupo (FormGroup)
Si queremos crear una estructura agrupada con campos independientes (su propio nombre de campo, tipos diferentes, etc.) la mejor opción sería un objeto FormGroup. Esto sería equivalente a usar la directiva ngModelGroup en los formularios de plantilla.
Al crear subgrupos en el formulario, a la hora de acceder a los valores del formulario de arriba, nos devolvería una estructura como esta:
Si no queremos crear estas estructuras de grupos dentro del formulario, siempre podríamos ponerle el validador de grupo directamente al objeto (FormGroup) del formulario. Otra opción sería crear un validador simple para el segundo campo y pasarle la referencia al primer campo por parámetro.
Mejorar el canDeactivate del formulario
A lo largo del curso, hemos configurado un guard de tipo CanDeactivate para que pregunte cuando queremos abandonar la página del formulario. Al guardar el producto y redirigir a otra página utilizamos un booleano para controlar que no pregunte. Sin embargo, sería buena idea que no preguntase tampoco si el formulario no ha sido modificado.
Como en los formularios reactivos tenemos acceso al objeto FormGroup directamente en el componente, podemos consultar su estado, y entre otras cosas la propiedad pristine (no modificado), o su opuesta, dirty, que nos indica si el formulario (alguno de sus campos) ha sido modificado. Veremos más adelante como hacer lo mismo con formularios de plantilla utilizando View Queries.
Reaccionando a cambios en el valor
Los objetos del tipo FormControl, FormGroup y FormArray tienen una propiedad de tipo Observable llamada valueChanges. Si nos suscribimos a este observable podemos recibir el valor cada vez que cambie y ejecutar una función asociada.
Busqueda con retardo (debounce)
Utilizando las posibilidades de los observables, podemos implementar un campo de búsqueda con formularios reactivos. En este caso no sería un formulario, sino simplemente un objeto del tipo FormControl. Podemos reaccionar a los cambios de valor (valueChanges) y procesarlos utilizando las siguientes funciones de rxjs:
- debounceTime - Tiempo mínimo para dejar pasar el siguiente valor. Si se emite un nuevo valor antes de ese tiempo, el anterior es descartado y el contador reiniciado) ignorando los anteriores.
- distinctUntilChanged - Si el valor es igual que el último emitido, no lo deja pasar. Solo si ha cambiado.
- swtichMap - Devuelve un observable (la llamada al servidor para filtrar) y el observable actual intercambia su valor (texto de búsqueda) por el valor que emite el observable devuelto.
Con Angular 19 podemos utilizar la API rxResource para hacer nuestro código más reactivo.