Como esperar até que um elemento exista?

237

Estou trabalhando em uma extensão no Chrome e me pergunto: qual é a melhor maneira de descobrir quando um elemento surge? Usando javascript simples, com um intervalo que verifica até a existência de um elemento, ou o jQuery tem alguma maneira fácil de fazer isso?

Mattsven
fonte
1
Parece que todas as opções aqui hoje (incluindo comentários) estão desatualizadas ou incompletas. Eles não consideram totalmente a incrível entrada de @ hughsk, o argumento de compatibilidade. Enquanto isso, eu recomendaria simplesmente usar a atualização de Brandon na resposta de Ryan para simplificar geral e menos risco de sobrecarga, suponho.
Cregox # 01/15
4
MutationObserver> DOM Mutation Events> setTimeout.
mattsven
2
Não de onde eu estou. setTimeouté compatível, simples de implementar, simples de manter e possui sobrecarga insignificante.
Cregox # 02/15
setTimeout+ jQueryé menos do que ideal na minha opinião, por duas razões: 1.) jQuery inchar 2.) você está desnecessariamente consultando manualmente o DOM quanto a elementos, eventos superam essa velocidade com facilidade, 3.) sempre será mais lento que qualquer nativo implementação. Se você precisar fazer algo com base na presença de um elemento razoavelmente rápido, especialmente se a experiência perfeita do usuário for sua meta, ela será inferior.
mattsven
3
Existem 3 tipos de pessoas: quem pode contar e quem não pode. ; P
cregox 03/04

Respostas:

149

DOMNodeInsertedestá sendo preterido, junto com os outros eventos de mutação do DOM, devido a problemas de desempenho - a abordagem recomendada é usar um MutationObserver para monitorar o DOM. No entanto, ele é suportado apenas em navegadores mais novos, portanto, você deve voltar DOMNodeInsertedquando MutationObservernão estiver disponível.

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
fonte
50
Eu sempre achei a API do MutationObserver um pouco complexa, então criei uma biblioteca, arrival.js , para fornecer uma API mais simples para ouvir a criação / remoção de elementos.
Uzair Farooq
15
Eu recomendo usar a excelente biblioteca do @UzairFarooq github.com/uzairfarooq/arrive
Dennis
3
Duas coisas a serem observadas: (1) Seria melhor fazer, if (mutation.addedNodes.length)pois if (mutation.addedNodes)ainda retornaria verdadeiro, mesmo que seja uma matriz vazia. (2) Você não pode fazer isso mutation.addedNodes.forEach()porque addedNodes é um nodeList e não pode iterar por meio de um nodeList com forEach. Para uma solução para isso, consulte toddmotto.com/ditch-the-array-foreach-call-nodelist-hack
thdoan
3
Você pode dar um exemplo de como alguém usaria isso? Não sei onde colocar meu seletor jquery ou código que quero que seja executado quando o elemento DOM existir.
Superdooperhero
1
@Superdooperhero Eu respondi com um exemplo fácil. Verifique-o. stackoverflow.com/a/57395241/6542186
SilverSurfer
113

Eu estava tendo o mesmo problema, então fui em frente e escrevi um plugin para ele.

$(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
fonte
5
Obrigado pelo plugin. Bifurquei e melhorei um pouco. Sinta-se livre para pegar o que quiser da minha atualização. Eu tenho mais algumas melhorias planejado, ainda: atualizados plug-in
Brandon Belvin
8
Seria bom sem dep jquery também ...;)
knutole
4
talvez você deva mencionar como ele funciona: ele pergunta perguntando a cada 500 ms se o elemento existe (usando a window.setInterval). Eu não sei se a MutationObserverresposta funciona pesquisando também ... #
sports
2
Não funciona corretamente se o elemento já estiver na página. Aqui é a versão correta desta função: gist.github.com/PizzaBrandon/5709010
Roland Soós
2
Você pode explicar o que é o uso ;no início da função ( ;(function ($, window) {)?
Mrid 23/12/16
76

Aqui está uma função JavaScript principal para aguardar a exibição de um elemento.

Parâmetros:

  1. selector: Esta função procura o elemento $ {selector}
  2. time: Esta função verifica se esse elemento existe a cada $ {time} milissegundos.

    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 exemplo, defina selector="#div1"e time=5000procure a tag HTML cuja id="div1"cada 5000 milissegundos.

Etienne Tonnelier
fonte
Agradável! Você pode escrever isso para que qualquer seletor possa ser aceito?
mattsven
Eu duvido que eu posso fazer isso .. Mas por favor dê uma olhada neste post para obter o getElementByXpath: stackoverflow.com/questions/10596417/...
Etienne Tonnelier
1
Você pode escrever para usar o observador de mutações?
SuperUberDuper
ou você poderia reescrever este para usar uma promessa?
SuperUberDuper
25

Você pode ouvir DOMNodeInsertedou DOMSubtreeModifiedeventos que o fogo sempre que um novo elemento é adicionado ao DOM.

Há também o plug- in LiveQuery jQuery que detectaria quando um novo elemento fosse criado:

$("#future_element").livequery(function(){
    //element created
});
serg
fonte
1
Muito bom plugin! Existe alguma função como essa no jquery diretamente? Eu estou querendo saber que não há recurso existente para fazer isso. E se este é o plugin, vote nesta resposta;) Para mim, ele funciona perfeitamente. Muito obrigado.
Samuel
1
Nota O IE 9 implementa o DOMNodeInserted, mas possui um erro grave, que não é acionado quando você adiciona um elemento durante o tempo, que é a maior parte do tempo em que você deseja usá-lo. Detalhes em: help.dottoro.com/ljmcxjla.php
mikemaccana
23

Eu usei essa abordagem para aguardar a exibição de um elemento para que eu possa executar as outras funções depois disso.

Digamos que a doTheRestOfTheStuff(parameters)função só deve ser chamada depois que o elemento com ID the_Element_IDaparecer ou o carregamento terminar, podemos usar,

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

Você pode fazer

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

});

Observe que isso só funcionará se o elemento estiver presente no DOM ao ser solicitado pelo servidor. Se o elemento estiver sendo adicionado dinamicamente via JavaScript, ele não funcionará e você pode precisar procurar as outras respostas.

Splynx
fonte
7
A .ready()função funciona para quase qualquer coisa (se não alguma coisa), não apenas document. Ele simplesmente não funciona com elementos criados dinamicamente, mesmo .live().
Richard Neil Ilagan
7
@Bery, como Richard apontou, isso funciona apenas para elementos que já estão presentes no HTML quando ele é solicitado pela primeira vez no servidor. Se o Javascript for usado para adicionar um elemento dinamicamente ao DOM, ele não funcionará.
precisa saber é o seguinte
6
@ Sam, você pode esclarecer como anexá-lo à referência do elemento na memória?
Vikas Singhal
3
Esta resposta está incorreta. O que você está verificando aqui é regular $(document).ready(), e não o elemento que você acha que será aplicado também. É assim que esse ouvinte especial funciona. Exemplo
Shikkediel 02/02
1
Este uso não é recomendado de acordo com api.jquery.com/ready
splintor
14

Acho que ainda não há resposta aqui com um exemplo de trabalho fácil e legível. Use MutationObserver interface para detectar alterações no DOM, assim:

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: os documentos sobre o Mozilla em espanholMutationObserver são mais detalhados se você quiser obter mais informações.

Surfista Prateado
fonte
2
Considere deixar um comentário explicando o motivo do voto negativo, para que eu possa melhorar minha resposta. Obrigado.
SilverSurfer
12

Basta adicionar o seletor desejado. Depois que o elemento é encontrado, você pode acessar a função de retorno de chamada.

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
fonte
2
PossuirConcordo que esta é uma solução muito limpa e funciona para mim.
Jstafford #
3
Esta resposta funciona no IE8-10 e nos navegadores modernos. O principal problema é que ele continuará funcionando se o elemento não existir - portanto, é melhor quando você tem certeza de que o elemento estará lá. Caso contrário, você pode adicionar um contador.
Para o nome
1
Funcionou perfeitamente para mim
James Stewart
1
Trabalhou como charme !!
Aman
1
Eles eram semelhantes, não idênticos. Além disso, muitas pessoas estão fazendo o mesmo. Por fim, eu mesmo codifiquei essa solução. Esse é um raciocínio ruim, no entanto, se fosse esse o caso, eu apreciaria um comentário me avisando. A resposta resolve o problema do OP e não tem motivos aparentes para serem rebaixados.
Diego Fortes
11

Para uma abordagem simples usando jQuery, achei que funcionou bem:

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

Aqui, simplesmente verificamos a cada 500ms para ver se o elemento está carregado, quando está, podemos usá-lo.

Isso é especialmente útil para adicionar manipuladores de clique a elementos que foram adicionados dinamicamente ao documento.

Hedley Smith
fonte
8

E a biblioteca insertionQuery ?

insertionQuery usa retornos de chamada de Animação CSS anexados ao (s) seletor (es) especificado (s) para executar um retorno de chamada quando um elemento é criado. Esse método permite que os retornos de chamada sejam executados sempre que um elemento é criado, não apenas na primeira vez.

No github:

Maneira não-dom-event para capturar nós aparecendo. E usa seletores.

Não é apenas para um suporte mais amplo ao navegador; pode ser melhor que o DOMMutationObserver para certas coisas.

Por quê?

  • Como os eventos do DOM desaceleram o navegador e o inserttionQuery não
  • Como o DOM Mutation Observer tem menos suporte ao navegador que o inserttionQuery
  • Porque com o inserttionQuery você pode filtrar as alterações do DOM usando seletores sem sobrecarga de desempenho!

Suporte generalizado!

IE10 + e principalmente qualquer outra coisa (incluindo dispositivos móveis)

b3wii
fonte
7

Aqui está uma função que atua como um invólucro fino em torno do MutationObserver. O único requisito é que o navegador suporte MutationObserver; não há dependência no JQuery. Execute o snippet abaixo para ver um exemplo de trabalho.

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
fonte
6

Aqui está uma solução que promete retorno em Javascript baunilha (sem retornos de chamada confusos). Por padrão, ele verifica a 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);
            }
        }
    });
}
blaster
fonte
5

Aqui está uma função Javascript pura que permite aguardar qualquer coisa. Defina o intervalo mais longo para consumir menos recursos da 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 chamar isso, por exemplo, no jQuery, use algo como:

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

Uma solução que retorna Promisee permite usar um tempo limite (IE 11+ compatível).

Para um único elemento (digite 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 vários elementos (tipo 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);
    });
}

Exemplos:

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 para uma lista de elementos e um único elemento.

Anwar
fonte
1
Minha solução favorita! Por que verificar element instanceof HTMLElement? Pode alguma vez ser outra coisa senão nullou HTMLElement?
Leeroy
1
Você levanta um ponto interessante. Eu deveria torná-lo mais amplo usando Element(fixo). Acabei de fazer a verificação porque quero ter certeza de que a variável elementtem a propriedade innerHTMLconforme a documentação do Element MDN . Sinta-se livre para removê-lo, se você não se importa!
Anwar
2

Um exemplo mais limpo usando MutationObserver:

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

Esta é uma solução simples para aqueles que estão acostumados a promessas e não querem usar libs ou timers de terceiros.

Eu tenho usado em meus projetos há um tempo

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 usá-lo:

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

ou com assíncrono / espera

const elm = await waitForElm('.some-classs')
Yong Wang
fonte
Isso é legal! A parte legal é que você pode usá-lo com async/ awaittambém. Você também pode obter mais desempenho com issomutations.addedNodes.find(node => node.matchesSelector("..."))
mattsven 29/04
@mattsven Bom ponto! Verificar apenas os nós nas mutações é mais eficiente do que document.querySelector.
Yong Wang
Corrija o erro de ortografia, watiForElm to waitForElm
dalvir
1

Se você deseja que ele pare de cuidar de um tempo (tempo limite), o jQuery a seguir funcionará. O tempo limite será excedido após 10 segundos. Eu precisava usar esse código em vez de JS puro, porque precisava selecionar uma entrada por nome e estava tendo problemas para implementar algumas das outras soluções.

 // 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
fonte
0

Eu costumo usar esse snippet para o Gerenciador de tags:

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

se você tiver alterações assíncronas do dom, essa função verificará (com limite de tempo em segundos) os elementos DOM, não será pesado para o DOM e seus baseados em Promessa :)

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
fonte