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ística | Descripció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 observedAttributes()
.
Observa detenidamente los parámetros de este método:
Característica | Descripció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 valorPaco37
. - 3️⃣ Como el atributo
name
ha cambiado y está vigilado, salta el método. - 4️⃣ El primer cambio de
name
es denull
aPaco37
.
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.