Eventos en Polymer 2. Parte 5

Eventos en Polymer

Un evento nos permite ejecutar código cuando sucede algo, y Javascript tiene múltiples eventos para controlar ese ‘algo’, como son click, mousemove, keypress, keydown, etc.. y que seguramente has usado mas de una vez.

En todos los ejemplos que estamos desarrollando hasta ahora, la forma de crear los evento era de forma declarativa añadiendo el evento sobre el propio html.

Ahora vamos a crear eventos de forma imperativa, que básicamente es programar el evento usando Javascript.

Al definir los eventos de forma imperativa, podemos asignar cualquier tipo de evento no solo dentro del propio componente sino también al padre, que sería el propio componente.

Para hacer esa tarea vamos a definir los eventos usando Javascript nativo:

index.html

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">

  <title>Curso01</title>
  <meta name="description" content="Curso01 description">

  <!-- See https://goo.gl/OOhYW5 -->
  <link rel="manifest" href="./manifest.json">

  <script src="./bower_components/webcomponentsjs/webcomponents-loader.js"></script>
  <link rel="import" href="./src/sandbox/comp-evento1.html">
</head>

<body>
  <comp-evento1 valor='Click aquí'></comp-evento1>
</body>

</html>

comp-evento1.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<dom-module id="comp-evento1">
    <template>
        <style>
            :host {
                display: block;
            }
        </style>
        <p>[[valor]]</p>
    </template>
    <script>
        class ComponenteEvento1 extends Polymer.Element {
            static get is() {
                return 'comp-evento1';
            }
            static get properties() {
                return {
                    valor: {
                        type: String,
                        value: ''
                    }
                };
            }
            ready() {
                super.ready();
                this.addEventListener('click', this.clickHost);
            }
            clickHost() {
                alert("Click en componente");
            }
        }
        window.customElements.define(ComponenteEvento1.is, ComponenteEvento1);
    </script>
</dom-module>

Como indicaba anteriormente, el hecho de poder crear eventos usando Javascript nos permite poder crear también eventos fuera del propio componente.

Siempre que desarrollemos un componente y este use eventos fuera del componente deberemos eliminar ese evento cuando se elimine o desaparezca el componente que añadía ese evento.

De esta forma podríamos crear por ejemplo, un componente que capture las teclas pulsadas fuera de ese componente.

Para entender todo esto vamos a crear un vídeo con el comportamiento y el código fuente:

index.html

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">

  <title>Curso01</title>
  <meta name="description" content="Curso01 description">

  <!-- See https://goo.gl/OOhYW5 -->
  <link rel="manifest" href="./manifest.json">

  <script src="./bower_components/webcomponentsjs/webcomponents-loader.js"></script>


  <link rel="import" href="./src/sandbox/key-logger.html">
</head>

<body>

  <p>Usuario
    <input type='text'>
  </p>
  <p>Contraseña
    <input type='password'>
  </p>
  <key-logger></key-logger>
  <script>
    document.querySelector('key-logger').addEventListener('tecla', function (e) {
      console.log("Evento Padre. Se ha pulsado la tecla.....", e.detail.tecla);
    });

</body>

</html>

key-logger.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<dom-module id="key-logger">
    <script>
        class KeyLogger extends Polymer.Element {
            static get is() {
                return 'key-logger';
            }
            ready() {
                // Capturamos cualquier pulsación, tanto dentro como fuera de un elemento HTML
                debugger;
                super.ready();
                window.addEventListener('keyup', this.teclaPulsada.bind(this));
            }
            teclaPulsada(e) {
                // lanzamos el evento personalizado
                this.dispatchEvent(new CustomEvent('tecla', { detail: { tecla: e.key } }));
            }
            connectedCallback() {
                super.connectedCallback();
                console.log('connectedCallback');
            }
            disconnectedCallback() {
                super.disconnectedCallback();
                window.removeEventListener('keyup', this.teclaPulsada);
                console.log('disconnectedCallback');
            }
        }
        window.customElements.define(KeyLogger.is, KeyLogger);
    </script>
</dom-module>

Eventos personalizados

Para entender correctamente que son los eventos personalizados, deberíamos observar los eventos de javascript que ya conocemos, como mousemove, keypress, keydown, etc… 

En cualquiera de ellos cuando se lanza el evento se ejecuta una función asociada y ejecuta su código.

En polymer podemos usar todos los eventos de javascript nativo, pero puede ser que necesitemos algún evento que no este definido en javascript.

Por ejemplo, imaginemos una caja de texto, y la vamos a usar para realizar una llamada a un API REST, pero necesitamos que esa llamada solo se realice cuando pulsemos dos veces la tecla enter.

Como es de suponer en javascript no existe un evento ‘cuando pulses una tecla dos veces y que ademas se la tecla enter ejecuta este código‘. 

En el ejemplo anterior hemos creado un evento personalizado que se llama tecla, y que su función es capturar la tecla pulsada.

Cuando se ejecutan los eventos en javaScript, estas llamadas se hacen en forma de burbuja desde el elemento más interno hasta el más externo.

A este comportamiento se le denomina bubbling y es un comportamiento nativo de javascript, que solo se comporta así en los elementos que no tienen shadowDOM.

Cuando los componentes tienen shadowDOM los eventos suben de manera predeterminada y se quedan en el padre, no continúan propagándose.

Si queremos que los eventos de los componentes que tienen shadowDOM se comporten igual que en javascript nativo, deberemos añadir dos parámetros dentro del dispatchEvent. Los parámetros son bubbles y composed y debemos cambiar su valor a true.

En el siguiente vídeo se puede ver el comportamiento del bubbling sin los parámetros y con los parámetros:

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
  <title>Curso01</title>
  <meta name="description" content="Curso01 description">
  <!-- See https://goo.gl/OOhYW5 -->
  <link rel="manifest" href="./manifest.json">
  <script src="./bower_components/webcomponentsjs/webcomponents-loader.js"></script>
  <link rel="import" href="./src/sandbox/comp-abuelo.html">
</head>
<body>
  <comp-abuelo></comp-abuelo>
  <script>
    document.querySelector('comp-abuelo').addEventListener('mensaje', function (datos) {
      console.log(datos.detail.valor);
    });
  </script>
</body>
</html>

comp-abuelo.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../sandbox/comp-padre.html">

<dom-module id="comp-abuelo">
    <template>
        <comp-padre></comp-padre>
        <h1>[[who]]</h1>
        <button on-click='sendMessage'>Enviar mensaje</button>
    </template>
    <script>
        class CompoAbuelo extends Polymer.Element {
            static get is() {
                return 'comp-abuelo';
            }
            static get properties() {
                return {
                    who: {
                        type: String,
                        value: 'Abuelo'
                    }
                };
            }
            sendMessage() {
                this.dispatchEvent(new CustomEvent('mensaje', {
                    detail: {
                        valor: 'Soy el abuelo'
                    },
                    bubbles: true,
                    composed: true
                }));
            }
            ready() {
                super.ready();
                console.log('Ejecutándose ready abuelo');
            }
        }
        window.customElements.define(CompoAbuelo.is, CompoAbuelo);
    </script>
</dom-module>

comp-padre.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../sandbox/comp-nieto.html">
<dom-module id="comp-padre">
    <template>
        <comp-nieto></comp-nieto>
        <h1>[[who]]</h1>
        <button on-click='sendMessage'>Enviar mensaje</button>
    </template>
    <script>
        class CompoPadre extends Polymer.Element {
            static get is() {
                return 'comp-padre';
            }
            static get properties() {
                return {
                    who: {
                        type: String,
                        value: 'Padre'
                    }
                };
            }
            sendMessage() {
                this.dispatchEvent(new CustomEvent('mensaje', {
                    detail: {
                        valor: 'Soy el padre'
                    },
                    bubbles: true,
                    composed: true
                }))
            }
            ready() {
                super.ready();
                console.log('Ejecutándose ready padre');
            }
        }
        window.customElements.define(CompoPadre.is, CompoPadre);
    </script>
</dom-module>

comp-nieto.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<dom-module id="comp-nieto">
    <template>
        <h1>[[who]]</h1>
        <button on-click='sendMessage'>Enviar mensaje</button>
    </template>
    <script>
        class CompoNieto extends Polymer.Element {
            static get is() {
                return 'comp-nieto';
            }
            static get properties() {
                return {
                    who: {
                        type: String,
                        value: 'Nieto'
                    }
                };
            }
            sendMessage() {
                this.dispatchEvent(new CustomEvent('mensaje', {
                    detail: {
                        valor: 'Soy el nieto'
                    },
                    bubbles: true,
                    composed: true
                }))
            }
            ready() {
                super.ready();
                console.log('Ejecutándose ready nieto');
            }
        }
        window.customElements.define(CompoNieto.is, CompoNieto);
    </script>
</dom-module>

Con el ejemplo anterior hemos podido comprobar como funciona la propagación de un evento en polymer.

El patrón mediador

¿Que es el patrón mediador?. Es un patrón de diseño que nos permite una gestión más ágil dentro de una arquitectura de componentes.

¿Y como funciona?. La idea principal es tener un componente que centralice las comunicaciones del resto de los componentes, dejando todo el control sobre el componente mediador.

El patrón mediador nos indica una serie de pautas a seguir en el momento realizar la comunicación entre componentes:

  • Cuando nos comunicamos de los padres a los hijos, lo hacemos siempre por binding a las propiedades.
  • Cuando nos comunicamos de los hijos a los padres, los hacemos con un evento personalizado.

En el ejemplo anterior podemos comprobar como aplicamos este patrón, al realizar la llamada desde el hijo al padre usando un evento personalizado llamado mensaje, y para comunicarnos del padre al hijo lo hacemos bindeando la propiedad llamada who.

Eventos táctiles

Para finalizar el tema de eventos voy a explicar con algún ejemplo los eventos táctiles disponibles en polymer, ya que según las estadísticas desde 2017 la navegación móvil ha superado a la navegación de escritorio.

Estos son los siguientes evento:

  • down: Cuando se presiona.
  • up: Cuando se levanta.
  • tap: Cuando presionamos y levantamos.
  • track: Cuando arrastramos o movemos el ratón.

Para poder usar estos eventos necesitamos importar un fichero, ya que no esta incluido en el core de polymer.

A continuación un pequeño ejemplo:

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
  <title>Curso01</title>
  <meta name="description" content="Curso01 description">
  <!-- See https://goo.gl/OOhYW5 -->
  <link rel="manifest" href="./manifest.json">
  <script src="./bower_components/webcomponentsjs/webcomponents-loader.js"></script>
  <link rel="import" href="./src/sandbox/comp-gestos.html">
</head>
<body>
  <comp-gestos></comp-gestos>
</body>
</html>

comp-gestos.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/polymer/lib/mixins/gesture-event-listeners.html">
<dom-module id="comp-gestos">
    <template>
        <style>
            h1:nth-child(2) {
                user-select: none;
                cursor: pointer;
                border: 1px solid;
                padding: 40px;
                text-align: center;
            }
        </style>
        <h1 on-track='gesto'>Gestos</h1>
        <h1>Nº total de arrastres de izquierda a derecha...[[arrastrar]]</h1>
    </template>
    <script>
        class ComponenteGestos extends Polymer.GestureEventListeners(Polymer.Element) {
            static get is() {
                return 'comp-gestos';
            }
            static get properties() {
                return {
                    arrastrar: {
                        type: Number,
                        value: 0
                    }
                };
            }
            gesto(evt) {
                if (evt.detail.state === 'end' && evt.detail.dx > 150) {
                    this.arrastrar++;
                }
            }
        }
        window.customElements.define(ComponenteGestos.is, ComponenteGestos);
    </script>
</dom-module>