373

I want to execute a function when some div or input are added to the html. Is this possible?

For example, a text input is added, then the function should be called.

000
  • 26,951
  • 10
  • 71
  • 101
esafwan
  • 17,311
  • 33
  • 107
  • 166
  • 1
    Unless some third party script is adding the nodes to the DOM, this isn't necessary. – Justin Johnson Jul 10 '10 at 16:34
  • duplicate of http://stackoverflow.com/questions/2457043/most-efficient-method-of-detecting-monitoring-dom-changes – Gabriele Petrioli Jul 16 '10 at 00:05
  • Possible duplicate of [Is there a JavaScript/jQuery DOM change listener?](https://stackoverflow.com/questions/2844565/is-there-a-javascript-jquery-dom-change-listener) – Makyen Oct 28 '17 at 09:21
  • 12
    @JustinJohnson if you're making a chrome extension which injects JS code, it's useful. – FluorescentGreen5 May 02 '18 at 07:35
  • https://github.com/Maykonn/page-info-js – Maykonn Jul 26 '18 at 15:30
  • I really don't get it - it's like... eh.. requesting a hotel wake up call at their breakfast buffet... You change the DOM with JS and then want JS-events to notify you of what you just did? Sounds like an awesome code-base. If you're just waiting for a specific css selector to load there's older methods like onload="", jquery .ready() or a manual setInterval checks that solves it without an extra 50kb blob as the "solutions" below. :P – Christoffer Bubach Mar 12 '21 at 23:08
  • You are assuming a scenario where just your own code will change your dom. Imagine a scenario where you are using 3rd party library a day you want to know when they change the dom. There many other use cases too, that's the reason now we have MutationObserver support in all modern browsers. – esafwan Mar 12 '21 at 23:22
  • See: https://stackoverflow.com/questions/54112328/what-are-some-practical-examples-of-mutationobserver-use and https://eager.io/blog/three-real-world-use-cases-for-mutation-observer/ – esafwan Mar 12 '21 at 23:24

11 Answers11

301

Ultimate approach so far, with smallest code:

(IE11+, FF, Webkit)

Using MutationObserver and falling back to the deprecated Mutation events if needed:
(Example below if only for DOM changes concerning nodes appended or removed)

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
    }
    
    // browser support fallback
    else if( window.addEventListener ){
      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>",
    listElm = document.querySelector('ol');

document.querySelector('body > button').onclick = function(e){
  listElm.insertAdjacentHTML("beforeend", itemHTML);
}

// delete item
listElm.onclick = function(e){
  if( e.target.nodeName == "BUTTON" )
    e.target.parentNode.parentNode.removeChild(e.target.parentNode);
}
    
// Observe a specific DOM element:
observeDOM( listElm, 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(){
   listElm.removeChild(listElm.lastElementChild);
   listElm.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>&hellip;More will be added after 3 seconds&hellip;</em></li>
</ol>
vsync
  • 118,978
  • 58
  • 307
  • 400
  • 1
    it seems to work pretty nicely for new DOM nodes. Can we adapt it to also handle dom node changes (at least the DOM node values/text?) – Sebastien Lorber Feb 26 '14 at 13:31
  • 2
    Pass the `mutations, observer` parameters to the callback function for more control. – A1rPun Jun 27 '14 at 23:21
  • 4
    This helped me a lot, but how do I "unbind" this? Say I want to watch for a change only once, but do this on multiple occasions? oberserveDOM = null obviously won't work... – stiller_leser Nov 26 '14 at 11:25
  • Why would this only work for appended/removed? It looks like the mutation events cover more than that.. https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events – f0ster Jun 10 '15 at 17:57
  • This is a good start but failed to catch when are multiple mutations at once and so the `childList` mutation is not on `mutations[0]`. You should check every item of `mutations`. – Niloct Sep 25 '18 at 21:41
  • 1
    JSHint does not like ```!obj.nodeType === 1```. It's just checking to make sure the nodeType is an element, so you can use, ```obj.nodeType !== 1```. – stldoug Jul 07 '20 at 01:19
  • 2
    To "unwatch", use the `observer.disconnect();` see the documentation at https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver – Vadorequest May 17 '22 at 14:15
  • [If you also wondered who "foo" is](https://stackoverflow.com/a/13278380/1951524) ;) – Martin Schneider Dec 07 '22 at 10:08
  • Well, why writing all this manually when there are npm packages that did the same for you? (e.g. [jquery.initialize](https://www.npmjs.com/package/jquery.initialize) mentioned in another answer below) – Denis P Jun 27 '23 at 23:28
  • @DenisP - most projects don't use *jQuery*, and many are prohibit from installing 3rd-party scripts (due to various organization security or styleguide reasons). There are **plenty** of reasons why you would need to write this yourself, trust me (I'm a stranger from the internet) – vsync Jun 28 '23 at 08:52
253

2015 update, new MutationObserver is supported by modern browsers:

Chrome 18+, Firefox 14+, IE 11+, Safari 6+

If you need to support older ones, you may try to fall back to other approaches like the ones mentioned in this 5 (!) year old answer below. There be dragons. Enjoy :)


Someone else is changing the document? Because if you have full control over the changes you just need to create your own domChanged API - with a function or custom event - and trigger/call it everywhere you modify things.

The DOM Level-2 has Mutation event types, but older version of IE don't support it. Note that the mutation events are deprecated in the DOM3 Events spec and have a performance penalty.

You can try to emulate mutation event with onpropertychange in IE (and fall back to the brute-force approach if non of them is available).

For a full domChange an interval could be an over-kill. Imagine that you need to store the current state of the whole document, and examine every element's every property to be the same.

Maybe if you're only interested in the elements and their order (as you mentioned in your question), a getElementsByTagName("*") can work. This will fire automatically if you add an element, remove an element, replace elements or change the structure of the document.

I wrote a proof of concept:

(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);

Usage:

onDomChange(function(){ 
    alert("The Times They Are a-Changin'");
});

This works on IE 5.5+, FF 2+, Chrome, Safari 3+ and Opera 9.6+

gblazex
  • 49,155
  • 12
  • 98
  • 91
  • cant we add a active listener(dont knw wht to say!) or something that check in interval and check the dom? – esafwan Jul 10 '10 at 16:08
  • 4
    Wondering: how does jQuery live() solve this problem if they can't detect a DOM change? – Kees C. Bakker Jan 22 '11 at 16:43
  • 1
    @JoshStodola The bold was annoying me too. I decided to fix it. – Bojangles Feb 04 '13 at 13:58
  • 1
    Mutations events are deprecated. You should use MutationObserver. I've written my plugin for problems like this - https://github.com/AdamPietrasiak/jquery.initialize – Adam Pietrasiak Feb 06 '15 at 17:05
  • 1
    How can I get jquery onClick to fire before a mutation observer, that fires when a button is clicked with a ember action? https://stackoverflow.com/questions/29216434/how-to-get-a-jquery-onclick-to-fire-the-same-time-as-ember-click-and-before-muta – SuperUberDuper Mar 24 '15 at 10:43
  • 3
    BTW, passing *window* to *window* in `(function(window){...}(window))` is pointless. If the intention is to get the global/window object safely, pass in *this*: `(function(window){...}(this))` since in global code, *this* always points to the global/window object. – RobG Nov 01 '15 at 23:35
  • 1
    I can't see how the *naïve* function can work. *getElementsByTagName* returns a live node list, so *last* and *current* will always contain exactly the same elements. *last* should be static (e.g. use *querySelectorAll* instead). Perhaps that section of the code hasn't been tested? Also, even if converted to a static array, if the same number of nodes is added as removed, then the *length* test will exit early when there have been changes made. – RobG Nov 01 '15 at 23:38
  • Thanks for the warning about dragons – Samuel Nwaokoro Feb 01 '21 at 17:13
47

The following example was adapted from Mozilla Hacks' blog post and is using 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();

Browser support: Chrome 18+, Firefox 14+, IE 11+, Safari 6+

Lucio
  • 4,753
  • 3
  • 48
  • 77
Anthony Awuley
  • 3,455
  • 30
  • 20
16

I have recently written a plugin that does exactly that - jquery.initialize.
(NPM link - different NPM/GitHub user name, same author)

You use it the same way as .each function

$(".some-element").initialize( function(){
    $(this).css("color", "blue"); 
});

The difference from .each is - it takes your selector, in this case .some-element and wait for new elements with this selector in the future, if such element will be added, it will be initialized too.

In our case initialize function just change element color to blue. So if we'll add new element (no matter if with ajax or even F12 inspector or anything) like:

$("<div/>").addClass('some-element').appendTo("body"); //new element will have blue color!

Plugin will init it instantly. Also plugin makes sure one element is initialized only once. So if you add element, then .detach() it from body and then add it again, it will not be initialized again.

$("<div/>").addClass('some-element').appendTo("body").detach()
    .appendTo(".some-container");
//initialized only once

Plugin is based on MutationObserver - it will work on IE9 and 10 with dependencies as detailed on the readme page.

greybeard
  • 2,249
  • 8
  • 30
  • 66
Adam Pietrasiak
  • 12,773
  • 9
  • 78
  • 91
13

or you can simply Create your own event, that run everywhere

 $("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");

        });

Full example http://jsfiddle.net/hbmaam/Mq7NX/

HB MAAM
  • 3,913
  • 4
  • 29
  • 39
  • 1
    this is not the same... the method described above is still valid http://api.jquery.com/trigger/ – lefoy Nov 16 '16 at 20:20
13
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
  //...
});

Complete explanations: https://stackoverflow.com/a/11546242/6569224

Mahdi Bashirpour
  • 17,147
  • 12
  • 117
  • 144
7

Use the MutationObserver interface as shown in Gabriele Romanato's blog

Chrome 18+, Firefox 14+, IE 11+, Safari 6+

// The node to be monitored
var target = $( "#content" )[0];

// Create an observer instance
var observer = new MutationObserver(function( mutations ) {
  mutations.forEach(function( mutation ) {
    var newNodes = mutation.addedNodes; // DOM NodeList
    if( newNodes !== null ) { // If there are new nodes added
        var $nodes = $( newNodes ); // jQuery set
        $nodes.each(function() {
            var $node = $( this );
            if( $node.hasClass( "message" ) ) {
                // do something
            }
        });
    }
  });    
});

// Configuration of the observer:
var config = { 
    attributes: true, 
    childList: true, 
    characterData: true 
};

// Pass in the target node, as well as the observer options
observer.observe(target, config);

// Later, you can stop observing
observer.disconnect();
frzsombor
  • 2,274
  • 1
  • 22
  • 40
Anthony Awuley
  • 3,455
  • 30
  • 20
4

How about extending a jquery for this?

   (function () {
        var ev = new $.Event('remove'),
            orig = $.fn.remove;
        var evap = new $.Event('append'),
           origap = $.fn.append;
        $.fn.remove = function () {
            $(this).trigger(ev);
            return orig.apply(this, arguments);
        }
        $.fn.append = function () {
            $(this).trigger(evap);
            return origap.apply(this, arguments);
        }
    })();
    $(document).on('append', function (e) { /*write your logic here*/ });
    $(document).on('remove', function (e) { /*write your logic here*/ });

Jquery 1.9+ has built support for this(I have heard not tested).

Volomike
  • 23,743
  • 21
  • 113
  • 209
StartCoding
  • 394
  • 5
  • 16
4

8 years later, here is my solution using MutationObserver and RxJS

observeDOMChange(document.querySelector('#dom-changes-here'))
  .subscribe(val => log('DOM-change detected'));

The main difference from the other approaches is to fire a CustomEvent when DOM changes, and listen to the event debounced to execute user logic efficiently with the following features;

  • Debounce consecutive DOM changes to prevent too many executions
  • Stop watching after the given time
  • Removes event listeners/subscribers after stop watching DOM changes
  • Useful to watch DOM change happened in a framework, e.g., Angular
import { fromEvent, timer} from 'rxjs';
import { debounceTime, takeUntil, tap } from 'rxjs/operators';

function observeDOMChange(el, options={}) {
  options = Object.assign({debounce: 100, expires: 2000}, options);

  const observer = new MutationObserver(list =>  {
    el.dispatchEvent(new CustomEvent('dom-change', {detail: list}));
  });
  observer.observe(el, {attributes: false, childList: true, subtree: true });

  let pipeFn;
  if (options.expires) {
    setTimeout(_ => observer.disconnect(), options.expires);
    pipeFn = takeUntil(timer(options.expires));
  } else {
    pipeFn = tap(_ => _); 
  }

  return fromEvent(el, 'dom-change')
    .pipe(pipeFn, debounceTime(options.debounce));
}

Demo at stackblitz.
enter image description here

allenhwkim
  • 27,270
  • 18
  • 89
  • 122
1

Found the question so updating with a solution in 2022.

We have seen different solutions which mostly involve MutationObserver.

If anyone wants to record the DOM changes and store them to replay after some time, they can use rrweb

Edit:

Adding example, here are the hints:

rrweb you can use via CDN or npm

Let's take the example of CDN for recording the DOM changes events:

Step 1: just include following script tag in the <HTML><head> tag

<script src="https://cdn.jsdelivr.net/npm/rrweb@2.0.0-alpha.2/dist/rrweb-all.js" crossorigin="anonymous"></script>

Step 2: and add the below code in your code to capture the events generated by rrweb.

<script>
var events = [];
rrweb.record({
    emit(event) {
       events.push(event);
       // you can store this event anywhere and you can replay them later. ex: some JSON file, or DB
    }
});

</script>

This example is mostly for recording events for any web application.

To know and understand in the detail (how to record/replay), please read from rrweb documentation.

Replayer Example:

This was for debugging, however adding here so that anyone can check replayed side as well:

Replayer Example

Code_Crash
  • 734
  • 5
  • 22
0

Use TrackChanges for detect html changes. Link: https://www.npmjs.com/package/track-changes-js

Example

 let button = document.querySelector('.button');

 trackChanges.addObserver('buttonObserver', () => button);
 
 trackChanges.addHandler('buttonObserver', buttonHandler);

 function buttonHandler(button) {
   console.log(`Button created: ${button}`);
 }
itmor
  • 1
  • 1