Faire attendre la fonction jusqu'à ce que l'élément existe

159

J'essaie d'ajouter un canevas sur un autre canevas - comment puis-je faire attendre cette fonction pour démarrer jusqu'à ce que le premier canevas soit créé?

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;
    }
Steven
la source
4
Lorsque vous créez le canevas au clic, exécutez la fonction ou déclenchez un événement qui exécute un gestionnaire qui exécute la fonction. il n'y a pas d'événement inter-navigateur intégré qui se produit lorsqu'un élément devient disponible.
Kevin B
duplicata possible de Comment attendre qu'un élément existe?
user2284570

Réponses:

305

Si vous avez accès au code qui crée le canevas, appelez simplement la fonction juste après la création du canevas.

Si vous n'avez pas accès à ce code (par exemple, s'il s'agit d'un code tiers tel que Google Maps), vous pouvez tester l'existence dans un intervalle:

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

Mais notez que souvent, le code tiers a une option pour activer votre code (par rappel ou déclenchement d'événement) lorsqu'il a fini de se charger. C'est peut-être là que vous pouvez mettre votre fonction. La solution d'intervalle est vraiment une mauvaise solution et ne doit être utilisée que si rien d'autre ne fonctionne.

Iftah
la source
solution parfaite pour une utilisation dans angularjs typeahead. Merci de m'avoir guidé dans la bonne direction!
JuanTrev
1
Excellente solution pour attendre que quelque chose soit créé par Ajax avant d'y mettre quelque chose d'autre. Merci beaucoup.
Countzero
@iftah Comment pourrais-je faire fonctionner cela si le sélecteur est une variable? De plus, s'il s'agit d'un ID ou d'un sélecteur de classe, cela change également. Parfois, plusieurs éléments sont renvoyés lorsque je sélectionne avec une classe, et j'aurais besoin de trouver un moyen de transmettre un index au sélecteur pour déterminer lequel. Comment ferais-je cela? Merci
Kragalon
@Kraglon c'est une question complètement différente et ne convient pas aux commentaires de cette réponse. Je vous suggère de poser une nouvelle question, d'expliquer ce que vous avez essayé, quel est le problème, etc ...
Iftah
8
Une autre chose est importante à mentionner lors de l'utilisation de la solution donnée, vous devriez avoir ce morceau de code dans une boucle for et définir un compteur de tentatives maximum, si quelque chose ne va pas, vous ne vous retrouvez pas avec une boucle à l'infini :)
BJ
48

Selon le navigateur que vous devez prendre en charge, il existe l'option de MutationObserver .

EDIT: Tous les principaux navigateurs prennent en charge MutationObserver maintenant .

Quelque chose de ce genre devrait faire l'affaire:

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

NB Je n'ai pas testé ce code moi-même, mais c'est l'idée générale.

Vous pouvez facilement étendre cela pour rechercher uniquement la partie du DOM qui a changé. Pour cela, utilisez l' mutationsargument, c'est un tableau d' MutationRecordobjets.

putain
la source
2
J'ai adoré celui-ci. Je vous remercie.
insigne
1
Ce modèle est vraiment utile dans de nombreuses instances, en particulier si vous extrayez JS dans une page et que vous ne savez pas si d'autres éléments sont chargés.
Pour le nom
1
La meilleure réponse! Merci!
Antony Hatchkins
1
Je suis coincé avec un ancien navigateur (ff38) et cela m'a sauvé.
jung rhew du
1
Ceci est incroyable! J'aurais aimé savoir que cela existait plus tôt.
Rob
39

Cela ne fonctionnera qu'avec les navigateurs modernes, mais je trouve qu'il est plus facile d'utiliser simplement un thenalors testez d'abord mais:

Code

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

Ou en utilisant les fonctions du générateur

async function checkElement(selector) {
    const querySelector = document.querySelector(selector);
    while (querySelector === null) {
        await rafAsync()
    }
    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
la source
Il y a une erreur. Lors de l'utilisation des fonctions du générateur, le querySelector doit être mis à jour dans chaque boucle:while (document.querySelector(selector) === null) {await rafAsync()}
haofly
32

Une approche plus moderne de l'attente des éléments:

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

Notez que ce code devrait être enveloppé dans une fonction asynchrone .


la source
4
c'est plutôt chouette!
Dexter Bengil
Qu'y a-t- ril?
Daniel Möller
Bon, ok, mais d'où vient-il? Qu'est ce que ça fait? À quoi envoyez-vous setTimeout?
Daniel Möller
@ DanielMöller, vous devrez peut-être jeter un œil à Promises pour mieux comprendre ce code. Fondamentalement, ce que fait le code ici est de configurer un délai d'expiration de 500 ms et d'attendre qu'il se termine avant de lancer une nouvelle itération de la boucle while. Solution intelligente!
ClementParis016
8

Voici une amélioration mineure par rapport à la réponse de Jamie Hutber

const checkElement = async selector => {

while ( document.querySelector(selector) === null) {
    await new Promise( resolve =>  requestAnimationFrame(resolve) )
}

return document.querySelector(selector); };
wLc
la source
8

Il vaut mieux relayer requestAnimationFrameque dans un setTimeout. c'est ma solution dans les modules es6 et en utilisant Promises.

es6, modules et promesses:

// 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
  });
ncubica
la source
Uncaught TypeError: Cannot read property 'then' of undefined
Jeff Puckett
Je pense que j'ai raté un retour ... avant une nouvelle promesse.
ncubica
1
C'est la bonne solution, bien meilleure que toutes les vérifications périodiques basées sur la minuterie.
András Szepesházi
4
En fait, cela ne fonctionne pas dans sa forme actuelle. Si $ someElement est initialement nul (c'est-à-dire pas encore présent dans le DOM), alors vous passez cette valeur nulle (au lieu du sélecteur CSS) à votre fonction onElementReady et l'élément ne sera jamais résolu. Au lieu de cela, passez le sélecteur CSS sous forme de texte et essayez d'obtenir une référence à l'élément via .querySelector à chaque passage.
András Szepesházi
1
Cela a très bien fonctionné pour mon cas d'utilisation et me semble être une solution appropriée. Merci
rdhaese
5

Voici une solution utilisant des 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();                                                             
        });                                                                  
}                                                                            

Maintenant tu peux écrire

waitForElementToAppear(elementId).subscribe(el_ref => doSomethingWith(el_ref);
FrankL
la source
4

Vous pouvez vérifier si le dom existe déjà en définissant un délai jusqu'à ce qu'il soit déjà rendu dans le dom.

var panelMainWrapper = document.getElementById('panelMainWrapper');
setTimeout(function waitPanelMainWrapper() {
    if (document.body.contains(panelMainWrapper)) {
        $("#panelMainWrapper").html(data).fadeIn("fast");
    } else {
        setTimeout(waitPanelMainWrapper, 10);
    }
}, 10);
Carmela
la source
3

Si vous souhaitez une solution générique utilisant MutationObserver, vous pouvez utiliser cette fonction

// 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
Exemple d'utilisation

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

Remarque: MutationObserver a un excellent support de navigateur; https://caniuse.com/#feat=mutationobserver

Et voilà ! :)

rdhainaut
la source
2

Une autre variante d' 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);

Juste au cas où l'élément ne serait jamais affiché, nous ne vérifions donc pas indéfiniment.

Heriberto Perez
la source