Comunicación entre componentes

Aquí vamos a tocar un tema que, aunque no es, estrictamente hablando, exclusivo de los formularios, es en estos donde se usa con mayor frecuencia y, una vez que lo dominemos, podremos usarlo en otras vistas, aunque no tengan formularios. Se trata de lo siguiente. Sabemos como pasar el valor de una propiedad de la lógica a la vista, o en sentido inverso… dentro del componente en el que estamos trabajando. Sin embargo, en ocasiones es necesario que un componente le pase valores a otro. Por ejemplo, imagina que tienes una vista que monta un formulario y quieres que, una vez correctamente cumplimentado, se grabe. Podrías tenerlo de forma que haya un componente con la misión específica de grabar datos. Así, todos los componentes que deban grabar datos se los pasarán a este componente específico, delegando en él la responsabilidad de la grabación. De esta forma, logramos un mayor desacoplamiento y una mejor escalabilidad y mantenibilidad del proyecto. O podrías tener el formulario en un componente, y la grabación en un servicio, como veremos en posteriores ejemplos.

En este artículo todavía no vamos a hablar de como se graban datos. Para eso aún nos falta un poquito. Lo que sí vamos a aprender es a pasar datos de un componente a otro, bien sea del mismo módulo, o de otro diferente.

ENTENDIENDO LA JERARQUÍA

Este primer punto que vamos a comentar es, probablemente, el más peliagudo de todo el tema. Cuando se pasan datos de un componente a otro, debe estructurarse la aplicación mediante una jerarquización de componentes. Dicho así no parece muy impresionante. Sin embargo, esta jerarquía es algo que se establece, en cierto modo, de forma implícita, lo que la convierte en algo que, al principio, vamos a ver como un poco abstracto. Eso hace que, hasta que nos familiaricemos con esta forma de trabajar, nos requiera un pequeño esfuerzo pensar en ello cuando creamos los componentes.

Cuando se van a pasar datos entre componentes, siempre existe un componente «padre», que recibe y pasa los datos desde y hacia los componentes «hijos». Es un poco el concepto clásico de maestro-esclavos, o listo-tontos. Algunos desarrolladores, por no incurrir en algo políticamente incorrecto, prefieren la nomenclatura controlador-presentadores. Lo llames como lo llames, la idea siempre es la misma: un componente hace las gestiones de comunicación entre los demás componentes y él mismo, y es el que decide quién pasa qué a quién, y cuándo lo hace.

Lo más abstracto de esto es que la forma de establecer cuál es el componente maestro y cuáles los esclavos viene dada por nuestro criterio a la hora de programarlo, pero no hay ninguna parte específica del proyecto Angular donde establezcamos la jerarquía, sino que viene dada por la programación que hacemos.

Nosotros, para este artículo, vamos a manejar tres componentes:

  • Un componente maestro, que decidirá los datos que pasan, desde dónde a donde, y cuándo se pasan.
  • Un formulario, donde el usuario podrá teclear algunos datos. Estos datos pasarán al componente maestro que, a su vez, hará «algo» con ellos.
  • Un listado de los datos que se hayan tecleado, permitiendo borrar los que deseemos, también mediante la comunicación con el componente maestro.

El formulario será muy simple, porque aquí ya no se trata de ver como trabajar con los formularios, si no, más bien, con los datos, y no quiero que los árboles nos impidan ver el bosque. Será un registro de socios, donde sólo se pedirán dos datos (el DNI y el nombre, por ejemplo). Además, el componente maestro y el esclavo que hace el listado estarán en un módulo, mientras que el formulario estará en otro. Podríamos haberlos creado todos en el mismo módulo. Incluso es posible que hubiera tenido sentido, desde un punto de vista práctico, crear los tres en un módulo específico para ello. Sin embargo lo he hecho así, porque la comunicación de datos entre componentes de distintos módulos es el escenario más sofisticado que podíamos concebir y, cuando dominemos esto, nos será todo mucho más fácil. Vale la pena un poquito de esfuerzo adicional ahora.

LA ESTRUCTURA DE LA APLICACIÓN

La aplicación que vamos a realizar para ilustrar el paso de datos entre componentes tendrá tres módulos:

  • El módulo GlobalsModule. Este contendrá los componentes que serán comunes a la aplicación que serán comunes a la aplicación, es decir, la barra de navegación, la cabecera de las vistas, la vista de inicio y la página de error 404.
  • El módulo MyFormModule. Este contendrá el componente que renderizará el formulario para la captura de datos. Este será un componente esclavo en la jerarquíz que vamos a establecer.
  • El módulo FormManagerModule. Este contendrá un gestor, que será el componente maestro de nuestro artefacto, y otro componente esclavo para listar los socios que se graben a través del formulario.

Y una cosa que debemos entender. En la barra de navegación habrá un enlace para acceder al formulario donde se graban los socios. Sin embargo, como hemos decidido que este es un componente esclavo, en el enrutador no se apuntará al formulario, sino al componente maestro que, a su vez, incorporará el formulario. Observa el archivo app-routing.module.ts:

Observa, en las líneas resaltadas, como el enlace socios apunta al componente que hemos decidido que será el maestro.

LOS COMPONENTES

Vamos a ver en este apartado como están construidos los distintos componentes de la aplicación. Lo primero que te llamará la atención es que vamos a usar dos decoradores nuevos, así como una clase específica propia de Angular. Los decoradores son @Input, que se utiliza para recibir datos de otro componente, y @Output, que se utiliza, junto con la clase EventEmitter, para enviar datos a otro componente. Para ello, en el TypeScript de los componentes que vayan a usar estos decoradores importaremos Input, Output y EventEmitter del núcleo de Angular. No te asustes. Son elementos nuevos, que necesitamos para hacer el intercambio de datos, pero no muerden. De hecho, una vez que les «tomas el pulso» son bastantes fáciles de emplear. Además, usaremos una clase nuestra propia, para definir los objetos que contendrán a los socios, o usuarios, de nuestra aplicación. Esta clase está en acd-classes/acd-user.ts y no tiene nada de especial. Es muy similar a las que usamos en los dos artículos anteriores, aunque más simple, por los pocos datos que vamos a manejar de cada socio.

En este apartado vamos a ver como funcionan los distintos componentes.

EL FORMULARIO

Este es, quizá el componente conceptualmente más simple. Es el que renderizará el formulario en la vista correspondiente, para que el usuario grabe datos. Está en MyFormModule, en un componente llamado SociosFormComponent. La parte de la vista (socios-form.component.html) te resultará extremadamente parecida a más de lo mismo de lo que ya hemos visto en otros artículos:

Cómo ves, no tiene nada que no hayamos visto ya en otras ocasiones. Si acaso, pueden llamarte la atención los dos contenedores de bootstrap declarados al final. Mediante unas variables booleanas que se manejan en el TypeScript, nos informan de si se han tecleado los dos datos (con lo que se procesaría la «ficha» del usuario) o si faltan datos. Estas variables se activan o desactivan cuando se pulsa el botón. Es una mini validación muy sencilla. Lo que sí es importante es que, dado que estamos usando la directiva ngModel, tenemos que importar, en nuestro módulo, un módulo llamado Forms, propio de Angular, tal como aprendimos en este artículo. Lo haremos en my-form.module.ts, como ya sabemos, así:

Además, es importante que repares en las líneas resaltadas al final. Este módulo contiene el componente del formulario (SociosFormComponent). Sin embargo, este componente debe ser visible desde el módulo que contiene al componente maestro, por lo que debe figurar en la lista de exportaciones. Si no lo haces así, el módulo GlobalsModule (que es el que contiene al componente maestro) no podrá ver el formulario, y no habrá forma de interconectarlos. Lo ideal, para evitarte problemas, es que cuando crees el componente SociosFormComponent emplees la opción --export. Si no, tendrás que crear tú la clave exports de forma manual. Dicho de otro modo. La forma correcta de crear el componente será:

ng g c MyForm/SociosForm --export

En realidad, esto no es nada nuevo. Ya sabemos que, en Angular, cuando un componente debe ser alcanzable desde otro módulo, tenemos que hacerlo exportable. Sin embargo, por ser algo que hemos visto casi al principio del curso, es muy fácil tender a olvidarlo. Ten cuidado con esas cosas.

Y ahora viene la parte más delicada: la lógica del componente. El archivo socios-form.component.ts contiene varias cosas con las que no estamos familiarizados:

Vamos a ir viendo qué es lo que hacemos, centrándonos en las líneas resaltadas, aunque incluiremos algún comentario que considero oportuno de otras líneas.

En primer lugar, mira la línea 1. Vemos que, además de importar, como ya es habitual, las clases Component y OnInit, importamos otras dos clases del núcleo de Angular: Output y EventEmitter. Como el formulario va a mandar datos a otros componentes, las necesitaremos. En seguida veremos como las usamos.

En la línea 2 vemos que importamos la clase AcdUser, que es la que nosotros hemos definido para crear los objetos que representan a los socios en acd-classes/acd-user.ts. Esto ya aprendimos a hacerlo en anteriores formularios, y no presenta ninguna novedad, pero no debemos olvidarlo.

Las líneas de la 10 a la 14 no presentan nada de particular. Simplemente, definimos dos variables para recoger los datos del formulario, otras dos para marcar los estados de que el formulario se ha cumplimentado correctamente o que faltan datos, que son las que harán aparecer o desaparecer las notificaciones que definíamos al final de la vista, y una variable que es un objeto de la clase AcdUser, para almacenar el usuario y luego poder enviar este objeto al componente maestro. No olvidemos que, en la jerarquía que hemos establecido, el formulario es un componente esclavo.

La línea 16 es más interesante. Utiliza el decorador @Output() para crear un objeto, al que hemos llamado enviarUsuario, de la clase EventEmitter, indicándole a esta clase cual será el tipo de objeto. Observa el nombre AcdUser entre < y >, y que finalizamos con (). El decorador @Output no envía nada en este momento. Simplemente es, por decirlo de forma simple, la puerta de salida del dato que querremos enviar. Sin embargo, aún tendremos que enviarlo. Observa la peculiar sintaxis de esta «puerta de salida». En seguida vamos a ver como funciona.

En el método saveRecord, que hemos creado entre las líneas 26 y 39 recibe los datos del formulario y hace una pequeña mini validación, comprobando si tienen o no contenido. Podriamos haber hecho una validación más exhaustiva, como cotejar el DNI con una expresión regular, o mirar que el nombre tenga una longitud mínima, pero este no es el tema del presente artículo, por lo que con lo que tenemos ya basta y sobra. La cuestión es que, si algún campo está vacío, se activa la variable que muestra el aviso de error, y aquí acaba todo. La parte que nos interesa es cuando el formulario pasa la validación. En primer lugar se crea un objeto de la clase AcdUser con los datos del formulario, en la línea 34. Esto en sí no tiene nada de particular, ni aporta ninguna novedad.

Lo interesante viene en la línea 35. Usamos el objeto enviarUsuario, de la clase EventEmitter, que creamos en la línea 16. Este objeto tiene un método llamado emit(), propio de la clase EventEmitter, que recibe como argumento lo que vamos a enviar. En este caso es el objeto User, de la clase AcdUser. Es fundamental que lo que vayamos a enviar sea del mismo tipo que delacaramos en la línea 16, cuando creamos el objeto EventEmitter. El método emit() «abre» la «puerta de salida» que es el decorador @Output, y envía, a través de dicha puerta, el argumento que le hemos pasado. Es en este momento cuando mandamos el objeto User (o el dato que sea en cada caso)… ¿a dónde?. Esa es una cuestión que, a la vista de este código no está aún respondida. ¿A dónde mandamos ese objeto? La respuesta corta es que lo mandamos a «quien esté escuchando». Y ¿quién está escuchando? Bueno. En primer lugar, tiene que ser un componente que «vea» nuestro formulario. Por las importaciones y exportaciones que hemos hecho, el formulario se puede ver desde FormManagerModule. Y ahí tenemos el componente maestro (SociosManagerComponent) y el componente que lista los socios (SociosListComponent). Alguno de los dos deberá recibir el objeto enviado desde el formulario y, por lógica, cabe pensar que será el componente maestro. Sin embargo, tenemos que hacer que lo reciba. No basta con enviarlo desde el origen, si no que el receptor debe estar enlazado de algún modo al origen, para recibir el dato.

EL «OTRO» MÓDULO

El «otro» módulo de la aplicación tiene dos componentes: el maestro (SociosManagerComponent), que es el que lo gestiona todo, y otro componente esclavo, llamado SociosListComponent, cuya misión será listar los socios que se hayan registrado a través del formulario, y darnos opción a borrarlos individualmente.

La lógica del módulo (form-manager.module.ts) es bartante simple, a estas alturas del curso:

Lo único que no debes olvidar es que, como aquí vamos a usar un componente que forma parte de MyFormModule, debemos importar este, para que sea alcanzable, como ves en las líneas 5 y 10. Por lo demás, el módulo, como envolvente, no tiene nada de particular. Todo está en los componentes.

EL COMPONENTE MAESTRO

Este es el más «potente» de nuestra aplicación. Se llama SociosManagerComponent y su vista (socios-manager.component.html) es la siguiente:

Vamos a empezar por la línea 2. Vemos que aquí es donde se implementa el formulario de registro de socio, mediante las etiquetas que llaman al selector del componente del formulario. Hasta aquí casi nada nuevo. Desde la vista de un componente llamamos a otro a través de su selector, como ya hemos hecho innumerables veces. Sin embargo, la cuestión está en el «casi». A la etiqueta del selector le hemos puesto un binding de evento, que llama al evento enviarUsuario. Y aquí es donde establecemos las relaciones entre el formulario (SociosFormComponent) y el componente maestro (este mismo, SociosManagerComponent). El caso es que enviarUsuario es el objeto de la clase EventEmitter que definimos en la puerta de salida de socios-form.component.ts. Así cuando el componente del formulario ejecuta el método emit(), lanza un evento que es capturado por el binding de eventos de la vista del componente maestro, desdencadenado un método al que hemos llamado mostrar(). Este está en la lógica del componente maestro, y en seguida vamos a verlo. Sin embargo, quiero que veas que recibe, como argumento, $event. Y eso ¿qué es?. Pues $event es una variable particular de Angular que recibe el dato que envió el método emit() a través de la puerta de salida @Output. Como el método emit() envió un objeto llamado User, de la clase AcdUser, eso es, justamente, lo que se coloca, de modo automático, en $event, y eso es lo que pasa al método mostrar().

Para no liarnos, vamos a ver ahora sólo una parte de socios-manager.component.ts. Precisamente, la que contiene el método mostrar():

A través del argumento (que, aqui, se llama evento, pero que se puede llamar como tú quieras) recibimos el objeto usuario que se alojó en $event. Implementamos un objeto User -propio de este componente- y lo almacenamos en una matriz. Este objeto User permite que en la vista (socios-manager.component.html) se vean los datos del último registro que ha llegado, en las líneas de la 8 a la 13.

Es decir. El componente maestro hace un binding de eventos pero, en lugar de detectar un evento del propio componente (como habíamos hecho hasta ahora), captura un evento de otro componente. A través del @Output del otro componente, a este le llega el objeto User, que se aloja en $event, y que podemos emplear en la lógica del componente maestro.

Soy consciente de que, a estas alturas, el proceso parece enrevesado y, seguramente, tendrás que revisar el código entero, y releer este artículo, un par de veces o tres. Sin embargo, cuando veas la mecánica clara, te aseguro que es extremadamente simple. A título meramente anecdótico, creo recordar que entender este proceso me llevó tres o cuatro semanas de trabajo, pruebas y errores. Sin embargo, una vez lo ví, es algo que podría hacer con los ojos cerrados.

EL LISTADO DE USUARIOS

El listado de usuarios corre a cargo de otro componente esclavo, al que hemos llamado SociosListComponent. Si te acuerdas, hace un momento, en la lógica del componente maestro, hemos guardado el usuario recibido en una matriz. Bien. Pues esta matriz la va a requerir SociosListComponent para listar, en tiempo real, todos los usuarios (socios) que se hayan registrado.

Observa, en la vista del componente maestro (listada más arriba) que integramos, también a través de una etiqueta de selector, el compomente que hace el listado (línea 17 de la lista, que integra acd-socios-list). Esta etiqueta también tiene algo de particular. Le hemos añadido un binding de propiedades. Las propiedades UsersArray y registeredUsers entre corchetes son de SociosListComponent, mientras que los valores de UsersArray y registeredUsers son de la lógica de SociosManagerComponent. Es decir, estamos pasando estas propiedades a otras homónimas desde el componente maestro al esclavo.

La lógica del componente esclavo (socios-list.component.ts) debe estar preparada para recibir estas propiedades:

Vamos a ver que tenemos aquí. En primer lugar, está la línea 1, donde se importan las clases Input, Output y EventEmitter del núcleo de Angular. La clase Input es para poder recibir los datos que el componente maestro envía a este componente. Fíjate en las líneas 10 y 11, donde usamos el decorador @Input. Del mismo modo que decíamos que @Output es una puerta de salida de datos, @Input podemos considerarlo como una puerta de entrada de datos.

Y ¿por qué aquí usamos @Input para recibir los datos del componente maestro y, en cambio, en aquél no lo usamos para recibir los datos del formulario? La respuesta no tiene nada que ver con la jerarquía de componentes. Esto es algo que a mí, en su momento, también me costó entender. La razón es la forma en que se comunican los datos desde el componente de origen hasta el de destino. En el formulario teníamos un @Output, que se encargaba de mandar los datos. Por lo tanto, ya no hacía falta un @Input en el destino. Sin embargo, ahora los datos llegan mediante binding de propiedades (recuerda la línea 17 de socios-manager.component.html). Esto implica que en el destino hay que «abrir una puerta de entrada» para esos datos. Si no, no puede llevarse a cabo el binding.

En la vista del componente de listado (socios-list.component.html) tenemos lo siguiente:

Empezamos mostrando, si los hay, los datos de los socios registrados. Esta información (las variables UsersArray y registeredUsers) llegan, a través del binding del componente maestro, entrando en la lógica del componente de listados por las puertas de entrada @Input. Una vez que entendemos que, a través de @Input tenemos acceso a estos datos, lo demás es una simple interpolación de variables y unas directivas estructutrales, temas, ambos, que ya conocemos.

Lo más llamativo es el botón de borrado de cada usuario individual, en la línea resaltada, al final de la vista. Fíjate que tenemos un binding del evento click que llama al método deleteUser(), pasándole, como argumento, UserItem, que es un objeto que representa al usuario actual en un bucle, es decir, un objeto de la clase AcdUser.

El método deleteUser() está definido en la lógica del componente esclavo que hace el listado (en las líneas de la 19 a la 21), y lo único que hace es enviar el usuario que le hemos pasado como argumento a otra puerta de salida, para mandarlo al componente maestro. Y ¿por qué al componente maestro? Pues porque en la vista de este, en la misma línea 17, tenemos un binding de eventos que captura el evento eraseUser, que pertenece a SociosListComponent, y efectúa el borrado, sacando a ese usuario de la matriz, como se ve en las líneas 24 a 28 de socios-manager.component.ts.

CONCLUYENDO

Los puntos relevantes para entender toda esta lógica (y, después de leer esto te recomiendo que releas el artículo entero con esto en mente) son los siguientes:

El componente que es invocado desde el menú es siempre el controlador o maestro. Este, a su vez, integra los esclavos. Los esclavos solo recogen datos que les da el maestro, o se los envían, pero no los procesan. Es el maestro el que asume la responsabilidad del procesado de los datos. De esta forma, cuando tengamos que modificar ese procesado, no necesitamos andar buscando dónde se lleva a cabo la parte que necesitamos. Todo el procesado se lleva a cabo en un solo componente. Los componentes esclavos no procesan los datos.

El formulario recoge los datos tecleados, monta el objeto, y lo manda al componente maestro, que es el que lo almacena en la matriz y lo pone a disposición del componente de listados, que es otro esclavo.

El componente de listados te muestra los botones para borrar, pero no borra nada. Manda el usuario afectado al componente maestro, que es el que lo saca de la matriz.

Es decir. Todas las acciones relevantes sobre los datos están en el componente maestro. Los componentes esclavos sólo lanzan peticiones, y datos, o reciben datos del componente maestro, pero es este el que gestiona todo.

De hecho, para ser estrictos, incluso la minivalidación que hacemos en el formulario, deberíamos haberla hecho en el componente maestro. Incluso, podríamos haber añadido otra validación más compleja, para ver si, por ejemplo, el DNI tecleado está repetido. Todo esto se debería haber hecho en el componente maestro, mandando los resultados al componente esclavo del formulario, donde se nos mostraría el resultado. La idea, para hacer un sistema así, es que el componente maestro se ocupe de la mayor cantidad de operativa posible, de forma que los componentes esclavos no tengan que ocuparse de nada. Aquí he dejado la validación en el formulario, por no complicar más las cosas.

La aplicación completa para que puedas ver todo el código (en el texto sólo he reproducido una parte), la puedes descargar en este enlace. Además, este artículo lo he hecho sobre un proyecto Angular nuevo, para que no te líes con el arbol de componentes. De todas formas, si quieres ver lo mismo en el contexto de la aplicación que hemos ido construyendo con varios formularios en artículos anteriores, para que veas cómo se puede integrar todo, te la dejo en este otro enlace. Te aconsejo que empieces por analizar la primera, que es más simple, porque, al principio, este tema cuesta un poco de asimilar.

Y, ya puestos, te he dejado un regalito en este otro enlace. Se trata de lo que te comentaba hace un momento: este mismo ejercicio, pero haciendo la validación de datos en el componente maestro, que es lo que nos aconsejan las buenas prácticas de programación. Te dejo a ti la tarea de analizar el código, ver como se establecen las relaciones entre componentes y como la validación del maestro afecta al formulario. Verás que, realmente, no hay nada de lo que no hayamos hablado en el texto del artículo. Es más de lo mismo, pero analizarlo te servirá como entrenamiento para familiarizarte con estas técnicas.

Nos vemos en el siguiente artículo.

   

Deja un comentario