Trabajar con el DOM mediante Javascript nativo, muchas veces se suele tachar de algo que es demasiado complicado porque tienes que estar actualizando los datos, y por separado la interfaz de usuario. Sin embargo, esto no tiene que ser necesariamente así, sino que se hace muchas veces por desarrollar más rápido o simplemente por desconocimiento, ya que existe una API denominada MutationObserver.
¿Qué es MutationObserver?
MutationObserver es una API del navegador que pertenece a la familia de observadores. Los observadores, como su propio nombre indica, son sistemas que vigilan a otros y realizan una acción cuando detectan cambios u operaciones.
En el caso de MutationObserver, se trata de un observador que vigila un elemento del DOM para detectar cuando se realizan cambios en ese elemento del DOM observado. Si ocurre un cambio, se ejecuta una lógica previamente definida en el observador.
La API MutationObserver
La API MutationObserver tiene varios métodos para utilizar según lo que se desee. En principio, lo que haremos es crear una nueva instancia de MutationObserver, a la cuál se le pasará una función por parámetro con la lógica que queramos ejecutar cuando se detecte un cambio en el lugar del DOM deseado:
const observer = new MutationObserver(() => {
console.log("Cambio detectado en el contenedor.");
});
Como se puede ver, la instancia MutationObserver observer
tiene una funcionalidad (un console.log) que se ejecutará cuando detecte los cambios en el DOM que indiquemos un poco más tarde. Para ello, utilizaremos el método observe()
sobre esta instancia recién creada. Además, hay algunos otros métodos disponibles:
Métodos | Descripción |
---|---|
observe(target, options) | Comienza a observar cambios en el elemento target del DOM. |
disconnect() | Detiene la observación del objeto. |
takeRecords() | Elimina todas las notificaciones pendientes y las devuelve en un |
Observa que también utilizamos el método disconnect()
para detener las observaciones realizadas o la utilización del método takeRecords()
, que además de detener las pendientes, nos las devuelve en un array por si necesitamos trabajar con ellas posteriormente.
Veamos un ejemplo sencillo donde vamos a escuchar los cambios en el DOM de un elemento con clase .container
. Cada vez que se hagan cambios en ese elemento del DOM, se disparará la función que le pasamos al MutationObserver al crear nuestra instancia. Dicha función sólo escribe un mensaje en la consola del navegador, por lo que será conveniente abrirla:
<div class="container">No messages.</div>
<script>
const container = document.querySelector(".container");
// Añade aleatoriamente un nuevo mensaje al DOM
const addRandomMessage = () => {
const time = 1000 + Math.floor(Math.random() * 3000);
container.insertAdjacentHTML("afterbegin", `<div>¡Mensaje recibido!</div>`);
setTimeout(() => addRandomMessage(), time);
}
// Dispara una acción cuando ocurre un cambio en el DOM
const observer = new MutationObserver(() => {
console.log("Cambio detectado en el contenedor.");
});
// Iniciamos observación
observer.observe(container, { childList: true });
// Provocamos cambios en el DOM
setTimeout(() => addRandomMessage(), 3000);
</script>
Por otro lado, observa que la función addRandomMessage()
se va a llamar aleatoriamente cada ciertos segundos y añadirá un nuevo mensaje en el contenedor. De esta forma, cada cierto número de segundos, se debería disparar el console.log()
con el mensaje "Cambio detectado en el contenedor".
Opciones de observación
En el ejemplo anterior, sólo incluimos la opción childList
a true
en nuestro objeto de opciones, sin embargo podemos utilizar muchas otras opciones para personalizar la observación:
Opción de observación | Por defecto | Descripción |
---|---|---|
childList | false | Vigila cambios en el elemento target (sin incluir hijos de hijos). |
subtree | false | Vigila cambios en los hijos de hijos. |
attributes | false | Detecta cambios en los atributos del nodo observado. |
attributeFilter | - | Indica que atributos observar. Si se omite, se vigilan todos. |
attributeOldValue | false | Indica si se deben guardar los valores anteriores del cambio. |
characterData | false | Vigila cambios en los nodos de texto. |
characterDataOldValue | false | Indica si se deben guardar los valores anteriores en los nodos de texto. |
Especialmente importante la opción subtree
, ya que esta opción nos puede permitir indicar vigilar no sólo el elemento del DOM y sus cambios en hijos, sino también de los hijos de los hijos de ese elemento.
<div class="container">
<div class="message-list">
No messages.
</div>
</div>
<script>
const container = document.querySelector(".container");
const messageList = document.querySelector(".message-list");
const addRandomMessage = () => {
const time = 1000 + Math.floor(Math.random() * 3000);
messageList.insertAdjacentHTML("afterbegin", `<div>¡Mensaje recibido!</div>`);
setTimeout(() => addRandomMessage(), time);
}
const observer = new MutationObserver((mutationsList, observer) => {
console.log("Cambio detectado en el contenedor: ", mutationsList);
});
observer.observe(container, { childList: true, subtree: true });
setTimeout(() => addRandomMessage(), 3000);
// Cuando ya no se necesite el observador
// observer.disconnect();
</script>
Lista de mutaciones
Si observas detenidamente el ejemplo anterior, te darás cuenta que definimos un primer parámetro mutationsList
que no llegamos a utilizar y lo mismo con el siguiente parámetro. El observer
simplemente es el observador implicado, por si queremos hacer referencia a él.
Sin embargo, el mutationsList
es un
Propiedad | Descripción | Aplica cuando type es... |
---|---|---|
type | Tipo de mutación detectada: childList , attributes o characterData . | |
target | Nodo del DOM en el que ocurrió el cambio. | |
addedNodes | Lista de nodos agregados. | childList |
removedNodes | Lista de nodos eliminados. | childList |
attributeName | Nombre del atributo modificado. | attributes |
oldValue | Valor anterior del atributo o nodo de texto modificado. | attributes o characterData |
Observa este fragmento y fíjate como recorremos el mutationsList
con un forEach()
, ya que podemos estar trabajando con una sola mutación o con varias de ellas. De esta forma, podemos saber si se trata de un cambio de elementos del DOM o de atributos en el elemento:
const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach(mutation => {
console.log(`Tipo de cambio: ${mutation.type}`);
if (mutation.type === 'childList') {
console.log("Nodos agregados: ", mutation.addedNodes);
console.log("Nodos eliminados: ", mutation.removedNodes);
} else if (mutation.type === 'attributes') {
console.log(`Atributo modificado: ${mutation.attributeName}`);
}
});
});
observer.observe(targetNode, {
attributes: true,
childList: true,
subtree: true
});
Como puedes ver, la API de MutationObserver es una fantástica forma de detectar cambios en el DOM de forma automática y hacer un poco más predecibles nuestras aplicaciones web.