Observers de propiedades profundas de objetos en Polymer 2

  • Por
Aprende a crear observers complejos, en el que se observa el estado de propiedades de objetos, con caminos profundos.

En el Manual de Polymer 2 hemos dedicado ya dos capítulos a los observers, pero aún no hemos terminado de contarte todo. En el capítulo anterior vimos los observadores complejos de Polymer 2 y comprobamos que mediante su declaración podemos vigilar el cambio en dos propiedades al mismo tiempo. En este artículo debemos continuar hablando de ellos, pues hay detalles que no hemos explicado todavía y que necesitarás saber para no volverte loco al usarlos en tus propios componentes.

Veremos cómo acompañar el estado en las propiedades profundas de los objetos y cómo permitir que Polymer 2 ejecute los mecanismos de binding, y los observers, cuando se modifican mediante la asignación de nuevos valores en el código de los métodos del componente.

Qué son propiedades profundas y cuáles son sus características

Comencemos con una aclaración sobre las propiedades profundas de objetos (deep properties). Nosotros podemos tener un objeto dentro de una propiedad de Polymer y ese objeto puede tener varias propiedades profundas.

Por ejemplo, el objeto "contacto" puede tener propiedades profundas como "contacto.nombre" y "contacto.email". El objeto "contacto" podría ser la propiedad de un componente y para distinguir a las propiedades del objeto "nombre" o "email" las llamamos propiedades profundas.

El caso es que las propiedades profundas en Polymer, y esto es algo que ocurre tanto en 1.x como en 2.x, tienen unos tratamientos especiales, para aumentar el rendimiento del framework, que son los que vamos a explicar en este artículo.

Lo primero que debes saber es que, cuando realizamos un observador de un objeto, ese observador sólo se ejecutará cuando todo el objeto cambie y no cuando cambie una de sus propiedades.

Por ejemplo, si tengo un observer declarado sobre la propiedad contacto:

static get properties() {
  return {
    contacto: {
      type: Object,
      observer: 'vigilarContacto'
    }
  }
}

Aunque en algún momento cambien sus propiedades profundas (contacto.nombre, o contacto.email), no se ejecutará el observer "vigilarContacto". Eso es porque ese observador se ha declarado sobre el objeto en sí, el objeto completo y no sobre sus propiedades profundas.

Nota: Por si te lo preguntas, para que se ejecutase el observer vigilarContacto tendría que ocurrir que se le asignase otro objeto a esa propiedad. Por ejemplo con this.contacto = new('Miguel', 'miguel@desarrolloweb.com'); Al crearse ese nuevo objeto y asignarse a la propiedad contacto, como el objeto creado es totalmente nuevo, sí se se ejecutaría el método vigilarContacto. Sin embargo eso es algo que generalmente no se desea, pues lo más normal es que el objeto siga siendo siempre el mismo y lo que se altere en el uso del componente son sus propieades profundas.

Aunque usásemos observers complejos, si observamos el propio objeto, no estamos observando sus deep properties.

static get observers() {
  return [
    'observarObjeto(contacto)'
  ]
}

Esa era la sintaxis de los observers complejos, pero si lo que estamos observando es el objeto, no ocurriría nada aunque cambiasen sus propiedades profundas.

Observar propiedades profundas

Ahora que ha quedado claro que observar un objeto no funciona si lo que queremos es observar sus deep properties, vamos a aprender qué se puede hacer.

Para comenzar, debes saber que las propiedades profundas solo se pueden observar desde observers compejos. En ellos debemos declarar qué propiedades profundas del objeto se desean observar, indicando el camino por medio del operador "punto".

Esta es la sintaxis de la declaración observers en Polymer 2 en la que estaríamos observando propiedades profundas:

static get observers() {
  return [
    'comprobarNombre(contacto.nombre)',
    'comprobarVariosValores(contacto.nombre, contacto.email)'
  ]
}

En la declaración anterior tienes dos ejemplos de observadores declarados, en el primero se vigila una propiedad profunda única y en el segundo dos propiedades profundas al mismo tiempo.

Ya solamente te quedaría escribir el código de tus métodos observadores. Pero eso lo vamos a ver luego con un ejemplo completo de componente.

Asegurarse que se ejecutan los observadores y mecanismos de binding

Cuando se modifican propiedades profundas de los objetos Polymer 2 no siempre ejecuta los mecanismos de binding y los observers declarados. Vamos a aprender ahora a asegurarnos que todo vaya correctamente cuando nosotros alteramos las propiedades profundas en el código de los métodos del componente.

Nota: Esto lo hace Polymer por motivos de rendimiento, pues sería muy costoso escalar eventos ante la modificación de cualquier propiedad profunda, ante cualquier situación en la que ésta pueda alterarse. Ten en cuenta que un objeto puede tener decenas de propiedades y no todas te interesa que se estén controlando mediante binding o observers, por lo que Polymer 2 obvia cualquier cambio en las propiedades profundas a no ser que realmente se necesite.

Por ejemplo, vamos a continuar con nuestro ejemplo del objecto "contacto" y las propiedades profundas "nombre" y "email". Si nosotros hacemos algo como esto:

metodoDelComponente() {
  this.contacto.nombre = 'Miguel Angel Alvarez';
  this.contacto.email = 'miguel@escuela.it';
}

Al ejecutarse el metodoDelComponente(), a pesar que yo edito las propiedades profundas, asignando nuevos valores, esto no desencadena el binding o los observers.

No se ejecutaría el binding

Quiere decir que si yo en mi template tengo un binding como:

<div>
  Nombre: {{contacto.nombre}}
</div>

No se modificaría el template aunque hubiera asignado un nuevo valor a contacto.nombre con this.contacto.nombre = 'nuevo valor'.

No se ejecutarían los observers

Si yo tengo un observer declarado así:

static get observers() {
  return [
    'comprobarNombre(contacto.nombre)'
  ]
}

Tampoco se ejecutaría si se ejecuta algo como this.contacto.nombre = 'nuevo valor'.

Método set() para ejecutar binding y observers

El método set(), que forma parte de cualquier componente de Polymer 2, es el que nosotros debemos usar cuando se modifican propiedades profundas de los objetos. Es el que nos asegura que los mecanismos de binding y los observers se ejecuten como se desea.

Para usar set() simplemente debemos indicar la cadena con el camino a la propiedad profunda que queremos actualizar.

metodoDelComponente() {
  this.set('contacto.nombre', 'Miguel Angel Alvarez');
}

Una vez asignado un nuevo valor a contacto.nombre por medio de set(), comprobarás que los observadores y los mecanismos de binding sí se ejecutan.

Ejemplo completo de componente para practicar con observers sobre propiedades profundas

Ahora vamos a ver un componente que nos sirva de ejemplo para todo lo aprendido. En este componente tenemos una interfaz para colocar una clave, que tiene dos campos de texto, uno para colocar la clave y otro para repetirla (y así estar seguros que, siendo ambas claves iguales, la clave está bien escrita).

En nuestro componente tendremos la clave, y su repetición, declarada con un objeto, que tiene dos propiedades profundas.

static get properties() {
  return {
    clave: {
      type: Object,
      value: function() {
        return {
          value: '',
          repeticion: ''
        }
      }
    }
  };
}
Nota: es relevante que indiques una inicialización para los valores de la clave, a la hora de usar esos valores en campos de texto, aunque simplemente los campos desees que aparezcan vacíos. Los estamos inicializando mediante una función, algo que es necesario en Polymer.

Podemos definir dos observadores en este componente:

  • Uno para comprobar la clave y ver si está bien escrita, es segura y todo lo que necesites saber.
  • Otro para comprobar si las dos claves son iguales.
static get observers() {
  return [
    'comprobarClave(clave.value)',
    'comprobarIguales(clave.value, clave.repeticion)'
  ]
}

Ahora podemos ver los ejemplos de implementación de estos observadores, en los que se realizan tareas básicas.

comprobarClave(cadena) {
  if(cadena == '') {
    this.errorClave = '* Campo requerido';
  } else {
    this.errorClave = '';
  }
}
comprobarIguales(value, repeticion) {
  if(value != repeticion) {
    this.errorRepeticion = '* Las claves no son iguales';
  } else {
    this.errorRepeticion = '';
  }
}

Ahora imaginemos que tenemos un método que debe realizar un reseteo de los campos clave y la repetición de la clave. Para ese reseteo debemos asignar nuevos valores a las propiedades profundas. Veamos este código primero, aunque de momento esta alternativa "va con trampa" para reforzar algo sobre lo que ya hemos hablado antes.

reset() {
  this.clave.value = '';
  this.clave.repeticion = '';
}

¿Ese código de reset() funcionaría? Obviamente no, porque al modificar propiedades profundas debemos usar set(), pues en caso contrario no se ejecutarían los observers ni los mecanismos de binding.

Esta otra alternativa sería la correcta para nuestro método reset().

reset() {
  this.set('clave.value', '');
  this.set('clave.repeticion', '');
}

Podemos ver el código completo del componente que hemos construido para ilustrar este artículo.

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
<dom-module id="claves-iguales">
  <template>
    <style>
      :host {
        display: block;
        border: 1px solid #ddd;
        padding: 10px;
      }
      div {
        display: flex;
        align-items: center;
      }
      span {
        color: red;
        font-size: 0.9em;
        margin: 10px 0 0 20px;
      }
      paper-input {
        width: 50%;
      }
    </style>
    <div>
      <paper-input label="clave" value="{{clave.value}}"></paper-input>
      <span>[[errorClave]]</span>
    </div>
    <div>
      <paper-input label="Repetir clave" value="{{clave.repeticion}}"></paper-input>
      <span>[[errorRepeticion]]</span>
    </div>
    <button on-click="reset">Resetear</button>
  </template>
  <script>
 
    class ClavesIguales extends Polymer.Element {
      static get is() {
        return 'claves-iguales';
      }
      static get properties() {
        return {
          errorClave: String,
          errorRepeticion: String,
          clave: {
            type: Object,
            value: function() {
              return {
                value: '',
                repeticion: ''
              }
            }
          }
        };
      }
      
      static get observers() {
        return [
          'comprobarClave(clave.value)',
          'comprobarIguales(clave.value, clave.repeticion)'
        ]
      }
      constructor() {
        super();
      }
      comprobarClave(cadena) {
        if(cadena == '') {
          this.errorClave = '* Campo requerido';
        } else {
          this.errorClave = '';
        }
      }
      comprobarIguales(value, repeticion) {
        console.log('comprobarIguales', value, repeticion);
        if(value != repeticion) {
          this.errorRepeticion = '* Las claves no son iguales';
        } else {
          this.errorRepeticion = '';
        }
      }
      reset() {
        // this.clave.value = '';
        // this.clave.repeticion = '';
        this.set('clave.value', '');
        this.set('clave.repeticion', '');
      }
    }
    window.customElements.define(ClavesIguales.is, ClavesIguales);
  </script>
</dom-module>

Con esto ya hemos avanzado mucho en el campo de los observers, pero aún nos queda por aprender otros casos importantes, como son la posibilidad de observar cambios en cualquier propiedad profunda, por medio de un comodín, así como la realización de observadores en elementos de arrays. Más adelante lo abordaremos.