portada

Bus de Eventos. Conectar componentes hijos con hijos


Creamos un bus de eventos para comunicar componentes de tipo hijo entre sí



Qué es un bus de eventos en javascript.


Un bus de eventos frontend es una forma de comunicación entre componentes del DOM que permite que los componentes puedan comunicarse entre sí cuando no cumplen una relación directa de padre-hijo.


Cuando desarrollamos frontend utilizando el paradigma de Programación Orientada a Componentes ( PoC ) solemos utilizar eventos para comunicar con las capas/componentes superiores. Estos eventos son acciones que se desencadenan en un punto dado del árbol DOM pero no necesariamente nos estamos refiriendo a eventos nativos del API de comunicación con el navegador. Esto es, no sólo nos referimos a los eventos tipo ‘click’, por ejemplo, también nos referimos a los eventos personalizados que no responden a los nativos pero que se propagan de la misma forma.


Y es la forma en la que se propagan las acciones de tipo evento la que limita la comunicación entre capas profundas que no están directamente conectadas. Vamos a entenderlo con un ejemplo.


Cómo se propagan las acciones de tipo evento a través del árbol DOM


ejemplo de capas eventos

Imaginemos que tenemos un árbol DOM muy sencillito con un contenedor global que puede ser el propio documento ( para establecer el elemento superior a todos ), una capa intermedia denominada contenedor y el elemento objetivo del evento click que será un botón.


En la imagen hemos representado estos elementos, no como contendores sino como capas con el fin de entender la captura y propagación de los eventos.


Cuando hacemos click o touch en el botón lo que en realidad estamos pulsando no es el botón sino la capa superior a todas que queda por encima. Esto, en nuestro ejemplo es el Document. Y a su vez, al estar solapadas, recibe esa pulsación o click la siguiente capa que en este caso es el div Contenedor para finalmente llegar al Botón, el cual es el objetivo del evento.


ejemplo de capas eventos 2

Normalmente, los elementos del DOM que tienden a ser el objetivo del eventos de usuario suelen estar en capas inferiores como el caso de los botones. Por ello, estos eventos pueden burbujear. Este concepto significa que una vez capturado el evento por el elemento destinatario ( objetivo ) puede devolverlo hacia las capas superiores realizando el efecto que realiza una burbuja que se escapa desde la profundidad de un líquido hacia la superficie.


ejemplo propagacion de eventos

La propiedad de burbujear ( bubble ) no es una propiedad del elemento objetivo sino del propio evento. Hay eventos que de forma inicial la tienen activada como en el caso del evento click. En cualquier caso esta propiedad puede ser modificada para cualquier evento.


Por lo tanto, el evento se desplaza, en un primer momento, desde el componente padre que está por encima hasta el componente hijo objetivo del evento para después propagarse en sentido inverso, pasando en ambos sentidos por las demás capas/componentes intermedias. Sin embargo, cuando definimos la escucha de eventos con el método addeventlistener, este es definido para escuchar el burbujeo del evento por defecto. Para definir un escuchador de evento en el ciclo de captura debemos pasar un tercer parámetro de tipo booleano:


element.addEventListener(‘click’, (event)=>{
        //… hacer algo
    }, true);
    


Propagación por el DOM


Ahora veamos cómo se propaga representado en un árbol:


propagacion del eventos entre elementos del DOM

De esta forma podemos ver los elementos involucrados que son capaces de escuchar el evento generado. ¿Pero qué pasa si queremos que escuche el mismo evento el componente hijo de color verde? Tendríamos que escuchar el evento en algún componente que sí pueda escucharlo y definir el comportamiento para el componente hijo de color verde. Esto supone un fuerte acoplamiento a la estructura y la necesidad de conocer desde un punto del dom el resto de la estructura. ¿Qué pasa si en otros desarrollos ese elemento se quita del árbol? ¿ Y si quitamos el elemento donde se definió el escuchador? Estaríamos creando un código malo difícil de mantener.


Para solucionarlo podemos proponer que los componentes hijos se comunique en otra capa transversal simulando una comunicación por eventos como la que propone el API para eventos del DOM. Esto es a través del Bus de Eventos.


propagacion del eventos con un bus de eventos

En la imagen se muestra la representación de cómo un componente emite un evento que es recogido en el bus de eventos y este se lo comunica a los componentes interesados.


Cómo crear un Bus de Eventos con Javascript.

Vamos a crear un archivo html y dentro de él creamos la siguiente estructura base:


<!DOCTYPE html>
    <html>
    <body>
        <div>
            <button id="boton">click</button>
        </div>
        <div>
            <h1 id="titulo"></h1>
        </div>
    </body>
</html>
    

Seguido al último Div vamos a creamos una etiqueta scripts que contendrá la definición del bus de eventos declarado con la sintaxis de clases:


   <script>
        class EventBus {
            constructor() {
                this.suscripciones = new Map();
            }
            suscribir(tipo, callback) {
                if (!this.suscripciones.has(tipo)) {
                    this.suscripciones.set(tipo, new Map());
                };
                let id = Date.now() + Math.random();
                this.suscripciones.get(tipo).set(id, callback);
                return {
                    cancelar: () => {
                        this.suscripciones.get(tipo).delete(id);
                    }
                }
            }
            emitir(tipo, evento) {
                if (this.suscripciones.has(tipo)) {
                    this.suscripciones.get(tipo).forEach((callback) => {
                        callback(evento);
                    });
                }
            }
        };
    </script>
    

Repasando la clase creada tenemos un constructor que crea un nuevo objeto con la propiedad suscripciones que es de tipo Mapa. Al no crearla como una clase estática podemos utilizarla para crear varios bus dentro de la misma aplicación si fuese necesario. Luego tenemos dos métodos, suscribir y emitir. El primero registra en el mapa las funciones callback que se lanzarán cuando se publique un evento del tipo esperado y a la vez devuelve un objeto con un método, cancelar. Este método borra del mapa el callback para mantener el bus cuando este callback ya no sea necesario.


EL método emitir será el encargado de lanzar el evento. Recibe el tipo de evento y un objeto con los parámetros que esperan recibir los callback definidos mediante el método publicar.


Ahora implementamos la utilización de nuestra nueva clase:


const eventBus = new EventBus();
 
const suscripcion =  eventBus.suscribir('saludar', (data) => {
     document.querySelector('#titulo').innerText = data.detalle;
 });

 document.querySelector('#boton').addEventListener('click', (event) => {
     eventBus.emitir('saludar', { detalle: 'Hola Mundo!!!!' });
 });
    

Creamos un objeto de tipo EventBus. Establecemos la primera suscripción para el evento “saludar”, el cual introduce en la etiqueta H1 que habíamos creado y le introduce el valor almacenado en la propiedad detalle del parámetro pasado. Luego creamos un escuchado para para el evento click en el botón. Y en este listener establecemos que se emita el evento saludar en nuestro bus de eventos. Recibe el nombre como primer parámetro y el objeto con la propiedad detalle.


Podemos probarlo abriendo el archivo html en un explorador y al pulsar en el botón veremos el saludo creado:


holamundo

Podemos hacer otro ejemplo simulando que se hace una llamada a un APi asíncrono mediante una promesa. Para ello ampliamos el script con las siguiente líneas de código:


   const llamada = new Promise((success) => {
        setTimeout(() => {
            success({ detalle: 'Hola desde la Llamada' });
        }, 1000);
    })
    llamada.then((data) => {
        eventBus.emitir('saludar', data);
    })
    

Si recargamos la página ahora tendremos un mensaje saludando desde la llamada. Si pulsamos en el botón cambiará el mensaje a Hola Mundo.


Por último vamos a crear un ejemplo donde utilizar el método cancelar. Primero creamos otro div con otro h1 y le damos el id titulo2.


    <div>
        <h1 id="titulo2"></h1>
    </div>
    

y ahora registramos una nueva suscripción:


   const nuevaSuscripcion = eventBus.suscribir('saludar', (data)=>{
        document.querySelector('#titulo2').innerText = data.detalle;
        // nuevaSuscripcion.cancelar();
    })

    

Como se ve tenemos la cancelación comentada. Así, si lo lanzamos tal cual podemos ver como los dos títulos se actualizan al unísono con el mensaje. Con esto estamos probando como al emitir un evento en un punto dado del DOM podemos actualizar los elementos mediante la suscripción al evento. Ahora vamos a descomentar el código y recargamos la página. En este caso, inicialmente, los dos títulos mantienen el mismo mensaje pero al pulsar en el botón solo cambia el primer título. Esto es porque en la suscripción del segundo título estamos estableciendo que al ejecutarse el callback se cancele esa misma suscripción por lo que al emitir la segunda vez el evento y a no se ejecuta la suscripción cancelada.


imagen ejecución en el esplorador

Con esto ya hemos visto de forma sencilla pero funcional cómo y por qué implementar un Bus de Eventos que nos permita interconectar elementos sin estar en la misma jeraquía directa.


Este sistema es utilizado por frameworks como Angular o Vue. En librerías como React utilizan otras soluciones como los Context pero dependiendo de la aplicación y construcción, utilizar un bus de eventos con esta biblioteca también es una muy buena opción.