Comment attendre qu'un élément existe?


237

Je travaille sur une extension dans Chrome et je me demande: quelle est la meilleure façon de savoir quand un élément existe? En utilisant du javascript simple, avec un intervalle qui vérifie jusqu'à ce qu'un élément existe, ou jQuery a-t-il un moyen simple de le faire?


1
Il semble que toutes les options ici aujourd'hui (y compris les commentaires) soient obsolètes ou incomplètes. Ils ne considèrent pas pleinement la formidable entrée de @ hughsk, l'argument de la compatibilité. En attendant, je recommanderais simplement d'utiliser la mise à jour de Brandon sur la réponse de Ryan pour une simplicité générale et moins de risque de surcharge, je suppose.
cregox

4
MutationObserver> DOM Mutation Events> setTimeout.
mattsven

2
Pas d'où je me tiens. setTimeoutest compatible, simple à mettre en œuvre, simple à entretenir et a une surcharge négligeable.
cregox

setTimeout+ jQueryest moins qu'idéal à mon avis pour deux raisons: 1.) jQuery bloat 2.) vous interrogez inutilement manuellement le DOM pour les éléments, les événements battent facilement en termes de vitesse, 3.) il sera toujours plus lent que n'importe quel natif la mise en oeuvre. Si vous devez faire quelque chose en fonction de la présence d'un élément assez rapidement, en particulier si une expérience utilisateur transparente est votre objectif, il est inférieur.
mattsven

3
Il existe 3 types de personnes: celles qui peuvent compter et celles qui ne le peuvent pas. ; P
cregox

Réponses:


149

DOMNodeInsertedest obsolète, ainsi que les autres événements de mutation DOM, en raison de problèmes de performances - l'approche recommandée consiste à utiliser un MutationObserver pour surveiller le DOM. Cependant, il n'est pris en charge que dans les nouveaux navigateurs, vous devriez donc vous rabattre sur le DOMNodeInsertedmoment où il MutationObservern'est pas disponible.

var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    if (!mutation.addedNodes) return

    for (var i = 0; i < mutation.addedNodes.length; i++) {
      // do things to your newly added nodes here
      var node = mutation.addedNodes[i]
    }
  })
})

observer.observe(document.body, {
    childList: true
  , subtree: true
  , attributes: false
  , characterData: false
})

// stop watching using:
observer.disconnect()

50
J'ai toujours trouvé l'API MutationObserver un peu complexe, j'ai donc construit une bibliothèque, arrivé.js , pour fournir une API plus simple pour écouter la création / suppression d'éléments.
Uzair Farooq

15
Je recommande l'utilisation de l'excellente bibliothèque @UzairFarooq github.com/uzairfarooq/arrive
Dennis

3
Deux choses à noter: (1) Il serait préférable de le faire if (mutation.addedNodes.length)car if (mutation.addedNodes)il retournerait toujours vrai même s'il s'agit d'un tableau vide. (2) Vous ne pouvez pas le faire mutation.addedNodes.forEach()car addedNodes est une nodeList et vous ne pouvez pas parcourir une nodeList avec forEach. Pour une solution à cela, voir toddmotto.com/ditch-the-array-foreach-call-nodelist-hack
thdoan

3
Pouvez-vous donner un exemple de la façon dont on pourrait utiliser cela? Je ne sais pas où mettre mon sélecteur jquery ou mon code que je veux exécuter lorsque l'élément DOM existe.
Superdooperhero

1
@Superdooperhero J'ai fait une réponse avec un exemple simple. Vérifie ça. stackoverflow.com/a/57395241/6542186
SilverSurfer

113

J'avais ce même problème, alors j'ai continué et j'ai écrit un plugin pour cela.

$(selector).waitUntilExists(function);

Code:

;(function ($, window) {

var intervals = {};
var removeListener = function(selector) {

    if (intervals[selector]) {

        window.clearInterval(intervals[selector]);
        intervals[selector] = null;
    }
};
var found = 'waitUntilExists.found';

/**
 * @function
 * @property {object} jQuery plugin which runs handler function once specified
 *           element is inserted into the DOM
 * @param {function|string} handler 
 *            A function to execute at the time when the element is inserted or 
 *            string "remove" to remove the listener from the given selector
 * @param {bool} shouldRunHandlerOnce 
 *            Optional: if true, handler is unbound after its first invocation
 * @example jQuery(selector).waitUntilExists(function);
 */

$.fn.waitUntilExists = function(handler, shouldRunHandlerOnce, isChild) {

    var selector = this.selector;
    var $this = $(selector);
    var $elements = $this.not(function() { return $(this).data(found); });

    if (handler === 'remove') {

        // Hijack and remove interval immediately if the code requests
        removeListener(selector);
    }
    else {

        // Run the handler on all found elements and mark as found
        $elements.each(handler).data(found, true);

        if (shouldRunHandlerOnce && $this.length) {

            // Element was found, implying the handler already ran for all 
            // matched elements
            removeListener(selector);
        }
        else if (!isChild) {

            // If this is a recurring search or if the target has not yet been 
            // found, create an interval to continue searching for the target
            intervals[selector] = window.setInterval(function () {

                $this.waitUntilExists(handler, shouldRunHandlerOnce, true);
            }, 500);
        }
    }

    return $this;
};

}(jQuery, window));

5
Merci pour le plugin. Je l'ai bifurqué et amélioré un peu. N'hésitez pas à prendre tout ce que vous voulez de ma mise à jour. J'ai encore quelques améliorations prévues: plugin mis à jour
Brandon Belvin

8
serait bien sans jquery dep aussi ...;)
knutole

4
vous devriez peut-être mentionner comment cela fonctionne: cela fonctionne en demandant toutes les 500 ms si l'élément existe (en utilisant a window.setInterval). Je ne sais pas si la MutationObserverréponse fonctionne aussi en sondant ...
sports

2
Cela ne fonctionne pas correctement si l'élément est déjà sur la page. Voici la version appropriée de cette fonction: gist.github.com/PizzaBrandon/5709010
Roland Soós

2
Pouvez-vous expliquer à quoi sert ;au début de la fonction ( ;(function ($, window) {)?
mrid

77

Voici une fonction JavaScript de base pour attendre l'affichage d'un élément.

Paramètres:

  1. selector: Cette fonction recherche l'élément $ {selector}
  2. time: Cette fonction vérifie si cet élément existe toutes les $ {time} millisecondes.

    function waitForElementToDisplay(selector, time) {
            if(document.querySelector(selector)!=null) {
                alert("The element is displayed, you can put your code instead of this alert.")
                return;
            }
            else {
                setTimeout(function() {
                    waitForElementToDisplay(selector, time);
                }, time);
            }
        }

Par exemple, en définissant selector="#div1"et time=5000recherchera la balise HTML dont id="div1"toutes les 5000 millisecondes.


Agréable! Pouvez-vous écrire ceci pour que tout sélecteur puisse être accepté?
mattsven

Je doute que je puisse le faire .. Mais s'il vous plaît jetez un oeil à ce post pour obtenir le getElementByXpath: stackoverflow.com/questions/10596417/…
Etienne Tonnelier


1
Pouvez-vous l'écrire pour utiliser l'observateur de mutation à la place?
SuperUberDuper

ou pourriez-vous réécrire celui-ci pour utiliser une promesse?
SuperUberDuper

25

Vous pouvez écouter DOMNodeInsertedouDOMSubtreeModified les événements qui le feu chaque fois qu'un nouvel élément est ajouté au DOM.

Il existe également un plugin LiveQuery jQuery qui détecterait quand un nouvel élément est créé:

$("#future_element").livequery(function(){
    //element created
});

1
Très joli plugin! Y a-t-il une fonction comme ça dans jquery directement? Je me demande s'il n'y a aucune fonctionnalité existante pour le faire. Et si c'est LE plugin, votez pour cette réponse;) Pour moi, cela fonctionne parfaitement. Merci beaucoup.
Samuel

1
Remarque IE 9 implémente DOMNodeInsert mais a un bug majeur où il ne se déclenche pas lorsque vous ajoutez un élément pour le moment, qui est la plupart du temps lorsque vous souhaitez l'utiliser. Les détails sont à: help.dottoro.com/ljmcxjla.php
mikemaccana

23

J'ai utilisé cette approche pour attendre qu'un élément apparaisse afin que je puisse exécuter les autres fonctions après cela.

Disons que la doTheRestOfTheStuff(parameters)fonction ne doit être appelée qu'après que l'élément avec l'ID soit the_Element_IDapparu ou que le chargement soit terminé, nous pouvons utiliser,

var existCondition = setInterval(function() {
 if ($('#the_Element_ID').length) {
    console.log("Exists!");
    clearInterval(existCondition);
    doTheRestOfTheStuff(parameters);
 }
}, 100); // check every 100ms

21

Tu peux faire

$('#yourelement').ready(function() {

});

Veuillez noter que cela ne fonctionnera que si l'élément est présent dans le DOM lors de la demande du serveur. Si l'élément est ajouté dynamiquement via JavaScript, cela ne fonctionnera pas et vous devrez peut-être consulter les autres réponses.


7
La .ready()fonction fonctionne pour presque tout (sinon rien), pas seulement document. Cela ne fonctionnera tout simplement pas avec des éléments créés dynamiquement, même sur .live().
Richard Neil Ilagan

7
@Bery, comme l'a souligné Richard, cela ne fonctionne que pour les éléments qui sont déjà présents dans le HTML lors de sa première demande au serveur. Si Javascript est utilisé pour ajouter un élément dynamiquement au DOM, cela ne fonctionne pas.
Chandranshu

6
@Sam, pouvez-vous préciser comment l'attacher à la référence de l'élément en mémoire?
Vikas Singhal

3
Cette réponse est incorrecte. Ce que vous vérifiez en fait ici est un habitué $(document).ready(), pas l'élément que vous pensez qu'il s'appliquera également. Voilà comment fonctionne cet auditeur spécial . Exemple
Shikkediel

1
Cette utilisation n'est pas recommandée selon api.jquery.com/ready
splintor

14

Je pense qu'il n'y a toujours pas de réponse ici avec un exemple de travail facile et lisible. Utiliser MutationObserver interface pour détecter les modifications DOM, comme ceci:

var observer = new MutationObserver(function(mutations) {
    if ($("p").length) {
        console.log("Exist, lets do something");
        observer.disconnect(); 
        //We can disconnect observer once the element exist if we dont want observe more changes in the DOM
    }
});

// Start observing
observer.observe(document.body, { //document.body is node target to observe
    childList: true, //This is a must have for the observer with subtree
    subtree: true //Set to true if changes must also be observed in descendants.
});
            
$(document).ready(function() {
    $("button").on("click", function() {
        $("p").remove();
        setTimeout(function() {
            $("#newContent").append("<p>New element</p>");
        }, 2000);
    });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<button>New content</button>
<div id="newContent"></div>

Remarque: Mozilla espagnol documents surMutationObserver sont plus détaillés si vous souhaitez plus d'informations.


2
Pensez à laisser un commentaire expliquant la raison du vote négatif, afin que je puisse améliorer ma réponse. Merci.
SilverSurfer

12

Ajoutez simplement le sélecteur souhaité. Une fois l'élément trouvé, vous pouvez y accéder dans la fonction de rappel.

const waitUntilElementExists = (selector, callback) => {
const el = document.querySelector(selector);

if (el){
    return callback(el);
}

setTimeout(() => waitUntilElementExists(selector, callback), 500);
}

waitUntilElementExists('.wait-for-me', (el) => console.log(el));

2
PossessWinin d'accord, c'est une solution très propre et fonctionne pour moi.
jstafford

3
Cette réponse fonctionne sur IE8-10 ainsi que sur les navigateurs modernes. Le principal problème est qu'il continuera de fonctionner si l'élément n'existe pas - il est donc préférable de savoir quand l'élément sera là. Sinon, vous pourriez ajouter un compteur.
Pour le nom

1
A parfaitement fonctionné pour moi
James Stewart

1
Fonctionné comme un charme !!
Aman

1
Ils étaient similaires, pas identiques. De plus, beaucoup de gens font de même. Enfin, j'ai codé cette solution moi-même. C'est un mauvais raisonnement, cependant, même si c'était effectivement le cas, j'apprécierais un commentaire pour me le faire savoir. La réponse résout le problème d'OP et n'a pas de motifs apparents pour être rétrogradé.
Diego Fortes

11

Pour une approche simple en utilisant jQuery, j'ai trouvé que cela fonctionnait bien:

  // Wait for element to exist.
  function elementLoaded(el, cb) {
    if ($(el).length) {
      // Element is now loaded.
      cb($(el));
    } else {
      // Repeat every 500ms.
      setTimeout(function() {
        elementLoaded(el, cb)
      }, 500);
    }
  };

  elementLoaded('.element-selector', function(el) {
    // Element is ready to use.
    el.click(function() {
      alert("You just clicked a dynamically inserted element");
    });
  });

Ici, nous vérifions simplement toutes les 500 ms pour voir si l'élément est chargé, quand il l'est, nous pouvons l'utiliser.

Ceci est particulièrement utile pour ajouter des gestionnaires de clics aux éléments qui ont été ajoutés dynamiquement au document.


8

Et le insertionQuery bibliothèque ?

insertionQuery utilise des rappels d'animation CSS attachés aux sélecteurs spécifiés pour exécuter un rappel lorsqu'un élément est créé. Cette méthode permet d'exécuter des rappels chaque fois qu'un élément est créé, pas seulement la première fois.

De github:

Manière sans événement dom pour capturer les nœuds qui apparaissent. Et il utilise des sélecteurs.

Ce n'est pas seulement pour une prise en charge plus large du navigateur, il peut être meilleur que DOMMutationObserver pour certaines choses.

Pourquoi?

  • Parce que les événements DOM ralentissent le navigateur et insertionQuery ne le fait pas
  • Parce que DOM Mutation Observer prend moins en charge le navigateur que insertionQuery
  • Parce qu'avec insertionQuery, vous pouvez filtrer les modifications DOM à l'aide de sélecteurs sans surcharge de performances!

Un soutien généralisé!

IE10 + et surtout toute autre chose (y compris mobile)


7

Voici une fonction qui agit comme un wrapper mince autour de MutationObserver. La seule exigence est que le navigateur prenne en charge MutationObserver; il n'y a pas de dépendance sur JQuery. Exécutez l'extrait ci-dessous pour voir un exemple de travail.

function waitForMutation(parentNode, isMatchFunc, handlerFunc, observeSubtree, disconnectAfterMatch) {
  var defaultIfUndefined = function(val, defaultVal) {
    return (typeof val === "undefined") ? defaultVal : val;
  };

  observeSubtree = defaultIfUndefined(observeSubtree, false);
  disconnectAfterMatch = defaultIfUndefined(disconnectAfterMatch, false);

  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.addedNodes) {
        for (var i = 0; i < mutation.addedNodes.length; i++) {
          var node = mutation.addedNodes[i];
          if (isMatchFunc(node)) {
            handlerFunc(node);
            if (disconnectAfterMatch) observer.disconnect();
          };
        }
      }
    });
  });

  observer.observe(parentNode, {
    childList: true,
    attributes: false,
    characterData: false,
    subtree: observeSubtree
  });
}

// Example
waitForMutation(
  // parentNode: Root node to observe. If the mutation you're looking for
  // might not occur directly below parentNode, pass 'true' to the
  // observeSubtree parameter.
  document.getElementById("outerContent"),
  // isMatchFunc: Function to identify a match. If it returns true,
  // handlerFunc will run.
  // MutationObserver only fires once per mutation, not once for every node
  // inside the mutation. If the element we're looking for is a child of
  // the newly-added element, we need to use something like
  // node.querySelector() to find it.
  function(node) {
    return node.querySelector(".foo") !== null;
  },
  // handlerFunc: Handler.
  function(node) {
    var elem = document.createElement("div");
    elem.appendChild(document.createTextNode("Added node (" + node.innerText + ")"));
    document.getElementById("log").appendChild(elem);
  },
  // observeSubtree
  true,
  // disconnectAfterMatch: If this is true the hanlerFunc will only run on
  // the first time that isMatchFunc returns true. If it's false, the handler
  // will continue to fire on matches.
  false);

// Set up UI. Using JQuery here for convenience.

$outerContent = $("#outerContent");
$innerContent = $("#innerContent");

$("#addOuter").on("click", function() {
  var newNode = $("<div><span class='foo'>Outer</span></div>");
  $outerContent.append(newNode);
});
$("#addInner").on("click", function() {
  var newNode = $("<div><span class='foo'>Inner</span></div>");
  $innerContent.append(newNode);
});
.content {
  padding: 1em;
  border: solid 1px black;
  overflow-y: auto;
}
#innerContent {
  height: 100px;
}
#outerContent {
  height: 200px;
}
#log {
  font-family: Courier;
  font-size: 10pt;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h2>Create some mutations</h2>
<div id="main">
  <button id="addOuter">Add outer node</button>
  <button id="addInner">Add inner node</button>
  <div class="content" id="outerContent">
    <div class="content" id="innerContent"></div>
  </div>
</div>
<h2>Log</h2>
<div id="log"></div>


6

Voici une solution de retour de promesse en Javascript vanille (pas de rappels compliqués). Par défaut, il vérifie toutes les 200 ms.

function waitFor(selector) {
    return new Promise(function (res, rej) {
        waitForElementToDisplay(selector, 200);
        function waitForElementToDisplay(selector, time) {
            if (document.querySelector(selector) != null) {
                res(document.querySelector(selector));
            }
            else {
                setTimeout(function () {
                    waitForElementToDisplay(selector, time);
                }, time);
            }
        }
    });
}

5

Voici une fonction Javascript pure qui vous permet d'attendre quoi que ce soit. Définissez l'intervalle plus longtemps pour prendre moins de ressources CPU.

/**
 * @brief Wait for something to be ready before triggering a timeout
 * @param {callback} isready Function which returns true when the thing we're waiting for has happened
 * @param {callback} success Function to call when the thing is ready
 * @param {callback} error Function to call if we time out before the event becomes ready
 * @param {int} count Number of times to retry the timeout (default 300 or 6s)
 * @param {int} interval Number of milliseconds to wait between attempts (default 20ms)
 */
function waitUntil(isready, success, error, count, interval){
    if (count === undefined) {
        count = 300;
    }
    if (interval === undefined) {
        interval = 20;
    }
    if (isready()) {
        success();
        return;
    }
    // The call back isn't ready. We need to wait for it
    setTimeout(function(){
        if (!count) {
            // We have run out of retries
            if (error !== undefined) {
                error();
            }
        } else {
            // Try again
            waitUntil(isready, success, error, count -1, interval);
        }
    }, interval);
}

Pour appeler cela, par exemple dans jQuery, utilisez quelque chose comme:

waitUntil(function(){
    return $('#myelement').length > 0;
}, function(){
    alert("myelement now exists");
}, function(){
    alert("I'm bored. I give up.");
});

3

Une solution renvoyant un Promiseet permettant d'utiliser un timeout (compatible IE 11+).

Pour un seul élément (type Element):

"use strict";

function waitUntilElementLoaded(selector) {
    var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

    var start = performance.now();
    var now = 0;

    return new Promise(function (resolve, reject) {
        var interval = setInterval(function () {
            var element = document.querySelector(selector);

            if (element instanceof Element) {
                clearInterval(interval);

                resolve();
            }

            now = performance.now();

            if (now - start >= timeout) {
                reject("Could not find the element " + selector + " within " + timeout + " ms");
            }
        }, 100);
    });
}

Pour plusieurs éléments (tapez NodeList):

"use strict";

function waitUntilElementsLoaded(selector) {
    var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

    var start = performance.now();
    var now = 0;

    return new Promise(function (resolve, reject) {
        var interval = setInterval(function () {
            var elements = document.querySelectorAll(selector);

            if (elements instanceof NodeList) {
                clearInterval(interval);

                resolve(elements);
            }

            now = performance.now();

            if (now - start >= timeout) {
                reject("Could not find elements " + selector + " within " + timeout + " ms");
            }
        }, 100);
    });
}

Exemples:

waitUntilElementLoaded('#message', 800).then(function(element) {
    // element found and available

    element.innerHTML = '...';
}).catch(function() {
    // element not found within 800 milliseconds
});

waitUntilElementsLoaded('.message', 10000).then(function(elements) {
    for(const element of elements) {
        // ....
    }
}).catch(function(error) {
    // elements not found withing 10 seconds
});

Fonctionne à la fois pour une liste d'éléments et un seul élément.


1
Ma solution préférée! Pourquoi vérifier element instanceof HTMLElement? Cela peut-il être autre chose que nullou HTMLElement?
Leeroy

1
Vous soulevez un point intéressant. J'aurais dû le rendre plus large en utilisant à la Elementplace (fixe). Je fais juste la vérification parce que je veux être sûr que la variable elementa la propriété innerHTMLcomme l' indique la documentation Element MDN . N'hésitez pas à le retirer si vous ne vous en souciez pas!
Anwar

2

Un exemple plus propre utilisant MutationObserver:

new MutationObserver( mutation => {
    if (!mutation.addedNodes) return
    mutation.addedNodes.forEach( node => {
        // do stuff with node
    })
})

2

Il s'agit d'une solution simple pour ceux qui sont habitués aux promesses et ne veulent pas utiliser de bibliothèques ou de minuteries tierces.

Je l'utilise depuis longtemps dans mes projets

function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                resolve(document.querySelector(selector));
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

Pour l'utiliser:

waitForElm('.some-class').then(elm => console.log(elm.textContent));

ou avec async / attente

const elm = await waitForElm('.some-classs')

C'est bien! La partie intéressante à ce sujet est que vous pouvez l'utiliser avec async/ awaitaussi. Vous pourriez également en tirer plus de performances en faisantmutations.addedNodes.find(node => node.matchesSelector("..."))
mattsven

@mattsven Bon point! Vérifier uniquement les nœuds dans les mutations est plus performant que faire document.querySelector.
Yong Wang

Veuillez corriger une faute d'orthographe, watiForElm à waitForElm
dalvir

1

Si vous voulez qu'il cesse de chercher après un certain temps (timeout), la jQuery suivante fonctionnera. Il expirera après 10 secondes. J'avais besoin d'utiliser ce code plutôt que du JS pur car j'avais besoin de sélectionner une entrée via le nom et j'avais du mal à implémenter certaines des autres solutions.

 // Wait for element to exist.

    function imageLoaded(el, cb,time) {

        if ($(el).length) {
            // Element is now loaded.

            cb($(el));

            var imageInput =  $('input[name=product\\[image_location\\]]');
            console.log(imageInput);

        } else if(time < 10000) {
            // Repeat every 500ms.
            setTimeout(function() {
               time = time+500;

                imageLoaded(el, cb, time)
            }, 500);
        }
    };

    var time = 500;

    imageLoaded('input[name=product\\[image_location\\]]', function(el) {

     //do stuff here 

     },time);

0

J'utilise généralement cet extrait pour Tag Manager:

<script>
(function exists() {
  if (!document.querySelector('<selector>')) {
    return setTimeout(exists);
  }
  // code when element exists
})();  
</script>

0

si vous avez des changements dom asynchrones, cette fonction vérifie (avec une limite de temps en secondes) pour les éléments DOM, elle ne sera pas lourde pour le DOM et sa promesse :)

function getElement(selector, i = 5) {
  return new Promise(async (resolve, reject) => {
    if(i <= 0) return reject(`${selector} not found`);
    const elements = document.querySelectorAll(selector);
    if(elements.length) return resolve(elements);
    return setTimeout(async () => await getElement(selector, i-1), 1000);
  })
}

// Now call it with your selector

try {
  element = await getElement('.woohoo');
} catch(e) { // catch the e }

//OR

getElement('.woohoo', 5)
.then(element => { // do somthing with the elements })
.catch(e => { // catch the error });
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.