javascript - Detectar cambios en el DOM
Quiero ejecutar una función cuando se agregue algún div o input al HTML. ¿Es esto posible?
Por ejemplo, se agrega una entrada de texto, luego se debería llamar a la función.
Respuesta #1
Enfoque definitivo hasta ahora, con el código más pequeño:
(IE11+, FF, Webkit)
Usando MutationObserver y recurriendo a los eventos de mutación obsoletos si es necesario:
(Ejemplo a continuación solo para cambios en el DOM relacionados con nodos añadidos o eliminados)
var observeDOM = (function() {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
return function(obj, callback) {
if (!obj || obj.nodeType !== 1) {
return;
}
if (MutationObserver) {
// define a new observer
var mutationObserver = new MutationObserver(callback);
// have the observer observe for changes in children
mutationObserver.observe(obj, {childList: true, subtree: true});
return mutationObserver;
} else if (window.addEventListener) { // browser support fallback
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
}
})();
//------------< DEMO BELOW >----------------
// add item
var itemHTML = '<li><button>list item (click to delete)</button></li>',
listEl = document.querySelector('ol');
document.querySelector('body > button').onclick = function(e) {
listEl.insertAdjacentHTML('beforeend', itemHTML);
};
// delete item
listEl.onclick = function(e) {
if (e.target.nodeName == 'BUTTON') {
e.target.parentNode.parentNode.removeChild(e.target.parentNode);
}
}
// Observe a specific DOM element:
observeDOM(listEl, function(m) {
var addedNodes = [], removedNodes = [];
m.forEach(record => record.addedNodes.length & addedNodes.push(...record.addedNodes));
m.forEach(record => record.removedNodes.length & removedNodes.push(...record.removedNodes));
console.clear();
console.log('Added:', addedNodes, 'Removed:', removedNodes);
});
// Insert 3 DOM nodes at once after 3 seconds
setTimeout(function(){
listEl.removeChild(listEl.lastElementChild);
listEl.insertAdjacentHTML('beforeend', Array(4).join(itemHTML));
}, 3000);<button>Add Item</button>
<ol>
<li><button>list item (click to delete)</button></li>
<li><button>list item (click to delete)</button></li>
<li><button>list item (click to delete)</button></li>
<li><button>list item (click to delete)</button></li>
<li><em>…More will be added after 3 seconds…</em></li>
</ol>Respuesta #2
Actualización 2015, los navegadores modernos soportan el nuevo MutationObserver:
Chrome 18+, Firefox 14+, IE 11+, Safari 6+
Si necesitas dar soporte a versiones antiguas, puedes intentar recurrir a otros enfoques como los mencionados en esta respuesta de 5 (!) años de antigüedad a continuación. Allí hay dragones. Disfruta :)
¿Alguien más está cambiando el documento? Porque si tienes control total sobre los cambios, solo necesitas crear tu propia API domChanged - con una función o evento personalizado - y activarla/llamarla en todos los lugares donde modifiques cosas.
El DOM Nivel 2 tiene tipos de eventos de mutación, pero las versiones antiguas de IE no los soportan. Tenga en cuenta que los eventos de mutación están obsoletos en la especificación de Eventos DOM3 y tienen una penalización de rendimiento.
Puedes intentar emular el evento de mutación con onpropertychange en IE (y recurrir al enfoque de fuerza bruta si ninguno de ellos está disponible).
Para un cambio completo del DOM, un intervalo podría ser excesivo. Imagina que necesitas almacenar el estado actual de todo el documento y examinar cada propiedad de cada elemento para que sea igual.
Quizás si solo te interesan los elementos y su orden (como mencionaste en tu pregunta), un getElementsByTagName("*") puede funcionar. Esto se ejecutará automáticamente si agregas un elemento, eliminas un elemento, reemplazas elementos o cambias la estructura del documento.
Escribí una prueba de concepto:
(function (window) {
var last = +new Date();
var delay = 100; // default delay
// Manage event queue
var stack = [];
function callback() {
var now = +new Date();
if (now - last > delay) {
for (var i = 0; i < stack.length; i++) {
stack[i]();
}
last = now;
}
}
// Public interface
var onDomChange = function (fn, newdelay) {
if (newdelay) delay = newdelay;
stack.push(fn);
};
// Naive approach for compatibility
function naive() {
var last = document.getElementsByTagName('*');
var lastlen = last.length;
var timer = setTimeout(function check() {
// get current state of the document
var current = document.getElementsByTagName('*');
var len = current.length;
// if the length is different
// it's fairly obvious
if (len != lastlen) {
// just make sure the loop finishes early
last = [];
}
// go check every element in order
for (var i = 0; i < len; i++) {
if (current[i] !== last[i]) {
callback();
last = current;
lastlen = len;
break;
}
}
// over, and over, and over again
setTimeout(check, delay);
}, delay);
}
//
// Check for mutation events support
//
var support = {};
var el = document.documentElement;
var remain = 3;
// callback for the tests
function decide() {
if (support.DOMNodeInserted) {
window.addEventListener("DOMContentLoaded", function () {
if (support.DOMSubtreeModified) { // for FF 3+, Chrome
el.addEventListener('DOMSubtreeModified', callback, false);
} else { // for FF 2, Safari, Opera 9.6+
el.addEventListener('DOMNodeInserted', callback, false);
el.addEventListener('DOMNodeRemoved', callback, false);
}
}, false);
} else if (document.onpropertychange) { // for IE 5.5+
document.onpropertychange = callback;
} else { // fallback
naive();
}
}
// checks a particular event
function test(event) {
el.addEventListener(event, function fn() {
support[event] = true;
el.removeEventListener(event, fn, false);
if (--remain === 0) decide();
}, false);
}
// attach test events
if (window.addEventListener) {
test('DOMSubtreeModified');
test('DOMNodeInserted');
test('DOMNodeRemoved');
} else {
decide();
}
// do the dummy test
var dummy = document.createElement("div");
el.appendChild(dummy);
el.removeChild(dummy);
// expose
window.onDomChange = onDomChange;
})(window);
Uso :
onDomChange(function(){
alert("The Times They Are a-Changin'");
});
Esto funciona en IE 5.5+, FF 2+, Chrome, Safari 3+ y Opera 9.6+
Respuesta #3
El siguiente ejemplo fue adaptado de la publicación del blog de Mozilla Hacks y está utilizando MutationObserver.
// Select the node that will be observed for mutations
var targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
var config = { attributes: true, childList: true };
// Callback function to execute when mutations are observed
var callback = function(mutationsList) {
for(var mutation of mutationsList) {
if (mutation.type == 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type == 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
observer.disconnect();
Compatibilidad del navegador: Chrome 18+, Firefox 14+, IE 11+, Safari 6+
Respuesta #4
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var observer = new MutationObserver(function(mutations, observer) {
// fired when a mutation occurs
console.log(mutations, observer);
// ...
});
// define what element should be observed by the observer
// and what types of mutations trigger the callback
observer.observe(document, {
subtree: true,
attributes: true
//...
});
Explicaciones completas: https://stackoverflow.com/a/11546242/6569224
Respuesta #5
Recientemente he escrito un complemento que hace exactamente eso - jquery.initialize.
(Enlace de NPM - diferente nombre de usuario en NPM/GitHub, mismo autor)
Lo usas de la misma manera que la función .each
$(".some-element").initialize( function(){
$(this).css("color", "blue");
});
La diferencia con .each es que toma tu selector, en este caso .some-element, y espera a que aparezcan nuevos elementos con este selector en el futuro; si se agrega dicho elemento, también se inicializará.
En nuestro caso, la función de inicialización solo cambia el color del elemento a azul. Así que si agregamos un nuevo elemento (no importa si con ajax o incluso con el inspector F12 o cualquier cosa) como:
$("<div/>").addClass('some-element').appendTo("body"); //new element will have blue color!
El plugin lo inicializará instantáneamente. Además, el plugin se asegura de que un elemento se inicialice solo una vez. Entonces, si agregas un elemento, luego lo .detach() del body y luego lo agregas de nuevo, no se inicializará nuevamente.
$("<div/>").addClass('some-element').appendTo("body").detach()
.appendTo(".some-container");
//initialized only once
El plugin se basa en MutationObserver: funcionará en IE9 y 10 con las dependencias detalladas en la página del readme.
Respuesta #6
o simplemente puedes Crear tu propio evento, que se ejecute en todas partes
$("body").on("domChanged", function () {
//dom is changed
});
$(".button").click(function () {
//do some change
$("button").append("<span>i am the new change</span>");
//fire event
$("body").trigger("domChanged");
});
Ejemplo completo http://jsfiddle.net/hbmaam/Mq7NX/
