Binding en Polymer 2. Parte 6

Binding en Polymer

El data binding (enlace de datos) conecta los datos de un elemento personalizado a una propiedad o atributo de un elemento en su DOM local. Los datos del elemento host pueden ser una propiedad o sub-propiedad.

En polymer existen diferentes tipos de bindeado:

  • Binding de 1 y 2 direciones
  • Binding a atributo
  • Binding 2 direcciones en elementos nativos
  • Binding a sub-propiedades
  • Binding a arrays
  • Computed binding

Binding de 1 y 2 direciones

El binding  de 1 direccion no tiene mas misterio, la comunicación va del padre al hijo y la forma de la declaración es la siguiente: [[propiedad]]

El binding de 2 direciones es un poquito más complejo que el data binding de 1 dirección.

La comunicación se realiza desde el padre al hijo, y si el hijo modifica alguna propiedad este se la notifica al padre.

En nuestro caso el paper-input al tener binding en ambos sentidos, notifica al padre y este recibe  todos los cambios, realizando la modificación de la propiedad.

Se usa muy bien de hijo a padre pero si necesitamos subir mas arriba ya no es buena practica, y la forma de la declaración es: {{propiedad}}

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>BINDING</title>
  <meta name="description" content="BINDING 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/binding-app/binding-app.html">
</head>

<body>
  <binding-app></binding-app>
</body>

</html>

binding-app.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
<dom-module id="binding-app">
  <template>
    <style>
      :host {
        display: block;
      }
    </style>
    <!-- Binding 1 dirección Padre a hijo -->
    <h2>Tu profesión es.... [[profesion]]</h2>
    <!-- Binding 2 direciones -->
    <paper-input label='Profesion 2 Way' value='{{profesion}}'></paper-input>
    <!-- Binding 1 direción -->
    <paper-input label='Profesion 1 Way' value='[[profesion]]'></paper-input>
  </template>
  <script>
    /**
     * @customElement
     * @polymer
     */
    class BindingApp extends Polymer.Element {
      static get is() { return 'binding-app'; }
      static get properties() {
        return {
          profesion: {
            type: String,
            value: ''
          }
        };
      }
    }
    window.customElements.define(BindingApp.is, BindingApp);
  </script>
</dom-module>

Ahora que entendemos como funciona el binding de 1 y 2 direcciones, es el momento de crear nuestros componentes para que puedan informar al padre.

Para realizar esta acción vamos a usar una opción dentro de la configuración de las propiedades y que su nombre es notify.

Con esta configuración a true, se comunica con el padre y le informa de los cambios.

A continuación un vídeo y el código fuente:

binding-app.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
<link rel="import" href="../comp-notify/comp-notify.html">
<dom-module id="binding-app">
  <template>
    <style>
      :host {
        display: block;
      }
    </style>
    <!-- Binding 1 dirección Padre a hijo -->
    <h2>Tu profesión es.... [[profesion]]</h2>
    <!-- Binding 2 direciones -->
    <paper-input label='Profesion 2 direcciones' value='{{profesion}}'></paper-input>
    <!-- Binding 1 direción -->
    <paper-input label='Profesion 1 dirección' value='[[profesion]]'></paper-input>
    <p>Comp notify {{}} 2 direcciones</p>
    <comp-notify mensaje='{{profesion}}'></comp-notify>
    <p>Comp notify [[]] 1 dirección</p>
    <comp-notify mensaje='{{profesion}}'></comp-notify>
  </template>
  <script>
    /**
     * @customElement
     * @polymer
     */
    class BindingApp extends Polymer.Element {
      static get is() { return 'binding-app'; }
      static get properties() {
        return {
          profesion: {
            type: String,
            value: ''
          }
        };
      }
    }
    window.customElements.define(BindingApp.is, BindingApp);
  </script>
</dom-module>

comp-notify.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<dom-module id="comp-notify">
    <template>
        <style>
            :host {
                display: block;
            }
        </style>
        <p>
            <b>Tu mensaje es [[mensaje]]</b>
        </p>
        <button on-click='delete'>Delete</button>
    </template>
    <script>
        class ComponentNotify extends Polymer.Element {
            static get is() {
                return 'comp-notify';
            }
            static get properties() {
                return {
                    mensaje: {
                        type: String,
                        // Definir notify porque sino el padre no se entera
                        notify: true
                    }
                };
            }
            delete() {
                this.mensaje = '';
            }
        }
        window.customElements.define(ComponentNotify.is, ComponentNotify);
    </script>
</dom-module>

Binding a atributo

El binding a atributos de HTML (href, class, etc…) se realiza con el ‘$‘, porque el binding a una propiedad se realiza con la sintaxis conocida [[]] {{}}.

¿Y porque es necesario usar $?, pues porque algunos atributos de HTML no entienden el binding de polymer, y la solución para que eso no pase es usando el $.

Tenemos que tener en cuenta que el binding para los atributos es de 1 dirección.

Aquí tienes un listado de las propiedades afectadas por el binding a atributo:

Atributo Propiedad Anotaciones
class classList, className
style style
href href
for htmlFor
data-* dataset
value value Solo para type=’number’

Binding 2 direcciones en elementos nativos

Para realizar el binding de 2 direcciones en elementos nativos necesitamos usar una sintaxis especial indicando el tipo de evento.

Esto ocurre porque el elemento nativo no esta pensado para enviar los datos al componente padre, por eso motivo tenemos que avisar a polymer usando una sintaxis especial.

Por ejemplo, en HTML existen propiedades cuyo valor sí cambia en respuesta a un evento de usuario, como pueden ser value de un <input> o el checked checkbox por ejemplo.

Dado que estos elementos nativos no emiten un evento del tipo property-changed de Polymer, podemos establecer un binding a otro nombre de evento usando la siguiente sintaxis:

<input type=’checkbox’ checked='{{myBoolProperty::change}}’>

Con esto estamos consiguiendo que el valor de myBoolProperty se iguale con el valor del checked del input cuando se produzca el evento nativo change.

A continuación el código de 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>input-two</title>
  <meta name="description" content="input-two 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/input-two-app/input-two-app.html">
</head>

<body>
  <input-two-app label_prop='Escribe un mensaje' value_prop=''></input-two-app>
</body>

</html>

input-two-app.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../input-three-app/input-three-app.html">
<dom-module id="input-two-app">
  <template>
    <style>
      :host {
        display: block;
      }
    </style>
    <span>[[label_prop]]</span>
    <br> Sintaxis correcta
    <input type="text" value='{{value_prop::input}}'>
    <br> Sintaxis incorrecta
    <input type="text" value='{{value_prop}}'>
    <br>
    <input-three-app mensaje='[[value_prop]]'></input-three-app>
  </template>

  <script>
    /**
     * @customElement
     * @polymer
     */
    class InputTwoApp extends Polymer.Element {
      static get is() { return 'input-two-app'; }
      static get properties() {
        return {
          label_prop: {
            type: String,
            value: ''
          },
          value_prop: {
            type: String,
            value: ''
          }
        };
      }
    }

    window.customElements.define(InputTwoApp.is, InputTwoApp);
  </script>
</dom-module>

input-three-app.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<dom-module id="input-three-app">
    <template>
        <p>Mensaje...[[mensaje]]</p>
    </template>
    <script>
        class InputThreeApp extends Polymer.Element {
            static get is() {
                return 'input-three-app';
            }
            /**
              * Object describing property-related metadata used by Polymer features
              */
            static get properties() {
                return {
                    mensaje: {
                        type: String,
                        value: ''
                    }
                };
            }
        }
        window.customElements.define(InputThreeApp.is, InputThreeApp);
    </script>
</dom-module>

Binding a subpropiedades de objetos

Cuando tenemos que trabajar con las subpropiedades de los objetos nos encontramos con un problema, ya que si modificamos las subpropiedades no se ejecuta el binding y no informa de los cambios.

Como hemos dicho, el binding no funciona si modificamos una subpropiedad, pero si modificamos el objeto entero entonces si se ejecutara el binding.

¿Como hacemos para que el binding funcione con una subpropiedad?, pues polymer lo ha simplificado usando set, de esta forma conseguimos modificar esa subpropiedad y que se ejecute el binding.

La sintaxis es muy sencilla: this.set(‘ruta de la propiedad del objeto’, valor);

Con esa simple linea ya funcionaría el binding.

A continuación un vídeo y el código:

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>BINDING</title>
  <meta name="description" content="BINDING 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/sub-prop/sub-prop.html">
</head>

<body>
  <sub-prop edad='10'></sub-prop>
</body>

</html>

sub-prop.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<dom-module id="sub-prop">
    <template>
        <h1>Subpropiedades</h1>
        <p>[[objeto.edad.value]]</p>
        <button on-click='change'>Cambiar subpropiedad</button>
    </template>
    <script>
        class SubPro extends Polymer.Element {
            static get is() {
                return 'sub-prop';
            }
            static get properties() {
                return {
                    edad: Number,
                    objeto: {
                        type: Object,
                        value: function () {
                            return {
                                edad: {
                                    type: Number,
                                    value: 0
                                }
                            }
                        }
                    }
                }
            }
            change() {
                // No funciona el binding, necesitamos usar .set
                //this.objeto.edad.value = 100;
                this.set('objeto.edad.value', 20);
            }
        }
        window.customElements.define(SubPro.is, SubPro);
    </script>
</dom-module>

Binding en arrays

Como ocurre en las subpropiedades el binding en arrays no se lanza automáticamente salvo que se modifique el array entero.

Si modificas un elemento del array este quedara modificado, pero no lanzara el binding y no se mostraran los cambios.

Bien, ¿y como hacemos para que funcione el binding?. Muy simple, deberemos usar los métodos de manipulación de arrays que ofrece polymer y que son los mismos de javascript nativo.

Imaginemos un array que se llama libros, en javascript nativo si queremos añadir un elemento usaríamos la siguiente instrucción: libros.push(valor).

En polymer es igual, pero como necesitamos manipular el array que esta dentro del componente usaríamos la siguiente instrucción: this.push(‘array’, valor).

A continuación un vídeo y su código:

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>BINDING</title>
  <meta name="description" content="BINDING 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/bind-array/bind-array.html">
</head>

<body>
  <bind-array></bind-array>
</body>

</html>

bind-array.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/polymer/lib/elements/dom-repeat.html">
<dom-module id="bind-array">
    <template>
        <style>
            :host {
                display: block;
            }
        </style>
        <div>
            <p>Binding Array</p>
            <ul>
                <template is='dom-repeat' items='[[superheroes]]' as='sh'>
                    <li>[[sh]]</li>
                </template>
            </ul>
        </div>

        <p>
            <input type="text" placeholder="Superheroe" value='{{nombreHeroe::input}}'>
        </p>
        <p>
            <button on-click='modArrayJS'>Modificar array JS</button>
        </p>
        <p>
            <button on-click='modArrayPOL'>Modificar array Polymer</button>
        </p>
    </template>
    <script>
        class BindArray extends Polymer.Element {
            static get is() {
                return 'bind-array';
            }
            static get properties() {
                return {
                    nombreHeroe: {
                        type: String,
                        value: ''
                    },
                    superheroes: {
                        type: Array,
                        value: function () {
                            return ['Spiderman', 'Hulk', 'Thor'];
                        }
                    }
                };
            }
            modArrayJS() {
                if (this.nombreHeroe.length > 0) {
                    this.superheroes.push(this.nombreHeroe)
                    console.log(this.superheroes);
                    this.nombreHeroe = '';
                }
            }
            modArrayPOL() {
                if (this.nombreHeroe.length > 0) {
                    this.push('superheroes', this.nombreHeroe)
                    console.log(this.superheroes);
                    this.nombreHeroe = '';
                }
            }
        }
        window.customElements.define(BindArray.is, BindArray);
    </script>
</dom-module>

Computed binding

Son parecidas a las propiedades computadas, ya que nos permite definir una función que se ejecute y calcule el valor a bindear.

A continuación un ejemplo de computed binding y propiedades computadas:

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>BINDING</title>
  <meta name="description" content="BINDING 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/computed-bind/computed-bind.html">
</head>

<body>
  <computed-bind nombre='Test' edad='21'></computed-bind>
</body>

</html>

computed-bind.html

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/polymer/lib/elements/dom-repeat.html">
<dom-module id="computed-bind">
    <template>
        <style>
            :host {
                display: block;
            }

            .menor {
                color: #ff0000;
            }

            .mayor {
                color: #00aa00;
            }
        </style>
        <p>Computed Binding</p>
        <p>Datos del componente....</p>
        <div>Nombre...[[nombre]]</div>
        <div class$='[[computedBinding(edad)]]'>Edad.....[[edad]]</div>
        <div>Nueva edad
            <input type='number' value='{{edad::input}}'>
        </div>
        <div>
            <p>Mensaje propiedad computada...[[mensaje]]</p>
            <input type="text" value='{{nombre::input}}'>
        </div>
    </template>
    <script>
        class ComputedBinding extends Polymer.Element {
            static get is() {
                return 'computed-bind';
            }
            static get properties() {
                return {
                    nombre: {
                        type: String,
                        value: ''
                    },
                    edad: {
                        type: Number,
                        value: 0
                    },
                    mensaje: {
                        type: String,
                        computed: 'propComputed(nombre)'
                    }
                };
            }
            computedBinding(e) {
                console.log("Propiedad computada");
                return e < 18 ? 'menor' : 'mayor';
            }
            propComputed(nombre) {
                console.log("Computed binding");
                return nombre;
            }
        }
        window.customElements.define(ComputedBinding.is, ComputedBinding);
    </script>
</dom-module>