Is there a JavaScript / jQuery DOM change listener?
Asked Answered
G

5

469

Essentially I want to have a script execute when the contents of a DIV change. Since the scripts are separate (content script in the Chrome extension & webpage script), I need a way simply observe changes in DOM state. I could set up polling but that seems sloppy.

Gaskell answered 16/5, 2010 at 16:43 Comment(1)
Does this answer your question? Detect changes in the DOMIrmine
G
561

For a long time, DOM3 mutation events were the best available solution, but they have been deprecated for performance reasons. DOM4 Mutation Observers are the replacement for deprecated DOM3 mutation events. They are currently implemented in modern browsers as MutationObserver (or as the vendor-prefixed WebKitMutationObserver in old versions of Chrome):

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
  //...
});

This example listens for DOM changes on document and its entire subtree, and it will fire on changes to element attributes as well as structural changes. The draft spec has a full list of valid mutation listener properties:

childList

  • Set to true if mutations to target's children are to be observed.

attributes

  • Set to true if mutations to target's attributes are to be observed.

characterData

  • Set to true if mutations to target's data are to be observed.

subtree

  • Set to true if mutations to not just target, but also target's descendants are to be observed.

attributeOldValue

  • Set to true if attributes is set to true and target's attribute value before the mutation needs to be recorded.

characterDataOldValue

  • Set to true if characterData is set to true and target's data before the mutation needs to be recorded.

attributeFilter

  • Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed.

(This list is current as of April 2014; you may check the specification for any changes.)

Glyco answered 18/7, 2012 at 16:39 Comment(12)
@AshrafBashir I see the sample working fine in Firefox 19.0.2: I see ([{}]) logged to the console, which shows the expected MutationRecord when I click on it. Please check again, as it might have been a temporary technical failure in JSFiddle. I have not tested it in IE yet, since i don't have IE 10, which is currently the only version to support mutation events.Glyco
I just posted an answer that works in IE10+ and mostly anything else.Christyna
The specification doesn't seem to have a green box that lists the mutation observer options any longer. It does list the options in section 5.3.1 and describes them just a little further below that.Forefinger
@LS Thanks, I've updated the link, removed the bit about the green box, and edited the entire list into my answer (just in case of future link rot).Glyco
Here’s a browser compatibility table from Can I Use.Overstudy
Is it possible to see what changed the dom? Like a backtrace or somethingSerbocroatian
Holy moley - awesome answer, thank you. I feel like this is a hidden part of javascript I never encountered until I started working w/ Chrome Extension content script trying to detect DOM changes. I really appreciate the detailed explanation you put in and the links to documentation, helped me move along in my project when I hit this "unknown" territory.Amble
For me this works perfect on FF but not on Chrome :/Motive
In Chrome and this doesn't trigger anything.Twelvemonth
How do you monitor newly added sub nodes without an ID?Adowa
I wonder if it's possible to wait a specific element to be mounted, i mean, it depends on third party API, therefore, it might not get mounted. Is it possible to listen to a element that WILL be mounted, but still don't exit?Canebrake
I worked with the MutationObserver api directly for a while until I found arrive.js. If you need MutationObserver functionality then I strongly recommend using arrrive.js as it provides a clear query selector style api on top of MutationObserver that is far more intuitive and reliable (because I don't make as many subtle mistakes that are hard to troubleshoot).Priesthood
D
225

Edit

This answer is now deprecated. See the answer by apsillers.

Since this is for a Chrome extension, you might as well use the standard DOM event - DOMSubtreeModified. See the support for this event across browsers. It has been supported in Chrome since 1.0.

$("#someDiv").bind("DOMSubtreeModified", function() {
    alert("tree changed");
});

See a working example here.

Detritus answered 16/5, 2010 at 17:25 Comment(8)
I've noticed that this event can be fired even after certain selectors. I'm currently investigating.Izaguirre
w3.org/TR/DOM-Level-3-Events/#event-type-DOMSubtreeModified says this event is deprecated, what would we use instead?Demoralize
@Maslow- There isn't! #6660162Coulson
There is github.com/joelpurra/jquery-mutation-summary which basically solves it for jquery users.Norword
Still works if you're building chrome extensions. invaluable.Amplification
Still need the DOMSubtreeModified because apsillers answer only works in FF and Chrome uptil now ;/Ariminum
DOMwhatever events are harmful, because they make rendering engine check if there's an event handler on every DOM change. It slows down your dynamic content generation pretty bad in some cases. There are other solutions.Christyna
It still works but better stable solution is really MutationObserver.Dissymmetry
A
91

Many sites use AJAX/XHR/fetch to add, show, modify content dynamically and window.history API instead of in-site navigation so current URL is changed programmatically. Such sites are called SPA, short for Single Page Application.


Usual JS methods of detecting page changes

  • MutationObserver (docs) to literally detect DOM changes.

    Info/examples:

  • Event listener for sites that signal content change by sending a DOM event:

  • Periodic checking of DOM via setInterval:
    Obviously this will work only in cases when you wait for a specific element identified by its id/selector to appear, and it won't let you universally detect new dynamically added content unless you invent some kind of fingerprinting the existing contents.

  • Cloaking History API:

    let _pushState = History.prototype.pushState;
    History.prototype.pushState = function (state, title, url) {
      _pushState.call(this, state, title, url);
      console.log('URL changed', url)
    };
    
  • Listening to hashchange, popstate events:

    window.addEventListener('hashchange', e => {
      console.log('URL hash changed', e);
      doSomething();
    });
    window.addEventListener('popstate', e => {
      console.log('State changed', e);
      doSomething();
    });
    

P.S. All these methods can be used in a WebExtension's content script. It's because the case we're looking at is where the URL was changed via history.pushState or replaceState so the page itself remained the same with the same content script environment.

Antoniettaantonin answered 15/9, 2016 at 10:36 Comment(0)
S
29

Another approach depending on how you are changing the div. If you are using JQuery to change a div's contents with its html() method, you can extend that method and call a registration function each time you put html into a div.

(function( $, oldHtmlMethod ){
    // Override the core html method in the jQuery object.
    $.fn.html = function(){
        // Execute the original HTML method using the
        // augmented arguments collection.

        var results = oldHtmlMethod.apply( this, arguments );
        com.invisibility.elements.findAndRegisterElements(this);
        return results;

    };
})( jQuery, jQuery.fn.html );

We just intercept the calls to html(), call a registration function with this, which in the context refers to the target element getting new content, then we pass on the call to the original jquery.html() function. Remember to return the results of the original html() method, because JQuery expects it for method chaining.

For more info on method overriding and extension, check out http://www.bennadel.com/blog/2009-Using-Self-Executing-Function-Arguments-To-Override-Core-jQuery-Methods.htm, which is where I cribbed the closure function. Also check out the plugins tutorial at JQuery's site.

Sisterinlaw answered 24/5, 2012 at 17:1 Comment(0)
M
9

In addition to the "raw" tools provided by MutationObserver API, there exist "convenience" libraries to work with DOM mutations.

Consider: MutationObserver represents each DOM change in terms of subtrees. So if you're, for instance, waiting for a certain element to be inserted, it may be deep inside the children of mutations.mutation[i].addedNodes[j].

Another problem is when your own code, in reaction to mutations, changes DOM - you often want to filter it out.

A good convenience library that solves such problems is mutation-summary (disclaimer: I'm not the author, just a satisfied user), which enables you to specify queries of what you're interested in, and get exactly that.

Basic usage example from the docs:

var observer = new MutationSummary({
  callback: updateWidgets,
  queries: [{
    element: '[data-widget]'
  }]
});

function updateWidgets(summaries) {
  var widgetSummary = summaries[0];
  widgetSummary.added.forEach(buildNewWidget);
  widgetSummary.removed.forEach(cleanupExistingWidget);
}
Mulligan answered 10/5, 2016 at 12:0 Comment(1)
I have found arrive.js to be a very good MutationObserver convenience library. I echo your disclaimer, not the author but very satisfied user.Priesthood

© 2022 - 2024 — McMap. All rights reserved.