Formularios complejos

En este artículo vamos a empezar a darle a nuestro formulario mayor complejidad, añadiendo más tipos de campos y funcionalidades. Veremos como conectar la vista con el modelo de datos de una forma más eficiente (aunque también, inherentemente, más compleja) y como optimizar la gestión general del componente.

En definitiva, se trata de poder diseñar un formulario que satisfaga todas las necesidades que podamos tener para mejorar la experiencia del usuario. Al mismo tiempo, debe poder cumplir especificaciones operativas, e integrar los datos con el objeto al que van detinados.

Lo que vamos a ver en este artículo optimiza mucho la creación de formularios, mediante el uso de directivas propias de Angular. Al usarlas, optimizamos código y aligeramos carga de trabajo.

LO QUE VAMOS A HACER

Esto es lo primero que tenemos que hacer: plantearnos qué es lo que vamos a hacer. Suena a perogrullada, pero es muy difícil dar una respuesta sin tener clara la pregunta. Bien. Lo que quiero es que montemos un formulario complejo, y por complejo me refiero a que:

  • Tenga datos de distintos tipos.
  • Sea configurable por TypeScript.
  • Actualice los datos del modelo en tiempo real, según el usuario los actualice en la vista.
  • Exista un binding bidireccional entre la vista y el modelo. Hemos aprendido a hacer binding de propiedades (desde el modelo a la vista) y de eventos (de la vista al modelo). Ahora conoceremos el doble binding.
  • Finalmente, almacene los datos en un objeto que luego, en una aplicación real, sería enviado al servidor.

Todo esto dicho así parece muy sencillo, y lo será si empleamos las técnicas adecuadas que Angular nos ofrece. El aspecto del formulario que queremos crear es el que vemos a continación:

Cómo ves, tenemos campos de distintos tipos, y un botón que, por defecto, aparece inhabilitado. Cuando todos los campos estén cumplimentados se habilitará para ser pulsado. Esto nos muestra ya una especie de tosca prevalidación. Más adelante hablaremos sobre validación de formularios. De momento, vamos a aprender a crearlo.

PREPARANDO EL TERRENO

Antes de empezar a montar el formulario debemos prepararnos un poco el terreno. Una de las cosas que hemos mencionado es la técnica de doble binding. En este artículo vamos a aprender lo que es, y cómo hacerla, mediante una directiva de Angular llamada ngModel. Antes de aprender a emplearla debemos saber que, en cualquier módulo en el que uno de sus componentes vaya a empelar ngModel debemos importar el módulo FormsModule (propio de Angular), en nuestro propio módulo. Lo vamos a importar en my-forms.module.ts, así:

En la línea 3 vemos como lo importamos desde @angular/forms y en la línea 13 vemos como lo añadimos a la clave imports del decorador de nuestro módulo.

Lo siguiente que tenemos que hacer es crear una clase para almacenar los objetos que representen a los usuarios. Ya en el artículo anterior hicimos esto, con muy pocos datos. Esta vez nuestra clase va a definir unos objetos con más propiedades. En la terminal de VSC tecleamos:

ng g class amf-classes/AmfFullUser

Con esto se creará el archivo amf-clases/amf-full-user.ts, que modificaremos para dejarlo así:

En las líneas de la 2 a la 10 vemos que declaramos las propiedades del objeto. Como ya es habitual, las encapsulamos declarándolas como privadas. Accederemos a ellas a través de los métodos setter y getter que declaramos en las líneas de la 24 a la 78. El constructor crea la estructura básica de un objeto, rellenando el identificador con el método uniqueId, y la fecha de creación con el método getActualDate, ambos definidos en la propia clase.

En el componente donde vamos a usar esta clase (en este ejemplo, Form03Component) debemos importarla. Lo hacemos en la lógica (form03.component.ts), añadiendo la siguiente línea:

Ahora sí. Ya tenemos todo preparado para empezar a contruir la lógica y la vista de nuestro formulario.

MONTANDO EL FORMULARIO

Aquí empieza la parte interesante. Es, para ser la primera vez que montamos un formulario con estas presataciones, un poco como bailar en dos pistas, porque tenemos que ir viendo puntos de la lógica y de la vista del componente, y como se relacionan entre sí. Para facilitarte la comprensión, te voy a ir detallando uno a uno, reproduciendo fragmentos de ambos elementos. Si deseas verlos en contexto, en el código completo, ya sabes que al final del artículo te dejo el enlace de descarga para que lo tengas a mano, y puedas experimentar.

En la lógica nos vamos a centrar en la parte donde se define la clase del componente, que es la que nos afecta a nosotros ahora. Lo primero que ves, en la línea 11, es que declaramos una variable, a la que he llamado User. Esta será luego el objeto que almacenará el usuario. De momento, simplemente la declaramos, así:

Lo siguiente que hacemos es crear unos placeholders para los campos de texto. Esto no tiene nada nuevo. En el artículo anterior ya lo hicimos. Sólo que ahora son más. Los ves entre las líneas 13 y 19, en form03.component.ts:

Ahora tenemos que declarar unas variables en las que se irán almacenando provisionalmente los datos que teclee el usuario, según los vaya tecleando. Como te digo, estas variables son provisionales. Luego, cuando el usuario pulse el botón para grabar el formulario se almacenarán en el objeto User. Las declaramos en un arreglo al que hemos llamado userData, entre las líneas 21 y 30, así:

A continuación vamos a declaraar una serie de variables que definirán los campos de tipo radio button que usamos en el formulario para que el usuario indique su género. Como son sólo dos opciones, hemos optado por este tipo de campos. Lo que hacemos es declarar todas las propiedades de ambos objetos. Que nadie me tilde de machista por haber marcado como activado por defecto el que corresponde a «Hombre». Tenía que marcar uno por defecto, para no dar opción a que en formlario no se marque ninguna opción. La definición de los campos aparece entre las líneas 32 a 47:

Ahora vamos a definir las propiedades del selector de continente. Esta vez hemos escogido un selector desplegable porque hay varias opciones de las que se deberá escoger una. Las propiedades de las opciones del selector las hemos declarado entre las líneas 49 a 86, así:

Por último (de momento), declaramos una variable que determinará el estado de activado o desactivado del botón de grabación, en la línea 88, así:

En la vista (form03.component.html) el valor de esta variable se asigna a la propiedad disabled del botón, como vemos en la línea 65 de dicha vista:

En realidad, esta parte no es nueva. Aprendimos a usar el binding de propiedades, con valores booleanos, en este artículo. Esto es mas de lo mismo.

Y, ya que estamos, vamos a ir viendo como hacemos el formulario en la vista. Lo primero que nos llama la atención es la etiqueta que inicia, precisamente, el formulario. La hemos puesto un binding de eventos (puedes leer sobre el tema en este artículo), de forma que, cuando se produzca un cambio en los valores del formulario se llame a un método de la lógica que comprobará si el botón tiene que permanecer desactivado, o debe activarse para poderse pulsar. Lo ves en la línea 3, así:

El método CheckButton está definido en la lógica del componete, entre las líneas 90 y 101:

Como ves, lo que hace es poner en botonDesactivado el resultado de comprobar si alguno de los campos está vacío, o si la clave no coincide con la confirmación. Y aquí es donde la cosa empieza a ponerse interesante. Realmente no estamos comprobando el valor de los campos, sino de las variables temporales que declaramos en el arreglo userData. Así pues, hay que lograr que cuando el usuario teclee algo en alguno de los campos, la correspondiente variable se actualice en tiempo real, de modo que, cuando se llame a este método, la variable tenga el valor tecleado por el usuario. Vamos a ver como lo hacemos, por ejemplo, con el primer campo (el que corresponde al nombre real del usuario). Lo tenemos declarado en la línea 7 de la vista:

Es posible que te choque la parte en la que usamos la directiva ngModel para enlazar la propiedad userData.realName con el valor de este campo. Si recuerdas el artículo sobre binding de propiedades quizá estés pensando que lo suyo habría sido definir el campo así:

Esto, que, en otras circunstancias, podría valer, aquí nos da un problema. Para la propiedad value del campo se toma el contenido de la variable en la lógica (que, inicialmente, es una cadena vacía). Es un binding «hacia arriba». Sin embargo, cuando el usuario teclea algo, no se actualiza la variable en tiempo real. Para eso haría falta un binding «hacia abajo», es decir, un binding de eventos, con el evento keyup, o change, o similar. En todo caso, complicaría la escritura de la etiqueta, y nos obligaría a añadir más lógica. Usar la directiva ngModel enlaza directamente el campo de la vista con la propiedad de la lógica en tiempo real. Es decir. Si la variable cambiara dentro de la lógica, se vería inmediatamente el cambio en el valor del campo, y si el usuario teclea algo en el campo, se actualiza inmediatamente la variable en la lógica. Por eso ves que la directiva ngModel se encierra entre los dos signos de binding: el binding de eventos con los paréntesis, y el binding de propiedades con los corchetes. Esto hace que se produzca un doble binding, hacia arriba y hacia abajo, simultáneamente, en tiempo real. Esta notación (que siempre se hace con los paréntesis dentro de los cochetes, nunca al revés) se conoce, en el mundillo de Angular, con el pintoresco nombre de banana in a box. Cosas de los americanos.

Gracias a esto, las propiedades de userData en la lógica se actualizan en tiempo real. Y como, cada vez que se produce un cambio en el formulario, se comprueba cual debe ser el estado del botón (gracias al binding del evento change del formulario), cuando todos los campos están cumplimentados, el botón se habilita. Observa que todos los campos que deben influir en la habilitación o inhablitación del botón usan la directiva ngModel con el doble binding, para mantener todas las propiedades de userData siempre actualizadas.

ATENCIÓN. Para que la directiva ngModel funcione correctamente, permitiendo hacer el doble binding que necesitamos, es necesario que se cumplan algunos requisitos:

  • Que se utilice la notación banana in a box.
  • Que el campo tenga establecida la propiedad name.
  • Que hayamos importado el FormsModule de Angular en nuestro propio módulo, como vimos en el apartado anterior de este mismo artículo.

MAS DETALLES SOBRE LA VISTA

Acabamos de aprender lo más importante de este ejecicio: el doble binding. Sin embargo, en la vista aún hay algunos detalles que debemos aprender. Por ejemplo, vamos a centrarnos en el selector desplegable que hemos usado para el continene. Mira como está declarado (líneas de la 36 a la 38):

En la etiqueta select usamos el doble binding, de modo que, cuando se cambie la opción elegida, se actualizará, como ya sabemos, en tiempo real, el valor de la propiedad userData.continent de la lógica. Sin embargo, lo que más nos interesa ahora es como se declara la lista de opciones. Observa que hemos usado la directiva estructural *ngFor para recorrer el arreglo donde definíamos las opciones en la lógica. Para cada iteración empleamos las propiedades value, selected, disabled y text del elemento sobre el que estamos iterando. Habíamos aprendido a usar esta directiva con una etiqueta ng-container, en un artículo anterior. Sin embargo, como ves, no siempre es necesario emplear esta etiqueta. Las directivas estructurales pueden aplicarse en otras etiquetas que nos convengan. En este caso, aplicada a la etiqueta option crea las opciones para cada uno de los elementos del arreglo sobre el que se itera.

También tenemos en la vista otras cosas interesantes. El grupo de botones de radio, por ejemplo. Aqu´´i usamos varios recursos que nos ofrece Angular. Mira como están definidos:

Observa, especialmente, como hemos usado las propiedades que definimos en el arreglo en la lógica. Incluso el identificador (el precedido por una #) se ha definido en la lógica, y se emplea mediante la interpolación de la correspondiente propiedad. Además, cada botón tiene su propia propiedad value, con lo que, al usar ngModel logramos que, cuando se haga clic sobre uno de los botones, se actualice la propiedad userData.gender en tiempo real con el value del botón pulsado.

Una vez que está correctamente cumplimentado el formulario, el botón de grabar queda activado. Al pulsarlo llamamos al método saveUser de la lógica. Este no tiene mayor misterio. Simplemente, crea un objeto de la clase AmfFullUser, y graba los valores de las variables temporales dentro. Lo vemos porque el objeto está montado en la vista con el pipe json, para serializarlo.

CONCLUYENDO

En este artículo hemos repasado algunas cosas que ya sabíamos, viéndolas desde un nuevo prisma, lo que aumenta nuestra perspectiva del framework. También hemos aprendido algunas cosas nuevaas. La más interesante, desde mi punto de vista, es el uso de la directiva ngModel para hacer doble binding. Es una técnica que, en formularios complejos, nos puede dar mucho juego. Otra cosa que hemos hecho es una tosca validación del formulario. Hasta que no están cumplimentados todos los campos, no se activa el botón. Por supuesto, esta no sería una validación útil en un caso real. El botón debería estar siempre habilitado y, si falta algo, deberíamos avisar al usuario mediante algún mensaje, por ejemplo. Eso haría el formulario más amigable. Podemos hacerlo por programación, con lo que ya sabemos (por ejemplo, con la directiva *ngIf). No obstante, existen técnicas avanzadas de validación, como los llamados Reactive Forms, que es un tema del que hablaremos más adelante en el curso.

Como siempre, el código completo de la aplicación, en su estado actual, lo tienes en este enlace.

   

Deja un comentario