Search code examples
javascriptelementexecuteexists

Function that executes once and only when an element is fully loaded


I'm having a bit of difficulty getting a JavaScript function to execute once and only when a DOM element is fully loaded. I've tried countless combindations of setIntervals, seTimeouts, FOR loops, IF statements, WHILE loops, etc and gotten nowhere.

I did manage to get it to work once, but it's hit-and-miss as I could only get it to work by delaying the function by 2 seconds (which isn't good, as there's no telling exactly how long it takes to load) and rapidly re-firing the same function over and over, which also isn't good.

I just need something to constantly scan the page to tell whether an element exists and has content (innerHTML != undefined, or something), execute a block of code as soon as it is loaded (and only once) and then stop scanning the page.

Has anyone found a way to do this? Also, I need JavaScript, not jQuery.

Thanks.

Original Code

function injectLink_Bridge(){
    setTimeout(function(){
        injectLink();
    }, 2000);
}

function injectLink(){
    var headerElement = document.getElementsByClassName("flex-module-header");

    if (headerElement.innerHTML == undefined) {
        console.log("Element doesn't exist. Restarting function");

        setTimeout(function(){
            injectLink_Bridge(); //I can't remember if the bridge is necessary or not
        }, 2000); 
    } else {
        console.log("Element exists. Injecting");

        setTimeout(function(){
            headerElement[1].innerHTML += "code" //Inject code into the second instance of the class-array
        }, 2000);
    }
}

Finished code

function injectLink(){
    var headerElement = document.getElementsByClassName("flex-module-header")[1]; //Get the second instance

    if(headerElement && headerElement.innerHTML != ""){
        console.log("Element exists and has content. Injecting code...");
        headerElement.innerHTML += "code"; //Currently revising, due to T.J. Crowder's side-note
    } else {
        console.log("Element doesn't exist or has no content. Refiring function...");
        setTimeout(injectLink, 250);
    }
}

Solution

  • Updated answer after you posted code:

    getElementsByClassName returns a NodeList, not an element; NodeList doesn't have an innerHTML property. If you want the second matching element, use [1] to get it, and then test whether you got something back (or if you like you can use .length > 1 to check first, but NodeList is documented as returning null for indexes that don't exist). Also, you don't need a function wrapper around your existing function when passing it to setTimeout. So:

    function injectLink(){
        var headerElement = document.getElementsByClassName("flex-module-header")[1];
    
        if (!headerElement) {
            console.log("Element doesn't exist. Restarting function");
    
            setTimeout(injectLink, 2000); 
        } else {
            console.log("Element exists. Injecting");
    
            // Do you really want a further two second delay?
            setTimeout(function(){
                headerElement.innerHTML += "code"; //Inject code into the second instance of the class-array
            }, 2000);
        }
    }
    

    Side note: Using .innerHTML += "markup"; is generally not a great way to append content to an element. Here's what the browser does when you do that:

    1. Spins through the element and any descendant elements it has building an HTML string to give to the JavaScript layer.
    2. Tears down all content from the element, which has the by-product of removing any event handlers on descendant elements.
    3. Parses the HTML string and builds new nodes and elements for it.

    So a bit of work, and also the possibility of wiping out event handlers. If you're adding a new child element, look at using createElement and appendChild. If you're adding further text content, look at createTextNode and appendChild.


    Original answer:

    If you give your element an id (although the same concept works regardless of how you're finding the element), you can do this:

    (function() {
        setTimeout(check, 0);
        function check() {
            var elm = document.getElementById('theId');
            if (!elm) {
                setTimeout(check, 250); // Check again in a quarter second or so
                return;
            }
    
            // Use the element
        }
    })();
    

    There, we launch our first check almost immediately, and if we don't find the element, we schedule the check again 250ms later. Because we only schedule the next check if the previous one didn't find the element, we only loop as many times as necessary to find the element.

    If the element doesn't have an id, presumably you're finding it somehow (querySelector, querySelectorAll, etc.), and so will know whether you found it and will be able to schedule the next check.

    Probably worth putting a limit on it, e.g.:

    (function() {
        var start = new Date();
        setTimeout(check, 0);
        function check() {
            var elm = document.getElementById('theId');
            if (!elm) {
                if (new Date() - start > 5000) { // More than five seconds
                    throw "Couldn't find the element";
                }
                setTimeout(check, 250); // Check again in a quarter second or so
                return;
            }
    
            // Use the element
        }
    })();
    

    ...so if you change your HTML and the element doesn't exist anymore, you see an error when testing and are reminded to update your code.