Comment puis-je être averti lorsqu'un élément est ajouté à la page?

139

Je veux qu'une fonction de mon choix s'exécute lorsqu'un élément DOM est ajouté à la page. C'est dans le contexte d'une extension de navigateur, donc la page Web fonctionne indépendamment de moi et je ne peux pas modifier sa source. Quelles sont mes options ici?

Je suppose qu'en théorie, je pourrais simplement utiliser setInterval()pour rechercher continuellement la présence de l'élément et effectuer mon action si l'élément est là, mais j'ai besoin d'une meilleure approche.

Kevin Burke
la source
Devez-vous vérifier pour un élément particulier un autre script mis dans la page ou pour tout élément ajouté quelle que soit la source?
Jose Faeti
1
savez-vous quelle fonction ajoute l'élément dans le code de quelqu'un d'autre, si c'est le cas, vous pouvez l'écraser et ajouter une ligne supplémentaire déclenchant un événement personnalisé?
jeffreydev
1
duplication possible de Y a
apsillers

Réponses:

86

Avertissement!

Cette réponse est désormais dépassée. DOM Level 4 a introduit MutationObserver , fournissant un remplacement efficace pour les événements de mutation obsolètes. Voir cette réponse pour une meilleure solution que celle présentée ici. Sérieusement. N'interrogez pas le DOM toutes les 100 millisecondes; cela gaspillera la puissance du processeur et vos utilisateurs vous détesteront.

Étant donné que les événements de mutation étaient obsolètes en 2012 et que vous n'avez aucun contrôle sur les éléments insérés car ils sont ajoutés par le code de quelqu'un d'autre, votre seule option est de les vérifier en permanence.

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

Une fois cette fonction appelée, elle s'exécutera toutes les 100 millisecondes, soit 1/10 (un dixième) de seconde. À moins que vous n'ayez besoin d'une observation des éléments en temps réel, cela devrait suffire.

José Faeti
la source
1
jose, comment terminer ton règlement lorsque la condition est réellement remplie? c'est-à-dire que vous avez trouvé l'élément qui s'est finalement chargé x secondes plus tard sur votre page?
klewis
1
@blachawk vous devez attribuer la valeur de retour setTimeout à une variable, que vous pouvez ensuite passer en tant que paratemer à clearTimeout () pour l'effacer.
Jose Faeti
3
@blackhawk: Je viens de voir cette réponse et +1; mais sachez que vous n'avez en fait pas besoin d'utiliser clearTimeout () ici, puisque setTimeout () ne s'exécute qu'une seule fois! Un «if-else» suffit: ne laissez pas l'exécution passer sur setTimeout () une fois que vous avez trouvé l'élément inséré.
1111161171159459134
Un guide pratique utile pour utiliser MutationObserver(): davidwalsh.name/mutationobserver-api
gfullam
4
C'est sale. N'y a-t-il pas vraiment d '«équivalent» aux AS3 Event.ADDED_TO_STAGE?
Bitterblue
19

Entre la dépréciation des événements de mutation et l'émergence de MutationObserver, un moyen efficace d'être notifié lorsqu'un élément spécifique était ajouté au DOM était d' exploiter les événements d'animation CSS3 .

Pour citer l'article du blog:

Configurez une séquence d'images clés CSS qui cible (via votre choix de sélecteur CSS) les éléments DOM pour lesquels vous souhaitez recevoir un événement d'insertion de nœud DOM. J'ai utilisé une propriété css relativement bénigne et peu utilisée, clip J'ai utilisé la couleur de contour pour éviter de jouer avec les styles de page prévus - le code ciblait autrefois la propriété clip, mais il n'est plus animable dans IE à partir de la version 11. Cela dit, toute propriété qui peut être animée fonctionnera, choisissez celle que vous aimez.

Ensuite, j'ai ajouté un écouteur animationstart à l'échelle du document que j'utilise en tant que délégué pour traiter les insertions de nœuds. L'événement d'animation comporte une propriété appelée animationName qui vous indique quelle séquence d'images clés a lancé l'animation. Assurez-vous simplement que la propriété animationName est la même que le nom de la séquence d'images clés que vous avez ajouté pour les insertions de nœuds et vous êtes prêt à partir.

jokeyrhyme
la source
C'est la solution la plus performante, et il y a une réponse dans une question en double mentionnant une bibliothèque pour cela.
Dan Dascalescu
1
Réponse sous-estimée.
Gökhan Kurt
Prudent. Si la précision à la milliseconde est importante, cette approche est loin d'être précise. Cette jsbin démontre qu'il ya plus de 30ms différence entre un rappel en ligne et en utilisant animationstart, jsbin.com/netuquralu/1/edit .
Gajus
Je suis d'accord avec Kurt, c'est sous-estimé.
Zabbu
13

Vous pouvez utiliser le livequeryplugin pour jQuery. Vous pouvez fournir une expression de sélecteur telle que:

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

Chaque fois qu'un bouton d'une removeItemButtonclasse est ajouté, un message apparaît dans une barre d'état.

En termes d'efficacité, vous voudrez peut-être éviter cela, mais dans tous les cas, vous pouvez utiliser le plugin au lieu de créer vos propres gestionnaires d'événements.

Réponse revisitée

La réponse ci-dessus était uniquement destinée à détecter qu'un élément a été ajouté au DOM via le plugin.

Cependant, très probablement, un jQuery.on() approche serait plus appropriée, par exemple:

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

Si vous disposez d'un contenu dynamique qui doit répondre aux clics, par exemple, il est préférable de lier les événements à un conteneur parent à l'aide de jQuery.on.

Zéro distraction
la source
11

ETA 24 avril 17 Je voulais simplifier un peu cela avec certains async/await magic, car cela le rend beaucoup plus succinct:

En utilisant le même observable promisifié:

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

Votre fonction d'appel peut être aussi simple que:

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 vous souhaitez ajouter un délai d'attente, vous pouvez utiliser un Promise.racemodèle simple comme illustré ici :

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

Vous pouvez le faire sans bibliothèques, mais vous devrez utiliser des éléments ES6, alors soyez conscient des problèmes de compatibilité (par exemple, si votre public est principalement des utilisateurs Amish, Luddite ou, pire, IE8)

Tout d'abord, nous utiliserons l' API MutationObserver pour construire un objet observateur. Nous allons envelopper cet objet dans une promesse, et resolve()quand le rappel est déclenché (h / t davidwalshblog) article du blog de David Walsh sur les mutations :

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

Ensuite, nous allons créer un fichier generator function. Si vous ne les avez pas encore utilisés, alors vous passez à côté - mais un bref résumé est: il fonctionne comme une fonction de synchronisation, et quand il trouve une yield <Promise>expression, il attend de manière non bloquante que la promesse soit rempli (les générateurs font plus que cela, mais c'est ce qui nous intéresse ici ).

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

Une partie délicate des générateurs est qu'ils ne «retournent» pas comme une fonction normale. Nous allons donc utiliser une fonction d'assistance pour pouvoir utiliser le générateur comme une fonction normale. (encore une fois, h / t à 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 );
            }
        }
    })();
}

Ensuite, à tout moment avant que la mutation DOM attendue ne se produise, exécutez simplement runGenerator(getMutation).

Vous pouvez désormais intégrer des mutations DOM dans un flux de contrôle de style synchrone. Que diriez-vous de ça.

Brandon
la source
8

Il existe une bibliothèque javascript prometteuse appelée Arrive qui semble être un excellent moyen de commencer à tirer parti des observateurs de mutation une fois que la prise en charge du navigateur devient courante.

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

Dave Kiss
la source
3

Découvrez ce plugin qui fait exactement cela - jquery.initialize

Cela fonctionne exactement comme la fonction .each, la différence est que cela prend le sélecteur que vous avez entré et surveille les nouveaux éléments ajoutés à l'avenir correspondant à ce sélecteur et les initialise

Initialiser ressemble à ceci

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

Mais maintenant, si un nouvel élément correspondant au .some-elementsélecteur apparaîtra sur la page, il sera instantanément initialisé.

La façon dont le nouvel élément est ajouté n'est pas importante, vous n'avez pas besoin de vous soucier des rappels, etc.

Donc, si vous ajoutez un nouvel élément comme:

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

il sera instantanément initialisé.

Le plugin est basé sur MutationObserver

tarte6k
la source
0

Une solution javascript pure (sans 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'));
védant
la source