How can I be notified when an element is added to the page?
Asked Answered
L

8

170

I want a function of my choosing to run when a DOM element is added to the page. This is in the context of a browser extension, so the webpage runs independently of me and I cannot modify its source. What are my options here?

I guess that, in theory, I could just use setInterval() to continually search for the element's presence and perform my action if the element is there, but I need a better approach.

Lee answered 15/9, 2011 at 17:1 Comment(5)
Do you have to check for a particular element another script of yours put into the page or for any element which is added no matter the source?Hrvatska
do you know what function adds the element in someone else's code, if so you can overwrite it and add one extra line triggering a custom event?Hanover
possible duplicate of Is there a jQuery DOM change listener?Modena
Does this answer your question? Determining if a HTML element has been added to the DOM dynamicallyQuarterphase
Duplicate vote because the accepted answer recommends (and links to) another solution, which is provided over here: https://mcmap.net/q/145115/-determining-if-a-html-element-has-been-added-to-the-dom-dynamically/712526Quarterphase
H
99

Warning!

This answer is now outdated. DOM Level 4 introduced MutationObserver, providing an effective replacement for the deprecated mutation events. See this answer to another question for a better solution than the one presented here. Seriously. Don't poll the DOM every 100 milliseconds; it will waste CPU power and your users will hate you.

Since mutation events were deprecated in 2012, and you have no control over the inserted elements because they are added by someone else's code, your only option is to continuously check for them.

function checkDOMChange()
{
    // check for any new element being inserted here,
    // or a particular node being modified

    // call the function again after 100 milliseconds
    setTimeout( checkDOMChange, 100 );
}

Once this function is called, it will run every 100 milliseconds, which is 1/10 (one tenth) of a second. Unless you need real-time element observation, it should be enough.

Hrvatska answered 15/9, 2011 at 17:30 Comment(11)
jose, how do you end your settimeout when the condition is actually met? ie, you found the element that finally loaded x seconds later onto your page?Goodden
@blachawk you need to assign the setTimeout return value to a variable, which you can later pass as a paratemer to clearTimeout() to clear it.Hrvatska
@blackhawk : I just saw that answer and +1 it; but you'd be aware that you actually don't need to use clearTimeout() here, since setTimeout() only run once! One 'if-else' is just sufficient: don't let execution pass on setTimeout() once you have found the inserted element.Jaimie
A useful practical guide to using MutationObserver(): davidwalsh.name/mutationobserver-apiGarland
That's dirty. Is there really no "equivalent" to AS3's Event.ADDED_TO_STAGE?Clarance
@Clarance I'm looking for a similar event as wellCookgeneral
If you use this method, make sure you don't call the function with () i.e. setTimeout( checkDOMChange(), 100 ); This will overflow your browser with function calls.Azotize
@Clarance if i understand what you're looking for, i think my answer below might do the trickPolacca
HTML/JavaScript still a decade behind AS3 in 2016. Seriously, no common DisplayObject inheriting from EventDispatcher. No common event model, muddled by jQuery's own events, closures, and 'off' methods that just wipe out all events, whether you own them or not. No equivalent "added" or "removed" events on nodes, nor an equivalent "added to document" event. JavaScript really, really is behind and incapable.Careen
Don't do this if you have a large DOM. Constantly scanning the DOM for a new element just isn't cool.Musk
An efficient way to check if the element is added to DOM is provided by HTMLElement:closest() like this: if (element.closest('body') === document.body) { ... }Tallow
K
47

The actual answer is "use mutation observers" (as outlined in this question: Determining if a HTML element has been added to the DOM dynamically), however support (specifically on IE) is limited (http://caniuse.com/mutationobserver).

So the actual ACTUAL answer is "Use mutation observers.... eventually. But go with Jose Faeti's answer for now" :)

Kalynkam answered 15/8, 2013 at 16:33 Comment(0)
L
22

Between the deprecation of mutation events and the emergence of MutationObserver, an efficent way to be notified when a specific element was added to the DOM was to exploit CSS3 animation events.

To quote the blog post:

Setup a CSS keyframe sequence that targets (via your choice of CSS selector) whatever DOM elements you want to receive a DOM node insertion event for. I used a relatively benign and little used css property, clip I used outline-color in an attempt to avoid messing with intended page styles – the code once targeted the clip property, but it is no longer animatable in IE as of version 11. That said, any property that can be animated will work, choose whichever one you like.

Next I added a document-wide animationstart listener that I use as a delegate to process the node insertions. The animation event has a property called animationName on it that tells you which keyframe sequence kicked off the animation. Just make sure the animationName property is the same as the keyframe sequence name you added for node insertions and you’re good to go.

Lissome answered 7/6, 2014 at 8:11 Comment(5)
This is the highest-performance solution, and there's an answer in a duplicate question mentioning a library for this.Newhall
Underrated answer.Pronoun
Careful. If millisecond precision is important, this approach is far from accurate. This jsbin demonstrates that there is more than 30ms difference between an inline callback and using animationstart, jsbin.com/netuquralu/1/edit.Bilbo
I agree with kurt, this is underrated.Shuster
Please make sure the element doesn't have 'display:none;' style applied to it, It causes the element to not render and hence the trick would not work even If you can see the element present on inspecting.Bilski
P
17

ETA 24 Apr 17 I wanted to simplify this a bit with some async/await magic, as it makes it a lot more succinct:

Using the same promisified-observable:

const startObservable = (domNode) => {
  var targetNode = domNode;

  var observerConfig = {
    attributes: true,
    childList: true,
    characterData: true
  };

  return new Promise((resolve) => {
      var observer = new MutationObserver(function (mutations) {
         // For the sake of...observation...let's output the mutation to console to see how this all works
         mutations.forEach(function (mutation) {
             console.log(mutation.type);
         });
         resolve(mutations)
     });
     observer.observe(targetNode, observerConfig);
   })
} 

Your calling function can be as simple as:

const waitForMutation = async () => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {
      const results = await startObservable(someDomNode)
      return results
    } catch (err) { 
      console.error(err)
    }
}

If you wanted to add a timeout, you could use a simple Promise.race pattern as demonstrated here:

const waitForMutation = async (timeout = 5000 /*in ms*/) => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {

      const results = await Promise.race([
          startObservable(someDomNode),
          // this will throw after the timeout, skipping 
          // the return & going to the catch block
          new Promise((resolve, reject) => setTimeout(
             reject, 
             timeout, 
             new Error('timed out waiting for mutation')
          )
       ])
      return results
    } catch (err) { 
      console.error(err)
    }
}

Original

You can do this without libraries, but you'd have to use some ES6 stuff, so be cognizant of compatibility issues (i.e., if your audience is mostly Amish, luddite or, worse, IE8 users)

First, we'll use the MutationObserver API to construct an observer object. We'll wrap this object in a promise, and resolve() when the callback is fired (h/t davidwalshblog)david walsh blog article on mutations:

const startObservable = (domNode) => {
    var targetNode = domNode;

    var observerConfig = {
        attributes: true,
        childList: true,
        characterData: true
    };

    return new Promise((resolve) => {
        var observer = new MutationObserver(function (mutations) {
            // For the sake of...observation...let's output the mutation to console to see how this all works
            mutations.forEach(function (mutation) {
                console.log(mutation.type);
            });
            resolve(mutations)
        });
        observer.observe(targetNode, observerConfig);
    })
} 

Then, we'll create a generator function. If you haven't used these yet, then you're missing out--but a brief synopsis is: it runs like a sync function, and when it finds a yield <Promise> expression, it waits in a non-blocking fashion for the promise to be fulfilled (Generators do more than this, but this is what we're interested in here).

// we'll declare our DOM node here, too
let targ = document.querySelector('#domNodeToWatch')

function* getMutation() {
    console.log("Starting")
    var mutations = yield startObservable(targ)
    console.log("done")
}

A tricky part about generators is they don't 'return' like a normal function. So, we'll use a helper function to be able to use the generator like a regular function. (again, h/t to dwb)

function runGenerator(g) {
    var it = g(), ret;

    // asynchronously iterate over generator
    (function iterate(val){
        ret = it.next( val );

        if (!ret.done) {
            // poor man's "is it a promise?" test
            if ("then" in ret.value) {
                // wait on the promise
                ret.value.then( iterate );
            }
            // immediate value: just send right back in
            else {
                // avoid synchronous recursion
                setTimeout( function(){
                    iterate( ret.value );
                }, 0 );
            }
        }
    })();
}

Then, at any point before the expected DOM mutation might happen, simply run runGenerator(getMutation).

Now you can integrate DOM mutations into a synchronous-style control flow. How bout that.

Polacca answered 19/9, 2016 at 17:51 Comment(0)
H
13

(see revisited answer at the bottom)

You can use livequery plugin for jQuery. You can provide a selector expression such as:

$("input[type=button].removeItemButton").livequery(function () {
    $("#statusBar").text('You may now remove items.');
});

Every time a button of a removeItemButton class is added a message appears in a status bar.

In terms of efficiency you might want avoid this, but in any case you could leverage the plugin instead of creating your own event handlers.

Revisited answer

The answer above was only meant to detect that an item has been added to the DOM through the plugin.

However, most likely, a jQuery.on() approach would be more appropriate, for example:

$("#myParentContainer").on('click', '.removeItemButton', function(){
          alert($(this).text() + ' has been removed');
});

If you have dynamic content that should respond to clicks for example, it's best to bind events to a parent container using jQuery.on.

Heartland answered 20/10, 2011 at 4:46 Comment(0)
T
5

Check out this plugin that does exacly that - jquery.initialize

It works exacly like .each function, the difference is it takes selector you've entered and watch for new items added in future matching this selector and initialize them

Initialize looks like this

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

But now if new element matching .some-element selector will appear on page, it will be instanty initialized.

The way new item is added is not important, you dont need to care about any callbacks etc.

So if you'd add new element like:

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

it will be instantly initialized.

Plugin is based on MutationObserver

Tobin answered 4/2, 2015 at 1:23 Comment(0)
C
1

A pure javascript solution (without jQuery):

const SEARCH_DELAY = 100; // in ms

// it may run indefinitely. TODO: make it cancellable, using Promise's `reject`
function waitForElementToBeAdded(cssSelector) {
  return new Promise((resolve) => {
    const interval = setInterval(() => {
      if (element = document.querySelector(cssSelector)) {
        clearInterval(interval);
        resolve(element);
      }
    }, SEARCH_DELAY);
  });
}

console.log(await waitForElementToBeAdded('#main'));
Champlin answered 15/12, 2018 at 9:9 Comment(0)
D
0

With jQuery you can do -

function nodeInserted(elementQuerySelector){
    if ($(elementQuerySelector).length===0){
        setTimeout(function(){
            nodeInserted(elementQuerySelector);
        },100);
    }else{
        $(document).trigger("nodeInserted",[elementQuerySelector]);
    }
}

The function search recursively for the node until it finds it then trigger an event against the document

Then you can use this to implement it

nodeInserted("main");
$(document).on("nodeInserted",function(e,q){
    if (q === "main"){
        $("main").css("padding-left",0);
    }
});
Dollar answered 13/6, 2021 at 14:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.