Como posso ser notificado quando um elemento é adicionado à página?

139

Quero que uma função da minha escolha seja executada quando um elemento DOM for adicionado à página. Isso ocorre no contexto de uma extensão do navegador; portanto, a página da web é executada independentemente de mim e não posso modificar sua fonte. Quais são minhas opções aqui?

Eu acho que, em teoria, eu poderia usar apenas setInterval()para procurar continuamente a presença do elemento e executar minha ação se o elemento estiver lá, mas preciso de uma abordagem melhor.

Kevin Burke
fonte
Você precisa verificar se há um elemento em particular que outro script seu colocou na página ou se há algum elemento adicionado independentemente da fonte?
Jose Faeti 15/09/11
1
você sabe qual função adiciona o elemento no código de outra pessoa; caso contrário, você pode substituí-lo e adicionar uma linha extra acionando um evento personalizado?
jeffreydev
1
possível duplicata de Existe um ouvinte de alteração do jQuery DOM?
Apsillers 13/08/2012

Respostas:

86

Aviso!

Esta resposta está desatualizada. O DOM Nível 4 introduziu o MutationObserver , fornecendo uma substituição eficaz para os eventos de mutação preteridos. Veja esta resposta para uma solução melhor do que a apresentada aqui. Seriamente. Não faça pesquisas no DOM a cada 100 milissegundos; isso desperdiçará energia da CPU e seus usuários o odiarão.

Como os eventos de mutação foram preteridos em 2012 e você não tem controle sobre os elementos inseridos porque eles são adicionados pelo código de outra pessoa, sua única opção é verificar continuamente por eles.

function checkDOMChange()
{
    // check for any new element being inserted here,
    // or a particular node being modified

    // call the function again after 100 milliseconds
    setTimeout( checkDOMChange, 100 );
}

Depois que essa função é chamada, ela será executada a cada 100 milissegundos, ou seja 1/10 (um décimo) de segundo. A menos que você precise de observação de elementos em tempo real, isso deve ser suficiente.

Jose Faeti
fonte
1
José, como você encerra seu tempo limite quando a condição é realmente atendida? ou seja, você encontrou o elemento que finalmente carregou x segundos depois na sua página?
klewis
1
@blachawk, você precisa atribuir o valor de retorno setTimeout a uma variável, que pode ser transmitida posteriormente como paratemer para clearTimeout () para limpá-lo.
Jose Faeti
3
@blackhawk: Acabei de ver essa resposta e marcar com +1; mas você deve estar ciente de que não precisa usar clearTimeout () aqui, pois setTimeout () é executado apenas uma vez! Um 'if-else' é apenas suficiente: não deixe a execução passar em setTimeout () depois de encontrar o elemento inserido.
1111161171159459134
Um guia prático útil para usando MutationObserver(): davidwalsh.name/mutationobserver-api
gfullam
4
Isso é sujo. Não existe realmente "equivalente" ao AS3 Event.ADDED_TO_STAGE?
Bitterblue
19

Entre a descontinuação de eventos de mutação e o surgimento de MutationObserver, uma maneira eficaz de ser notificado quando um elemento específico foi adicionado ao DOM era explorar os eventos de animação CSS3 .

Para citar a postagem do blog:

Configure uma sequência de quadro-chave CSS que tenha como alvo (por meio de seu seletor CSS) os elementos DOM para os quais você deseja receber um evento de inserção de nó DOM. Usei uma propriedade css relativamente benigna e pouco usada, usei cor de contorno na tentativa de evitar mexer com os estilos de página pretendidos - o código uma vez direcionava a propriedade clip, mas não é mais animador no IE a partir da versão 11. Isso dito que, qualquer propriedade que possa ser animada funcionará, escolha a que você preferir.

Em seguida, adicionei um ouvinte de início de animação em todo o documento que eu uso como delegado para processar as inserções de nó. O evento de animação possui uma propriedade chamada animationName, que informa qual sequência de quadros-chave iniciou a animação. Apenas certifique-se de que a propriedade animationName seja igual ao nome da sequência de quadros-chave que você adicionou para inserções de nó e você estará pronto.

jokeyrhyme
fonte
Esta é a solução de maior desempenho e há uma resposta em uma pergunta duplicada mencionando uma biblioteca para isso.
Dan Dascalescu
1
Resposta subestimada.
Gökhan Kurt
Cuidado. Se a precisão de milissegundos é importante, essa abordagem está longe de ser precisa. Este jsbin demonstra que há mais de 30 ms de diferença entre um retorno de chamada em linha e o uso animationstart, jsbin.com/netuquralu/1/edit .
Gajus
Eu concordo com Kurt, isso é subestimado.
Zabbu 11/09/18
13

Você pode usar o livequeryplugin para jQuery. Você pode fornecer uma expressão seletora como:

$("input[type=button].removeItemButton").livequery(function () {
    $("#statusBar").text('You may now remove items.');
});

Toda vez que um botão de uma removeItemButtonclasse é adicionado, uma mensagem é exibida na barra de status.

Em termos de eficiência, convém evitar isso, mas, em qualquer caso, você pode aproveitar o plug-in em vez de criar seus próprios manipuladores de eventos.

Resposta revisitada

A resposta acima foi criada apenas para detectar que um item foi adicionado ao DOM por meio do plug-in.

No entanto, provavelmente, uma jQuery.on()abordagem seria mais apropriada, por exemplo:

$("#myParentContainer").on('click', '.removeItemButton', function(){
          alert($(this).text() + ' has been removed');
});

Se você tiver um conteúdo dinâmico que responda a cliques, por exemplo, é melhor vincular eventos a um contêiner pai usando jQuery.on.

Zero Distração
fonte
11

ETA 24 Abr 17 Queria simplificar um pouco isso com alguma async/ awaitmagia, pois isso torna muito mais sucinto:

Usando o mesmo observável prometido:

const startObservable = (domNode) => {
  var targetNode = domNode;

  var observerConfig = {
    attributes: true,
    childList: true,
    characterData: true
  };

  return new Promise((resolve) => {
      var observer = new MutationObserver(function (mutations) {
         // For the sake of...observation...let's output the mutation to console to see how this all works
         mutations.forEach(function (mutation) {
             console.log(mutation.type);
         });
         resolve(mutations)
     });
     observer.observe(targetNode, observerConfig);
   })
} 

Sua função de chamada pode ser tão simples quanto:

const waitForMutation = async () => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {
      const results = await startObservable(someDomNode)
      return results
    } catch (err) { 
      console.error(err)
    }
}

Se você deseja adicionar um tempo limite, pode usar um Promise.racepadrão simples, conforme demonstrado aqui :

const waitForMutation = async (timeout = 5000 /*in ms*/) => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {

      const results = await Promise.race([
          startObservable(someDomNode),
          // this will throw after the timeout, skipping 
          // the return & going to the catch block
          new Promise((resolve, reject) => setTimeout(
             reject, 
             timeout, 
             new Error('timed out waiting for mutation')
          )
       ])
      return results
    } catch (err) { 
      console.error(err)
    }
}

Original

Você pode fazer isso sem bibliotecas, mas precisaria usar algumas coisas do ES6, portanto, esteja ciente dos problemas de compatibilidade (por exemplo, se o seu público for majoritariamente Amish, luddito ou, pior, usuários do IE8)

Primeiro, usaremos a API MutationObserver para construir um objeto observador. Embrulharemos esse objeto em uma promessa e, resolve()quando o retorno de chamada for acionado (h / t davidwalshblog), artigo da david walsh blog sobre mutações :

const startObservable = (domNode) => {
    var targetNode = domNode;

    var observerConfig = {
        attributes: true,
        childList: true,
        characterData: true
    };

    return new Promise((resolve) => {
        var observer = new MutationObserver(function (mutations) {
            // For the sake of...observation...let's output the mutation to console to see how this all works
            mutations.forEach(function (mutation) {
                console.log(mutation.type);
            });
            resolve(mutations)
        });
        observer.observe(targetNode, observerConfig);
    })
} 

Então, criaremos um generator function. Se você ainda não os usou, está perdendo - mas uma breve sinopse é: ela funciona como uma função de sincronização e, quando encontra uma yield <Promise>expressão, aguarda de maneira ininterrupta a promessa de ser cumprida. cumpridos (os geradores fazem mais do que isso, mas é nisso que estamos interessados ​​aqui ).

// we'll declare our DOM node here, too
let targ = document.querySelector('#domNodeToWatch')

function* getMutation() {
    console.log("Starting")
    var mutations = yield startObservable(targ)
    console.log("done")
}

Uma parte complicada dos geradores é que eles não 'retornam' como uma função normal. Portanto, usaremos uma função auxiliar para poder usar o gerador como uma função regular. (novamente, h / t para dwb )

function runGenerator(g) {
    var it = g(), ret;

    // asynchronously iterate over generator
    (function iterate(val){
        ret = it.next( val );

        if (!ret.done) {
            // poor man's "is it a promise?" test
            if ("then" in ret.value) {
                // wait on the promise
                ret.value.then( iterate );
            }
            // immediate value: just send right back in
            else {
                // avoid synchronous recursion
                setTimeout( function(){
                    iterate( ret.value );
                }, 0 );
            }
        }
    })();
}

Então, a qualquer momento antes que a mutação DOM esperada possa ocorrer, basta executar runGenerator(getMutation).

Agora você pode integrar mutações DOM em um fluxo de controle de estilo síncrono. Que tal isso.

Brandon
fonte
8

Existe uma biblioteca javascript promissora chamada Arrive, que parece uma ótima maneira de começar a tirar proveito dos observadores de mutações quando o suporte ao navegador se torna comum.

https://github.com/uzairfarooq/arrive/

Dave Kiss
fonte
3

Confira este plugin que faz exatamente isso - jquery.initialize

Funciona exatamente como a função .each, a diferença é que leva o seletor que você inseriu e observa novos itens adicionados no futuro que correspondem a esse seletor e os inicializa.

Inicializar é assim

$(".some-element").initialize( function(){
    $(this).css("color", "blue");
});

Mas agora, se o novo .some-elementseletor de correspondência de elemento aparecer na página, ele será inicializado instantaneamente.

A maneira como o novo item é adicionado não é importante, você não precisa se preocupar com nenhum retorno de chamada, etc.

Portanto, se você adicionar um novo elemento como:

$("<div/>").addClass('some-element').appendTo("body"); //new element will have blue color!

será inicializado instantaneamente.

O plug-in é baseado em MutationObserver

pie6k
fonte
0

Uma solução javascript pura (sem jQuery):

const SEARCH_DELAY = 100; // in ms

// it may run indefinitely. TODO: make it cancellable, using Promise's `reject`
function waitForElementToBeAdded(cssSelector) {
  return new Promise((resolve) => {
    const interval = setInterval(() => {
      if (element = document.querySelector(cssSelector)) {
        clearInterval(interval);
        resolve(element);
      }
    }, SEARCH_DELAY);
  });
}

console.log(await waitForElementToBeAdded('#main'));
vedante
fonte