25

So I am creating my page in my companies template, which only allow us access to the body of the page. We don't have access to the head tag and there are scripts loaded on a lower part of the page that we don't have access to. One of these scripts dynamically loads an element onto the page. I need to run another script on that element, but because the element isn't loaded on the page until after my script has already ran I can't access that element. Is there a way I can check to see if that element has already been loaded on the page before running my script?

Let me know if I need to explain better.

<head>Don't have access</head>


<body>
   <!--Needs to manipulate the element created by the companyScript.js--> 
   <script src="myScript.js"></script>

   <!--Script that runs after mine, which I don't have access too-->
   <script src="companyScript.js">
       /*Dynamically adds <div class="element"></div> to page*/
   </script>
</body>
user2052567
  • 409
  • 1
  • 5
  • 11

5 Answers5

43

2022 Version, with MutationObserver

Rewrote the 2020 code to use a MutationObserver instead of polling.

/**
 * Wait for an element before resolving a promise
 * @param {String} querySelector - Selector of element to wait for
 * @param {Integer} timeout - Milliseconds to wait before timing out, or 0 for no timeout              
 */
function waitForElement(querySelector, timeout){
  return new Promise((resolve, reject)=>{
    var timer = false;
    if(document.querySelectorAll(querySelector).length) return resolve();
    const observer = new MutationObserver(()=>{
      if(document.querySelectorAll(querySelector).length){
        observer.disconnect();
        if(timer !== false) clearTimeout(timer);
        return resolve();
      }
    });
    observer.observe(document.body, {
      childList: true, 
      subtree: true
    });
    if(timeout) timer = setTimeout(()=>{
      observer.disconnect();
      reject();
    }, timeout);
  });
}

waitForElement("#idOfElementToWaitFor", 3000).then(function(){
    alert("element is loaded.. do stuff");
}).catch(()=>{
    alert("element did not load in 3 seconds");
});

2020 version, with promises

This code will poll the DOM 10x/second and resolve a promise if it's found before the optional timeout.

/**
 * Wait for an element before resolving a promise
 * @param {String} querySelector - Selector of element to wait for
 * @param {Integer} timeout - Milliseconds to wait before timing out, or 0 for no timeout              
 */
function waitForElement(querySelector, timeout=0){
    const startTime = new Date().getTime();
    return new Promise((resolve, reject)=>{
        const timer = setInterval(()=>{
            const now = new Date().getTime();
            if(document.querySelector(querySelector)){
                clearInterval(timer);
                resolve();
            }else if(timeout && now - startTime >= timeout){
                clearInterval(timer);
                reject();
            }
        }, 100);
    });
}


waitForElement("#idOfElementToWaitFor", 3000).then(function(){
    alert("element is loaded.. do stuff");
}).catch(()=>{
    alert("element did not load in 3 seconds");
});

Original answer

function waitForElement(id, callback){
    var poops = setInterval(function(){
        if(document.getElementById(id)){
            clearInterval(poops);
            callback();
        }
    }, 100);
}

waitForElement("idOfElementToWaitFor", function(){
    alert("element is loaded.. do stuff");
});
I wrestled a bear once.
  • 22,983
  • 19
  • 69
  • 116
  • Genious! ;-) SO simple... yet effective! The only piece of code effectively works on Angular: calling of the function on `ngAfterViewInit()` and that's it! Thanks! – Pedro Ferreira Oct 29 '19 at 21:49
  • I liked your 2020 code, could you please explain about the if statements inside the waitForElement function? – Mathew Aug 23 '20 at 12:42
  • 1
    That is a very good code, it needs some explanation, but what this code is doing is to detect if the element is loaded then do stuff, what this code is not doing is to do stuff WHILE the element is being loaded – Mathew Aug 23 '20 at 12:44
  • @Mathew - That's what promises do, they let you do stuff while you wait for something else. The `if` statement just checks to see if the element exists yet. – I wrestled a bear once. Aug 24 '20 at 14:41
  • @Iwrestledabearonce. I'm trying to disable `observer.disconnect()` in MutationObserver to keep it always running, but I'm not figuring out to do so. Any clue? – LuizC Sep 28 '22 at 22:44
26

Sounds like a job for MutationObserver!

A MutationObserver is like an event listener: you can attach it to any DOM element to listen for changes:

var observer = new MutationObserver(function (mutationRecords) {
    console.log("change detected");
});

The callback is passed an array of MutationRecords, which hold different lists of added/deleted/modified nodes.

Then, we attach the observer to any node:

observer.observe(document.body, {childList: true});
// second argument is a config: in this case, only fire the callback when nodes are added or removed

Note: IE support is not amazing. (surprise, surprise) Only works on IE 11. (Edge supports it, though.)

Here's another SO question about detecting changes to the DOM: Detect changes in the DOM

Snippet:

document.querySelector("button").addEventListener("click", function () {
  document.body.appendChild(document.createElement("span"));
});

var observer = new MutationObserver(function (m) {
  if (m[0].addedNodes[0].nodeName === "SPAN")
    document.querySelector("div").innerHTML += "Change Detected<br>";
});

observer.observe(document.body, {childList: true});
<button>Click</button>
<div></div>
Hatchet
  • 5,320
  • 1
  • 30
  • 42
1

I would recommend against using MutationObserver. This approach has many drawbacks: it slows page loading, has poor browser support. There are situations when this is the only approach to solve the problem at hand.

A better approach is to use the onload DOM event. This event works on tags such as iframe and img. For other tags, this is the pseudo code you can take inspiration from:

<body>
<script>
function divLoaded(event){
  alert(event.target.previousSibling.previousSibling.id);
}
</script>
<div id="element_to_watch">This is an example div to watch it's loading</div>
<iframe style="display: none; width: 0; height: 0" onload="divLoaded(event)"/>
</body>

NOTE: The trick is to attach an iframe or img element after every element you want to watch for their loading.

Shyam Swaroop
  • 793
  • 10
  • 7
  • if you follow this approach, this specific question can be solved in many ways like you can watch for loading of script tag and then dynamically load another script using javascript. – Shyam Swaroop Feb 27 '20 at 19:49
0

Here's a function written in ClojureScript which invokes some function whenever a condition is met.

(require '[cljs.core.async :as a :refer [<!]]
         '[cljs-await.core :refer [await]])
(require-macros '[cljs.core.async.macros :refer [go]])

(defn wait-for-condition
  [pred-fn resolve-fn & {:keys [reject-fn timeout frequency]
                         :or {timeout 30 frequency 100}}]

  (let [timeout-ms (atom (* timeout 1000))]
    ;; `def` instead of `let` so it can recurse
    (def check-pred-satisfied
      (fn []
        (swap! timeout-ms #(- % frequency))
        (if (pred-fn)
          (resolve-fn)
          (if (pos? @timeout-ms)
            (js/setTimeout check-pred-satisfied frequency)
            (when reject-fn
              (reject-fn))))))

    (go (<! (await (js/Promise. #(js/setTimeout check-pred-satisfied frequency)))))))

Then you could do something like

wait_for_condition((id) => document.getElementById(id), () => alert("it happened"))

or

(wait-for-condition #(js/document.getElementById id)
                    #(js/alert "it happened")
                    :frequency 250
                    :timeout 10)
0

Expanding on @i-wrestled-a-bear-once's solution. With this js code, we can disable the popup as well when the target component is not present in the DOM anymore.

It can be used for the cases when you're asking for a file input from for a request, and don't want to display the file after he's done with the request.

// Call this to run a method when a element removed from DOM
function waitForElementToUnLoad(querySelector) {
    const startTime = new Date().getTime();
    return new Promise((resolve, reject)=>{
        const timer = setInterval(()=>{
            const now = new Date().getTime();
            if(!document.querySelector(querySelector)) {
                clearInterval(timer);
                resolve();
            }
        }, 1000);
    });
}

// Call this to run a method when a element added to DOM, set timeout if required.
// Checks every second.
function waitForElementToLoad(querySelector, timeout=0) {
    const startTime = new Date().getTime();
    return new Promise((resolve, reject)=>{
        const timer = setInterval(()=>{
            const now = new Date().getTime();
            if(document.querySelector(querySelector)){
                clearInterval(timer);
                resolve();
            }else if(timeout && now - startTime >= timeout){
                clearInterval(timer);
                reject();
            }
        }, 1000);
    });
}

// Removing onclose popup if no file is uploaded.
function executeUnloadPromise(id) {
  waitForElementToUnLoad(id).then(function(){
      window.onbeforeunload = null;
      executeLoadPromise("#uploaded-file");
  });
}

// Adding onclose popup if a file is uploaded.
function executeLoadPromise(id) {
  waitForElementToLoad(id).then(function(){
      window.onbeforeunload = function(event) { 
        return "Are you sure you want to leave?"; 
      }
      executeUnloadPromise("#uploaded-file");
  });
}

executeLoadPromise("#uploaded-file");

Would love suggestions on how I can make this better.