¿Cómo esperar hasta que exista un elemento?

237

Estoy trabajando en una extensión en Chrome, y me pregunto: ¿cuál es la mejor manera de saber cuándo un elemento surge? Usando JavaScript simple, con un intervalo que verifica hasta que exista un elemento, ¿o jQuery tiene alguna manera fácil de hacer esto?

Mattsven
fuente
1
Parece que todas las opciones aquí hoy (incluidas las de los comentarios) están desactualizadas o incompletas. No consideran completamente la impresionante entrada de @hughsk, el argumento de compatibilidad. Mientras tanto, recomendaría simplemente usar la actualización de Brandon sobre la respuesta de Ryan por simplicidad general y menos riesgo de sobrecarga, supongo.
cregox
44
MutationObserver> DOM Mutation Events> setTimeout.
mattsven
2
No desde donde estoy parado. setTimeoutes compatible, simple de implementar, simple de mantener y tiene una sobrecarga insignificante.
cregox
setTimeout+ jQueryes menos que ideal en mi opinión por dos razones: 1.) jQuery bloat 2.) estás consultando innecesariamente manualmente el DOM en busca de elementos, los eventos superan esa velocidad fácilmente, 3.) siempre será más lento que cualquier nativo implementación. Si necesita hacer algo basado en la presencia de un elemento de manera razonablemente rápida, especialmente si su objetivo es una experiencia fluida del usuario, es inferior.
mattsven
3
Hay 3 tipos de personas: las que pueden contar y las que no. ; P
cregox

Respuestas:

149

DOMNodeInsertedse está desaprobando, junto con otros eventos de mutación DOM, debido a problemas de rendimiento: el enfoque recomendado es usar un MutationObserver para ver el DOM. Sin embargo, solo es compatible con los navegadores más nuevos, por lo que debe recurrir DOMNodeInsertedcuando MutationObserverno esté 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()
Hughsk
fuente
50
Siempre encontré que la API MutationObserver es un poco compleja, así que he creado una biblioteca, begin.js , para proporcionar una API más simple para escuchar la creación / eliminación de elementos.
Uzair Farooq
15
Recomiendo usar @UzairFarooq excelente biblioteca github.com/uzairfarooq/arrive
Dennis
3
Dos cosas a tener en cuenta: (1) Sería mejor hacerlo, if (mutation.addedNodes.length)ya if (mutation.addedNodes)que aún sería verdadero incluso si se trata de una matriz vacía. (2) No puede hacerlo mutation.addedNodes.forEach()porque AddedNodes es una lista de nodos y no puede iterar a través de una lista de nodos con forEach. Para una solución a esto, vea toddmotto.com/ditch-the-array-foreach-call-nodelist-hack
thdoan
3
¿Puedes dar un ejemplo de cómo se usaría esto? No estoy seguro de dónde poner mi selector jquery o el código que quiero ejecutar cuando existe un elemento DOM.
Superdooperhero
1
@Superdooperhero Hice una respuesta con un ejemplo sencillo. Revisalo. stackoverflow.com/a/57395241/6542186
SilverSurfer el
113

Estaba teniendo este mismo problema, así que seguí adelante y escribí un complemento para ello.

$(selector).waitUntilExists(function);

Código:

;(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));
Ryan Lester
fuente
55
Gracias por el complemento. Lo bifurqué y lo mejoré un poco. Siéntase libre de tomar lo que quiera de mi actualización. Tengo algunas mejoras más planeadas, todavía: complemento actualizado
Brandon Belvin
8
estaría bien sin jquery dep también ...;)
knutole
44
quizás debería mencionar cómo funciona: funciona preguntando cada 500 ms si el elemento existe (usando a window.setInterval). No sé si la MutationObserverrespuesta también funciona por votación ...
deportes
2
No funciona correctamente si el elemento ya está en la página. Aquí está la versión adecuada de esta función: gist.github.com/PizzaBrandon/5709010
Roland Soós
2
¿Puede explicar cuál es el uso ;al comienzo de la función ( ;(function ($, window) {)?
mrid
77

Aquí hay una función básica de JavaScript para esperar la visualización de un elemento.

Parámetros:

  1. selector: Esta función busca el elemento $ {selector}
  2. time: Esta función verifica si este elemento existe cada $ {time} milisegundos.

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

Como ejemplo, configura selector="#div1"y time=5000buscará la etiqueta HTML id="div1"cada 5000 milisegundos.

Etienne Tonnelier
fuente
¡Agradable! ¿Puedes escribir esto para que cualquier selector pueda ser aceptado?
mattsven
Dudo que pueda hacerlo ... Pero por favor, eche un vistazo a esta publicación para obtener getElementByXpath: stackoverflow.com/questions/10596417/…
Etienne Tonnelier
1
¿Qué hay de querySelector? developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
mattsven
1
¿Puedes escribirlo para usar el observador de mutaciones?
SuperUberDuper el
o podrías reescribir este para usar una promesa?
SuperUberDuper el
25

Puede escuchar DOMNodeInsertedo DOMSubtreeModifiedeventos que se activan cada vez que se agrega un nuevo elemento al DOM.

También existe el complemento LiveQuery jQuery que detectaría cuándo se crea un nuevo elemento:

$("#future_element").livequery(function(){
    //element created
});
serg
fuente
1
Muy buen complemento! ¿Hay alguna función como esa en jquery directamente? Me pregunto si no hay una función existente para hacer eso. Y si este es EL complemento, vote por esta respuesta;) Para mí, funciona perfectamente. Muchas gracias.
Samuel
1
Tenga en cuenta que IE 9 implementa DOMNodeInserted pero tiene un error importante en el que no se activará cuando agregue un elemento por el momento, que es la mayoría de las veces cuando desea usarlo. Los detalles están en: help.dottoro.com/ljmcxjla.php
mikemaccana
23

Usé este enfoque para esperar a que aparezca un elemento para poder ejecutar las otras funciones después de eso.

Digamos que la doTheRestOfTheStuff(parameters)función solo debe invocarse después de que el elemento con ID the_Element_IDaparece o termina de cargar, podemos usar,

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

Tu puedes hacer

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

});

Tenga en cuenta que esto solo funcionará si el elemento está presente en el DOM cuando se lo solicite al servidor. Si el elemento se agrega dinámicamente a través de JavaScript, no funcionará y es posible que deba mirar las otras respuestas.

Splynx
fuente
77
La .ready()función funciona para casi cualquier cosa (si no cualquier cosa), no solo document. Simplemente no funcionará con elementos creados dinámicamente, incluso en .live().
Richard Neil Ilagan
77
@Bery, como señaló Richard, esto funciona solo para elementos que ya están presentes en el HTML cuando se solicita por primera vez desde el servidor. Si se usa Javascript para agregar un elemento dinámicamente al DOM, no funciona.
Chandranshu
66
@Sam, ¿puede aclarar cómo adjuntarlo a la referencia del elemento en la memoria?
Vikas Singhal
3
Esta respuesta es incorrecta. Lo que realmente está comprobando aquí es un elemento regular $(document).ready(), no el elemento que cree que se aplicará también. Así es como funciona este oyente especial . Ejemplo
Shikkediel
1
Este uso no se recomienda de acuerdo con api.jquery.com/ready
splintor
14

Creo que todavía no hay ninguna respuesta aquí con un ejemplo de trabajo fácil y legible. Use MutationObserver interface para detectar cambios DOM, como este:

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>

Nota: los documentos en español sobre MozillaMutationObserver son más detallados si desea obtener más información.

Surfista de plata
fuente
2
Considere dejar un comentario explicando la razón del voto negativo, para que pueda mejorar mi respuesta. Gracias.
SilverSurfer
12

Simplemente agregue el selector que desee. Una vez que se encuentra el elemento, puede tener acceso a él en la función de devolución de llamada.

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));
Diego Fortes
fuente
2
Sin estar de acuerdo, esta es una solución muy limpia y funciona para mí.
jstafford el
3
Esta respuesta funciona tanto en IE8-10 como en navegadores modernos. El problema principal es que seguirá ejecutándose si el elemento no existe, por lo que es mejor cuando esté seguro de que el elemento estará allí. De lo contrario, podría agregar un contador.
Por el nombre del
1
Funcionó perfectamente para mí
James Stewart
1
Funcionó como encanto !!
Aman el
1
Eran similares, no idénticos. Además, muchas personas están haciendo lo mismo. Por último, codifiqué esta solución yo mismo. Sin embargo, ese es un razonamiento pobre, si incluso fuera el caso, agradecería un comentario que me lo hiciera saber. La respuesta resuelve el problema de OP y no tiene motivos aparentes para ser rechazado.
Diego Fortes
11

Para un enfoque simple usando jQuery, he encontrado que esto funciona 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");
    });
  });

Aquí simplemente verificamos cada 500 ms para ver si el elemento está cargado, cuando lo está, podemos usarlo.

Esto es especialmente útil para agregar controladores de clics a elementos que se han agregado dinámicamente al documento.

Hedley Smith
fuente
8

¿Qué tal la biblioteca insertionQuery ?

insertionQuery utiliza devoluciones de llamada de animación CSS adjuntas a los selectores especificados para ejecutar una devolución de llamada cuando se crea un elemento. Este método permite ejecutar devoluciones de llamada cada vez que se crea un elemento, no solo la primera vez.

De github:

Manera sin evento dom para atrapar nodos que aparecen. Y usa selectores.

No es solo para un soporte de navegador más amplio, puede ser mejor que DOMMutationObserver para ciertas cosas.

¿Por qué?

  • Debido a que los eventos DOM ralentizan el navegador y insertionQuery no
  • Porque DOM Mutation Observer tiene menos soporte para el navegador que insertionQuery
  • ¡Porque con insertionQuery puede filtrar los cambios DOM usando selectores sin sobrecarga de rendimiento!

Soporte generalizado!

IE10 + y sobre todo cualquier otra cosa (incluido el móvil)

b3wii
fuente
7

Aquí hay una función que actúa como una envoltura delgada alrededor de MutationObserver. El único requisito es que el navegador sea compatible con MutationObserver; no hay dependencia en JQuery. Ejecute el fragmento a continuación para ver un ejemplo de trabajo.

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>

Ivan Karajas
fuente
6

Aquí hay una solución de devolución de promesas en Javascript vainilla (sin devoluciones de llamada desordenadas). Por defecto verifica cada 200ms.

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);
            }
        }
    });
}
desintegrador
fuente
5

Aquí hay una función Javascript pura que le permite esperar cualquier cosa. Establezca el intervalo más largo para tomar menos recursos de 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);
}

Para llamar a esto, por ejemplo en jQuery, use algo como:

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

Una solución que devuelve ay Promisepermite utilizar un tiempo de espera (IE 11+ compatible).

Para un solo elemento (tipo Elemento):

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

Para múltiples elementos (escriba 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);
    });
}

Ejemplos:

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

Funciona tanto para una lista de elementos como para un solo elemento.

Anwar
fuente
1
¡Mi solución favorita! ¿Por qué verificar element instanceof HTMLElement? ¿Puede ser algo más que nullo HTMLElement?
Leeroy
1
Planteas un punto interesante. Debería haberlo ampliado utilizando en su Elementlugar (fijo). Acabo de hacer la verificación porque quiero asegurarme de que la variable elementtenga la propiedad innerHTMLcomo dice la documentación de Element MDN . ¡Siéntase libre de eliminarlo si no le importa!
Anwar
2

Un ejemplo más limpio usando MutationObserver:

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

Esta es una solución simple para aquellos que están acostumbrados a las promesas y no quieren usar libs o temporizadores de terceros.

Lo he estado usando en mis proyectos por un tiempo

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

Para usarlo:

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

o con asíncrono / espera

const elm = await waitForElm('.some-classs')
Yong Wang
fuente
Esto es genial! Lo bueno de esto es que puedes usarlo con async/ awaittambién. También podría obtener más rendimiento al hacerlomutations.addedNodes.find(node => node.matchesSelector("..."))
mattsven
@mattsven Buen punto! Verificar solo los nodos en las mutaciones es más eficaz que hacer document.querySelector.
Yong Wang
Corrija el error de ortografía, watiForElm a waitForElm
dalvir
1

Si desea que deje de verse después de un tiempo (tiempo de espera), el siguiente jQuery funcionará. Terminará el tiempo después de 10 segundos. Necesitaba usar este código en lugar de JS puro porque necesitaba seleccionar una entrada por nombre y tenía problemas para implementar algunas de las otras soluciones.

 // 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);
S-Thomas
fuente
0

Usualmente uso este fragmento para Tag Manager:

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

si tiene cambios de dom asíncronos, esta función verifica (con límite de tiempo en segundos) para los elementos DOM, no será pesado para el DOM y su Promesa basada :)

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 });
Meni Edri
fuente