Reactividad en atributos HTML

Detección de cambios en atributos HTML


Un patrón habitual y muy interesante, utilizado en ciertos frameworks, es el de la reactividad. Este patrón se basa en que, cuando suceda un cambio en una estructura de datos, la aplicación o web debe ser capaz de reaccionar y actualizar todo lo que dependa de ese valor, de modo que no tengamos que hacerlo de forma manual.

Atributos HTML sin reactividad

Por defecto, el componente que creamos no tiene reactividad.

Veamos un ejemplo para entenderlo mejor. Observa que tenemos un componente app-element, y su atributo name es el que tendrá el nickname del usuario. Al pulsar en el botón, cambiaremos su nick y modificaremos el atributo del componente:

<app-element>Sin nickname establecido.</app-element>
<button>Cambiar nickname</button>

<script>
  const appElement = document.querySelector("app-element");
  const button = document.querySelector("button");

  button.addEventListener("click", () => {
    const nickname = "Usuario" + Math.floor(Math.random() * 255);
    appElement.setAttribute("name", nickname);
  });
</script>
class AppElement extends HTMLElement {
  constructor() {
    super();
    this.textContent = `El nickname del usuario es ${this.getAttribute("name")}.`;
  }
}

customElements.define("app-element", AppElement);

Es importante darse cuenta que el constructor() es un método que se ejecuta cuando se crea el componente. De hecho, está modificando el contenido de texto Sin nickname establecido por defecto y cambiandolo por otro. Pero al pulsar en el botón, aunque estamos cambiando su atributo name, el componente no se da cuenta de los cambios y no se actualiza automáticamente.

Observación de cambios

Para integrar reactividad en nuestro componente, vamos a implementar una propiedad de observación de atributos. Por defecto, los componentes no reaccionan a cambios. Si queremos que lo hagan, hay que definir la propiedad observedAttributes() para indicar los atributos que queremos vigilar:

CaracterísticaDescripción
static get observedAttributes()Indica los atributos que va a observar para notificar cuando haya cambios.

Ya que en el ejemplo anterior, no estaba funcionando como buscabamos, vamos a quitar el método constructor(). Ahora vamos a añadir el get observedAttributes() y devolvemos un array con los atributos HTML que queremos vigilar, es decir, en nuestro caso el atributo name:

class AppElement extends HTMLElement {
  static get observedAttributes() {
    return ["name"];
  }
}

customElements.define("app-element", AppElement);
<app-element>Sin nickname establecido.</app-element>
<button>Cambiar nickname</button>

<script>
  const appElement = document.querySelector("app-element");
  const button = document.querySelector("button");

  button.addEventListener("click", () => {
    const nickname = "Usuario" + Math.floor(Math.random() * 255);
    appElement.setAttribute("name", nickname);
  });
</script>

De momento, parece que sigue sin funcionar, sin embargo, el componente ya está reaccionando a los cambios. Cuando detecta un cambio en los atributos vigilados, dispara automáticamente el método attributeChangedCallback(). Sin embargo, como aún no lo hemos implementado, no realiza ninguna tarea.

Detección de cambios

Vamos a implementar el método especial attributeChangedCallback(). Como hemos dicho, este método se llamará automáticamente al detectar un cambio en uno de los atributos HTML vigilados en el que devuelve observedAttributes().

Observa detenidamente los parámetros de este método:

CaracterísticaDescripción
attributeChangedCallback(name,old,now)Se dispara cuando cambia un atributo observado.

El método nos pasará por parámetro el nombre del atributo que ha cambiado en name, así como el valor que tenía antes del cambio en old, y el valor al que ha cambiado en now. Los nombres de estos parámetros puedes cambiarlos a los que prefieras, lo importante es el orden.

Veamos una posible implementación de este funcionamiento:

class AppElement extends HTMLElement {
  static get observedAttributes() {
    return ["name"];
  }

  attributeChangedCallback(name, old, now) {
    this.innerHTML = `Nickname cambiado de <mark>${old}</mark> a <mark>${now}</mark>.`;
  }
}

customElements.define("app-element", AppElement);
<app-element>Sin nickname establecido.</app-element>
<button>Cambiar nickname</button>

<script>
  const appElement = document.querySelector("app-element");
  const button = document.querySelector("button");

  button.addEventListener("click", () => {
    const nickname = "Usuario" + Math.floor(Math.random() * 255);
    appElement.setAttribute("name", nickname);
  });
</script>

De esta forma, cada vez que el valor de los atributos observados de un WebComponent cambie, se ejecutará automáticamente el método attributeChangedCallback() con los valores específicos en sus parámetros name, old y now. Corre de nuestra cuenta escribir la lógica necesaria o llamar al método que queramos que actualice y renderice el HTML del componente.

Caso especial: Primer cambio

Observa este caso en el que <app-element> tiene un atributo name por defecto en el HTML. Lo que ocurre es lo siguiente:

  • 1️⃣ Primero se crea el componente <app-element>.
  • 2️⃣ Luego, se le añade el atributo name con valor Paco37.
  • 3️⃣ Como el atributo name ha cambiado y está vigilado, salta el método.
  • 4️⃣ El primer cambio de name es de null a Paco37.
class AppElement extends HTMLElement {
  static get observedAttributes() {
    return ["name"];
  }

  attributeChangedCallback(name, old, now) {
    this.innerHTML = `Nickname cambiado de <mark>${old}</mark> a <mark>${now}</mark>.`;
  }
}

customElements.define("app-element", AppElement);
<app-element name="Paco37">Sin nickname establecido.</app-element>
<button>Cambiar nickname</button>

<script>
  const appElement = document.querySelector("app-element");
  const button = document.querySelector("button");

  button.addEventListener("click", () => {
    const nickname = "Usuario" + Math.floor(Math.random() * 255);
    appElement.setAttribute("name", nickname);
  });
</script>

Esta casuística podría controlarse, simplemente comprobando si el valor de old es null. Pero es importante tenerlo en cuenta para añadir la lógica necesaria.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev