¿Cómo puedo ser notificado cuando se agrega un elemento a la página?


139

Quiero que se ejecute una función de mi elección cuando se agrega un elemento DOM a la página. Esto está en el contexto de una extensión del navegador, por lo que la página web se ejecuta independientemente de mí y no puedo modificar su fuente. ¿Cuáles son mis opciones aquí?

Supongo que, en teoría, podría usar setInterval()para buscar continuamente la presencia del elemento y realizar mi acción si el elemento está allí, pero necesito un mejor enfoque.


¿Tiene que verificar si hay un elemento en particular que otro script suyo puso en la página o cualquier elemento que se agregue sin importar la fuente?
Jose Faeti

1
¿sabe qué función agrega el elemento en el código de otra persona? De ser así, puede sobrescribirlo y agregar una línea adicional que active un evento personalizado.
jeffreydev

Respuestas:


86

¡Advertencia!

Esta respuesta ahora está desactualizada. DOM Level 4 introdujo MutationObserver , que proporciona un reemplazo efectivo para los eventos de mutación en desuso. Vea esta respuesta para una mejor solución que la presentada aquí. Seriamente. No sondee el DOM cada 100 milisegundos; desperdiciará energía de la CPU y tus usuarios te odiarán.

Dado que los eventos de mutación quedaron en desuso en 2012 y no tiene control sobre los elementos insertados porque los agrega el código de otra persona, su única opción es verificarlos continuamente.

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

Una vez que se llama a esta función, se ejecutará cada 100 milisegundos, que es 1/10 (una décima) de segundo. A menos que necesite observación de elementos en tiempo real, debería ser suficiente.


1
jose, ¿cómo terminas tu tiempo de espera establecido cuando la condición se cumple realmente? es decir, ¿encontraste el elemento que finalmente se cargó x segundos más tarde en tu página?
klewis

1
@blachawk necesita asignar el valor de retorno setTimeout a una variable, que luego puede pasar como un paratemer a clearTimeout () para borrarlo.
Jose Faeti

3
@blackhawk: acabo de ver esa respuesta y hacer +1 en ella; pero te darás cuenta de que en realidad no necesitas usar clearTimeout () aquí, ¡ya que setTimeout () solo se ejecuta una vez! Un 'if-else' es suficiente: no permita que la ejecución pase en setTimeout () una vez que haya encontrado el elemento insertado.
1111161171159459134

Una guía práctica útil para usar MutationObserver(): davidwalsh.name/mutationobserver-api
gfullam

44
Eso esta sucio ¿Realmente no hay un "equivalente" a los AS3 Event.ADDED_TO_STAGE?
Bitterblue


19

Entre la degradación de los eventos de mutación. y la aparición de MutationObserver, una manera eficiente de ser notificado cuando se agrega un elemento específico al DOM es explotar los eventos de animación CSS3 .

Para citar la publicación del blog:

Configure una secuencia de fotogramas clave CSS que apunte (mediante su elección de selector CSS) a cualquier elemento DOM para el que desee recibir un evento de inserción de nodo DOM. Utilicé una propiedad css relativamente benigna y poco utilizada, clip , utilicé el color del contorno en un intento de evitar jugar con los estilos de página deseados: el código una vez apuntó a la propiedad clip, pero ya no es animable en IE a partir de la versión 11. Eso Dicho esto, cualquier propiedad que pueda ser animada funcionará, elija la que más le guste.

A continuación, agregué un oyente de animación de inicio de todo el documento que uso como delegado para procesar las inserciones de nodos. El evento de animación tiene una propiedad llamada animationName que le indica qué secuencia de fotogramas clave inició la animación. Solo asegúrese de que la propiedad animationName sea la misma que el nombre de la secuencia de fotogramas clave que agregó para las inserciones de nodos y listo.


Esta es la solución de más alto rendimiento, y hay una respuesta en una pregunta duplicada que menciona una biblioteca para esto.
Dan Dascalescu

1
Respuesta infravalorada.
Gökhan Kurt

Cuidado. Si la precisión de milisegundos es importante, este enfoque está lejos de ser preciso. Este jsbin demuestra que hay más de 30 ms diferencia entre una devolución de llamada en línea y el uso animationstart, jsbin.com/netuquralu/1/edit .
Gajus

Estoy de acuerdo con Kurt, esto está subestimado.
Zabbu

13

Puede usar el livequerycomplemento para jQuery. Puede proporcionar una expresión de selector como:

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

Cada vez que removeItemButtonse agrega un botón de una clase, aparece un mensaje en una barra de estado.

En términos de eficiencia, es posible que desee evitar esto, pero en cualquier caso, puede aprovechar el complemento en lugar de crear sus propios controladores de eventos.

Respuesta revisada

La respuesta anterior solo pretendía detectar que un elemento se ha agregado al DOM a través del complemento.

Sin embargo, lo más probable es que un jQuery.on()enfoque sea más apropiado, por ejemplo:

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

Si tiene contenido dinámico que debería responder a los clics, por ejemplo, es mejor vincular eventos a un contenedor principal mediante jQuery.on.


11

ETA 24 abr 17 Quería simplificar esto un poco con algunos async/await magia, ya que lo hace mucho más sucinto:

Usando el mismo prometido-observable:

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

Su función de llamada puede ser tan simple como:

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

Si desea agregar un tiempo de espera, puede usar un Promise.racepatrón simple como se muestra aquí :

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

Puede hacer esto sin bibliotecas, pero tendría que usar algunas cosas de ES6, así que tenga en cuenta los problemas de compatibilidad (es decir, si su audiencia es principalmente amish, ludita o, peor aún, usuarios de IE8)

Primero, usaremos la API MutationObserver para construir un objeto observador. Envolveremos este objeto en una promesa, yresolve() cuando se active la devolución de llamada (h / t davidwalshblog) artículo del blog de david walsh sobre mutaciones :

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

Entonces, crearemos un generator function. Si aún no los ha usado, se está perdiendo, pero una breve sinopsis es: se ejecuta como una función de sincronización, y cuando encuentra unyield <Promise> expresión, espera sin bloqueos la promesa de ser cumplido (los generadores hacen más que esto, pero esto es lo que nos interesa aquí ).

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

Una parte difícil de los generadores es que no "regresan" como una función normal. Entonces, usaremos una función auxiliar para poder usar el generador como una función regular. (de nuevo, h / t a 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 );
            }
        }
    })();
}

Luego, en cualquier momento antes de que ocurra la mutación DOM esperada, simplemente ejecute runGenerator(getMutation).

Ahora puede integrar mutaciones DOM en un flujo de control de estilo síncrono. ¿Qué tal eso?



3

Echa un vistazo a este complemento que hace exactamente eso - jquery.initialize

Funciona exactamente como cada función, la diferencia es que toma el selector que ha ingresado y busca nuevos elementos agregados en el futuro que coincidan con este selector y los inicialice

Inicializar se ve así

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

Pero ahora si el nuevo elemento coincide .some-element aparece un selector de en la página, se inicializará instantáneamente.

La forma en que se agrega un nuevo elemento no es importante, no necesita preocuparse por ninguna devolución de llamada, etc.

Entonces, si agrega un nuevo elemento como:

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

Se inicializará instantáneamente.

El complemento se basa en MutationObserver


0

Una solución javascript pura (sin 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'));
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.