239

I'm trying to add a canvas over another canvas – how can I make this function wait to start until the first canvas is created?

function PaintObject(brush) {

    this.started = false;

    // get handle of the main canvas, as a DOM object, not as a jQuery Object. Context is unfortunately not yet
    // available in jquery canvas wrapper object.
    var mainCanvas = $("#" + brush).get(0);

    // Check if everything is ok
    if (!mainCanvas) {alert("canvas undefined, does not seem to be supported by your browser");}
    if (!mainCanvas.getContext) {alert('Error: canvas.getContext() undefined !');}

    // Get the context for drawing in the canvas
    var mainContext = mainCanvas.getContext('2d');
    if (!mainContext) {alert("could not get the context for the main canvas");}

    this.getMainCanvas = function () {
        return mainCanvas;
    }
    this.getMainContext = function () {
        return mainContext;
    }

    // Prepare a second canvas on top of the previous one, kind of second "layer" that we will use
    // in order to draw elastic objects like a line, a rectangle or an ellipse we adjust using the mouse
    // and that follows mouse movements
    var frontCanvas = document.createElement('canvas');
    frontCanvas.id = 'canvasFront';
    // Add the temporary canvas as a second child of the mainCanvas parent.
    mainCanvas.parentNode.appendChild(frontCanvas);

    if (!frontCanvas) {
        alert("frontCanvas null");
    }
    if (!frontCanvas.getContext) {
        alert('Error: no frontCanvas.getContext!');
    }
    var frontContext = frontCanvas.getContext('2d');
    if (!frontContext) {
        alert("no TempContext null");
    }

    this.getFrontCanvas = function () {
        return frontCanvas;
    }
    this.getFrontContext = function () {
        return frontContext;
    }
meetar
  • 7,443
  • 8
  • 42
  • 73
Steven
  • 2,547
  • 2
  • 14
  • 7
  • 4
    When you create the canvas on click, run the function or trigger an event that runs a handler that runs the function. there is no built-in cross-browser event that happens when an element becomes available. – Kevin B Apr 22 '13 at 14:18
  • possible duplicate of [How to wait until an element exists?](http://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists) – user2284570 Oct 12 '14 at 19:59

14 Answers14

408

If you have access to the code that creates the canvas - simply call the function right there after the canvas is created.

If you have no access to that code (eg. If it is a 3rd party code such as google maps) then what you could do is test for the existence in an interval:

var checkExist = setInterval(function() {
   if ($('#the-canvas').length) {
      console.log("Exists!");
      clearInterval(checkExist);
   }
}, 100); // check every 100ms

But note - many times 3rd party code has an option to activate your code (by callback or event triggering) when it finishes to load. That may be where you can put your function. The interval solution is really a bad solution and should be used only if nothing else works.

Iftah
  • 9,512
  • 2
  • 33
  • 45
  • perfect solution for use in angularjs typeahead. Thanks for guiding me in right direction! – JuanTrev Oct 27 '14 at 00:02
  • 1
    Excellent solution to wait for something being created by Ajax before putting some other thing in there. Thanks a lot. – Countzero Nov 24 '15 at 09:13
  • @iftah How would I get this to work if the selector is a variable? Also, if it is an ID or Class selector changes as well. Sometimes there are multiple elements returned when I select with a class, and I would need to find a way to pass an index to the selector to figure out which one. How would I do this? Thanks – Kragalon Mar 16 '16 at 20:25
  • @Kraglon this is a completely different question and not suitable for comments of this answer. I suggest you ask a new question, explain what you tried, what the problem is, etc... – Iftah Mar 17 '16 at 10:40
  • I definitely prefer this over setTimeout if I'm waiting for a value to change – HussienK Jun 14 '16 at 15:56
  • 10
    One more thing is important to mention when using the given solution, you should have that piece of code inside a for loop and set a maximum retry counter, if something goes wrong you dont end up with an infinity loop :) – B.J. Nov 07 '16 at 12:43
  • @B.J. is right. Without modification for max count this is a STACK BOMB! – hypers Feb 14 '20 at 09:00
  • 2
    This is not a stack bomb, if the element never shows up then this just calls the function every 100ms (in this example). It is a waste of CPU cycles but it will not blow up. – Iftah Feb 14 '20 at 09:03
92

Depending on which browser you need to support, there's the option of MutationObserver.

EDIT: All major browsers support MutationObserver now.

Something along the lines of this should do the trick:

// callback executed when canvas was found
function handleCanvas(canvas) { ... }

// set up the mutation observer
var observer = new MutationObserver(function (mutations, me) {
  // `mutations` is an array of mutations that occurred
  // `me` is the MutationObserver instance
  var canvas = document.getElementById('my-canvas');
  if (canvas) {
    handleCanvas(canvas);
    me.disconnect(); // stop observing
    return;
  }
});

// start observing
observer.observe(document, {
  childList: true,
  subtree: true
});

N.B. I haven't tested this code myself, but that's the general idea.

You can easily extend this to only search the part of the DOM that changed. For that, use the mutations argument, it's an array of MutationRecord objects.

damd
  • 6,116
  • 7
  • 48
  • 77
56

This will only work with modern browsers but I find it easier to just use a then so please test first but:

ES5

function rafAsync() {
    return new Promise(resolve => {
        requestAnimationFrame(resolve); //faster than set time out
    });
}

function checkElement(selector) {
    if (document.querySelector(selector) === null) {
        return rafAsync().then(() => checkElement(selector));
    } else {
        return Promise.resolve(true);
    }
}

ES6

async function checkElement(selector) {
    const querySelector = null;
    while (querySelector === null) {
        await rafAsync();
        querySelector = document.querySelector(selector);
    }
    return querySelector;
}  

Usage

checkElement('body') //use whichever selector you want
.then((element) => {
     console.info(element);
     //Do whatever you want now the element is there
});
Jamie Hutber
  • 26,790
  • 46
  • 179
  • 291
  • 1
    There is an error. When using generator functions, the querySelector should be updated in every loop: `while (document.querySelector(selector) === null) {await rafAsync()}` – haofly Mar 20 '20 at 10:12
  • Can I ask why you would want to create so many variable assignments for the selector? The reason this is better afaik is it'll be faster, then having to check the selector every single time the animation frame is changed. – Jamie Hutber Jul 30 '20 at 12:47
53

A more modern approach to waiting for elements:

while(!document.querySelector(".my-selector")) {
  await new Promise(r => setTimeout(r, 500));
}
// now the element is loaded

Note that this code would need to be wrapped in an async function.

  • 1
    What is `r` there? – Daniel Möller Mar 29 '20 at 17:10
  • Well, ok, but where does it come from? What does it do? What are you sending to `setTimeout`? – Daniel Möller Mar 31 '20 at 03:28
  • @DanielMöller you may need to take a look at [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) to get a better understanding of this code. Basically what the code does here is setting up a timeout of 500ms and wait for it to complete before kicking a new iteration of the while loop. Clever solution! – ClementParis016 Apr 28 '20 at 08:03
  • This is perfect when running looping code in the Chrome console. It cuts my run time to about a third by not needing to use generic await 10 seconds here or 30 seconds there for fluctuating load times. And another note, if you are running looping code in the Chrome console then it doesn't need to be in an async function. You just place the above code in the place where you need to pause until the element is present. I don't know about other browsers. I only changed it to getElementById in stead of general querySelector. – John Muggins Jan 05 '22 at 15:43
42

Here's a minor improvement over Jamie Hutber's answer

const checkElement = async selector => {
  while ( document.querySelector(selector) === null) {
    await new Promise( resolve =>  requestAnimationFrame(resolve) )
  }
  return document.querySelector(selector); 
};

To use:

checkElement('.myElement').then((selector) => {
  console.log(selector);
});
Volomike
  • 23,743
  • 21
  • 113
  • 209
wLc
  • 968
  • 12
  • 15
  • Great answer. Least lines of code. This answer doesn't watch the clock and slow the page down tremendously -- instead, it uses `requestAnimationFrame`. Editing now to show how to use it. – Volomike Nov 12 '20 at 04:16
  • 1
    IMHO this is better than the other answers and uses Promises, which are more performant than `setInterval`. +1 – user11809641 Feb 24 '21 at 01:06
16

If you want a generic solution using MutationObserver you can use this function

// MIT Licensed
// Author: jwilson8767

/**
 * Waits for an element satisfying selector to exist, then resolves promise with the element.
 * Useful for resolving race conditions.
 *
 * @param selector
 * @returns {Promise}
 */
export function elementReady(selector) {
  return new Promise((resolve, reject) => {
    const el = document.querySelector(selector);
    if (el) {resolve(el);}
    new MutationObserver((mutationRecords, observer) => {
      // Query for elements matching the specified selector
      Array.from(document.querySelectorAll(selector)).forEach((element) => {
        resolve(element);
        //Once we have resolved we don't need the observer anymore.
        observer.disconnect();
      });
    })
      .observe(document.documentElement, {
        childList: true,
        subtree: true
      });
  });
}

Source: https://gist.github.com/jwilson8767/db379026efcbd932f64382db4b02853e
Example how to use it

elementReady('#someWidget').then((someWidget)=>{someWidget.remove();});

Note: MutationObserver has a great browser support; https://caniuse.com/#feat=mutationobserver

Et voilà ! :)

rdhainaut
  • 2,659
  • 1
  • 24
  • 34
10

Is better to relay in requestAnimationFrame than in a setTimeout. this is my solution in es6 modules and using Promises.

es6, modules and promises:

// onElementReady.js
const onElementReady = $element => (
  new Promise((resolve) => {
    const waitForElement = () => {
      if ($element) {
        resolve($element);
      } else {
        window.requestAnimationFrame(waitForElement);
      }
    };
    waitForElement();
  })
);

export default onElementReady;

// in your app
import onElementReady from './onElementReady';

const $someElement = document.querySelector('.some-className');
onElementReady($someElement)
  .then(() => {
    // your element is ready
  }

plain js and promises:

var onElementReady = function($element) {
  return new Promise((resolve) => {
    var waitForElement = function() {
      if ($element) {
        resolve($element);
      } else {
        window.requestAnimationFrame(waitForElement);
      }
    };
    waitForElement();
  })
};

var $someElement = document.querySelector('.some-className');
onElementReady($someElement)
  .then(() => {
    // your element is ready
  });
Si8
  • 9,141
  • 22
  • 109
  • 221
ncubica
  • 8,169
  • 9
  • 54
  • 72
  • `Uncaught TypeError: Cannot read property 'then' of undefined` – Jeff Puckett Oct 23 '17 at 18:44
  • I think I missed a return... before new Promise. – ncubica Oct 23 '17 at 19:22
  • 1
    This is the proper solution, much better than all the periodic Timer-based checkings. – András Szepesházi Feb 09 '18 at 19:35
  • 4
    Actually, this does not work in its current form. If $someElement is initially null (i.e. not yet present in the DOM), then you pass this null value (instead of the CSS selector) to your onElementReady function and the element will never be resolved. Instead, pass the CSS selector as text and try to get reference to the element via .querySelector on each pass. – András Szepesházi Feb 10 '18 at 09:08
  • @AndrásSzepesházi thtat's quite simple to fix pass a function instead of the element replace `if ($element)` for `if (getElement())` then doesn't matter if the element is null or not at the beginning the spirit of the solution is the same. That doesn't change anything from the answer. – ncubica Oct 30 '20 at 04:37
6

Here is a solution using observables.

waitForElementToAppear(elementId) {                                          

    return Observable.create(function(observer) {                            
            var el_ref;                                                      
            var f = () => {                                                  
                el_ref = document.getElementById(elementId);                 
                if (el_ref) {                                                
                    observer.next(el_ref);                                   
                    observer.complete();                                     
                    return;                                                  
                }                                                            
                window.requestAnimationFrame(f);                             
            };                                                               
            f();                                                             
        });                                                                  
}                                                                            

Now you can write

waitForElementToAppear(elementId).subscribe(el_ref => doSomethingWith(el_ref);
FrankL
  • 374
  • 4
  • 5
5

You can check if the dom already exists by setting a timeout until it is already rendered in the dom.

var panelMainWrapper = document.getElementById('panelMainWrapper');
setTimeout(function waitPanelMainWrapper() {
    if (document.body.contains(panelMainWrapper)) {
        $("#panelMainWrapper").html(data).fadeIn("fast");
    } else {
        setTimeout(waitPanelMainWrapper, 10);
    }
}, 10);
Markus Amalthea Magnuson
  • 8,415
  • 4
  • 41
  • 49
Carmela
  • 687
  • 1
  • 8
  • 15
5

Another variation of Iftah

var counter = 10;
var checkExist = setInterval(function() {
  console.log(counter);
  counter--
  if ($('#the-canvas').length || counter === 0) {
    console.log("by bye!");
    clearInterval(checkExist);
  }
}, 200);

Just in case the element is never shown, so we don't check infinitely.

Heriberto Magaña
  • 882
  • 10
  • 11
2

A pure promise based JavaScript approach, you can tell for many milliseconds to wait.

    const waitElementFor = function(query, ms = 3000) { // 3000 === 3 seconds
        return new Promise((resolve) => {
            var waited = 0;
            var el = null;
            var wi = setInterval(function() {
                el = document.querySelector(query);
                if (waited >= ms || el) {
                    clearInterval(wi);
                    if(el) {
                        resolve(el);
                    } else {
                        resolve(null);
                    }
                }
                waited += 10;
            }, 10);  
        });
    }

To use the function, simply use the following code in an asynchronous function.

var element = await waitElementFor('#elementID');

Snippet:

const waitElementFor = function(query, ms = 3000) { // 3000 === 3 seconds
    return new Promise((resolve) => {
        var waited = 0;
        var el = null;
        var wi = setInterval(function() {
            el = document.querySelector(query);
            if (waited >= ms || el) {
                clearInterval(wi);
                if(el) {
                    resolve(el);
                } else {
                    resolve(null);
                }
            }
            waited += 10;
        }, 10);  
    });
}

async function snippetTestAyncFunction(){
    var element = await waitElementFor('#elementID');
    console.log(element);
}

snippetTestAyncFunction();
Daniel Tonon
  • 9,261
  • 5
  • 61
  • 64
Mike M
  • 71
  • 5
1

Maybe I'm a little bit late :), but here is a nice and brief solution by chrisjhoughton, which allows to perform a callback function when the wait is over.

https://gist.github.com/chrisjhoughton/7890303

var waitForEl = function(selector, callback) {
  if (jQuery(selector).length) {
    callback();
  } else {
    setTimeout(function() {
      waitForEl(selector, callback);
    }, 100);
  }
};

waitForEl(selector, function() {
  // work the magic
});

If you need to pass parameters to a callback function, you can use it this way:

waitForEl("#" + elDomId, () => callbackFunction(param1, param2));

But be careful! This solution by default can fall into a trap of an infinite loop.

Several improvements of the topicstarter's suggestion are also provided in The GitHub thread.

Enjoy!

I. Ulitin
  • 11
  • 2
1

This is for those of you who are running code in the Chrome console and not just hard-coded into the html.

user993683 above offered code that will work in your console code. His/her code is as follows:

while(!document.querySelector(".my-selector")) {
  await new Promise(r => setTimeout(r, 500));
}
// now the element is loaded

He/she added that it "needs to be inside an async function." And if you are using code in Chrome's console then in fact you DON'T need to wrap it in a function. It will work just as written. You only need to place it in your code at the place right before you try to access the element to make sure it exists.

The only caveat is that it won't work on elements that are only sometimes present under other circumstances. Otherwise it will loop indefinitely if the element never downloads and you'll have to close the browser to stop the wait. Only use it for elements which you are certain will be present.

My company's form page has a dozen or more fields to fill out for each case number. And I have hundreds of case numbers in the script array every day. The elements do not all load simultaneously when changing the iFrame SRC and "onload" does not work in Chrome console scripts. So this method is a god-send to me and it saves me at least 45 minutes every day over the old generic async wait 10 seconds here or 30 seconds there due to fluctuating load times.

The only change I made is "getElementById" instead of the general "querySelector" because all of the elements I need have ID's.

while(!document.getElementById("myFrame").contentWindow.document.getElementById('someDocID')) {
      await new Promise(r => setTimeout(r, 500));
    }
// After completing the wait above it is now safe to access the element
document.getElementById("myFrame").contentWindow.document.getElementById('someDocID'
).innerText = "Smith, John R";
// and now click the submit button then change the SRC to a fresh form, and use
//*emphasized text* the code again to wait for it to fully load

I apologize to the monitors, but I added this as an answer because after several months of research on console scripts and waiting for elements to load, user993683's remark about a function finally made me realize that console scripts do not require a function for this code. My goal here is only to save other consoler script users the same learning curve that I went through.

John Muggins
  • 1,198
  • 1
  • 6
  • 12
0

Just use setTimeOut with recursion:

waitUntilElementIsPresent(callback: () => void): void {
    if (!this.methodToCheckIfElementIsPresent()) {
        setTimeout(() => this.waitUntilElementIsPresent(callback), 500);
        return;
    }
    callback();
}

Usage:

this.waitUntilElementIsPresent(() => console.log('Element is present!'));

You can limit amount of attempts, so an error will be thrown when the element is not present after the limit:

waitUntilElementIsPresent(callback: () => void, attempt: number = 0): void {
    const maxAttempts = 10;
    if (!this.methodToCheckIfElementIsPresent()) {
        attempt++;
        setTimeout(() => this.waitUntilElementIsPresent(callback, attempt), 500);
        return;
    } else if (attempt >= maxAttempts) {
        return;
    }
    callback();
}
Falk Jäger
  • 436
  • 6
  • 12