Formularios reactivos
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 cualquier entrada se representa 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.
Ya no necesitaremos tener un objeto del tipo Product en el componente. Crearemos el objeto al vuelo con la información interna del formulario cuando vayamos a enviarlo al servidor. 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 el HTML (veremos más adelante como ponerlos). El atributo name puede omitirse al contrario que con los formularios de plantilla.
Convertir la imagen en base64
Para convertir la imagen a base64, seguiremos exactamente el mismo método que con los formularios de plantilla, utilizando el evento change en el input del archivo.
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.
Para modificar valores del formulario desde código, usamos el método setValue del objeto FormGroup si queremos modificar todos los valores, o patchValue si queremos modificar solo algunos campos.
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.
En los formularios de plantilla, esto se hace llamando al método resetForm del objeto NgForm.
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 de los campos 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.
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, 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
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.
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.
Reaccionando a cambios en el valor
Los objetos del tipo FormControl, FormGroup y FormArray tienen una propiedad observable llamada valueChanges. Si nos suscribimos a este observable podemos recibir el valor cada vez que cambie y ejecutar una función asociada.
Esto mismo se puede hacer con los formularios de plantilla referenciando el objeto NgModel, NgModelGroup o NgForm en el componente (con @ViewChild como ya vimos anteriormente), se puede hacer lo mismo, ya que estos objetos también tienen la propiedad valueChanges.