Jak mogę otrzymać powiadomienie, gdy element zostanie dodany do strony?

139

Chcę, aby wybrana przeze mnie funkcja działała po dodaniu elementu DOM do strony. Dzieje się to w kontekście rozszerzenia przeglądarki, więc strona działa niezależnie ode mnie i nie mogę modyfikować jej źródła. Jakie mam tutaj opcje?

Myślę, że teoretycznie mógłbym po prostu używać setInterval()do ciągłego poszukiwania obecności elementu i wykonywania akcji, jeśli element tam jest, ale potrzebuję lepszego podejścia.

Kevin Burke
źródło
Czy musisz sprawdzić konkretny element, który inny twój skrypt umieścił na stronie lub jakikolwiek element, który jest dodawany bez względu na źródło?
Jose Faeti
1
czy wiesz, która funkcja dodaje element w czyimś kodzie, jeśli tak, możesz go nadpisać i dodać jedną dodatkową linię wyzwalającą zdarzenie niestandardowe?
jeffreydev
1
możliwy duplikat Czy istnieje odbiornik zmian jQuery DOM?
apsillers

Odpowiedzi:

86

Ostrzeżenie!

Ta odpowiedź jest teraz nieaktualna. DOM Level 4 wprowadził MutationObserver , zapewniając skuteczny zamiennik przestarzałych zdarzeń mutacji. Zobacz tę odpowiedź, aby uzyskać lepsze rozwiązanie niż przedstawione tutaj. Poważnie. Nie sonduj DOMu co 100 milisekund; zmarnuje moc procesora, a Twoi użytkownicy będą Cię nienawidzić.

Ponieważ zdarzenia mutacji zostały wycofane w 2012 roku i nie masz kontroli nad wstawionymi elementami, ponieważ są one dodawane przez kod innej osoby, jedyną opcją jest ciągłe sprawdzanie ich obecności.

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

Po wywołaniu tej funkcji będzie ona działać co 100 milisekund, czyli 1/10 (jedna dziesiąta) sekundy. Jeśli nie potrzebujesz obserwacji elementów w czasie rzeczywistym, to powinno wystarczyć.

Jose Faeti
źródło
1
jose, jak zakończyć swój settimeout, gdy warunek jest rzeczywiście spełniony? tzn. znalazłeś element, który w końcu załadował się x sekund później na twojej stronie?
klewis
1
@blachawk musisz przypisać wartość zwracaną przez setTimeout do zmiennej, którą możesz później przekazać jako paratemer do clearTimeout (), aby ją wyczyścić.
Jose Faeti
3
@blackhawk: Właśnie zobaczyłem tę odpowiedź i dałem jej +1; ale możesz być świadomy, że w rzeczywistości nie musisz tutaj używać clearTimeout (), ponieważ setTimeout () działa tylko raz! Jedno „if-else” jest po prostu wystarczające: nie pozwól, aby wykonanie przeszło do setTimeout () po znalezieniu wstawionego elementu.
1111161171159459134
Przydatny praktyczny przewodnik po używaniu MutationObserver(): davidwalsh.name/mutationobserver-api
gfullam
4
To jest brudne. Czy naprawdę nie ma „odpowiednika” dla AS3 Event.ADDED_TO_STAGE?
Bitterblue
19

Pomiędzy wycofywaniem się zdarzeń mutacji a pojawieniem się MutationObserver, skutecznym sposobem powiadamiania o dodaniu określonego elementu do DOM było wykorzystanie zdarzeń animacji CSS3 .

Cytując wpis na blogu:

Skonfiguruj sekwencję klatek kluczowych CSS, która będzie kierowana (poprzez wybór selektora CSS) na dowolne elementy DOM, dla których chcesz otrzymać zdarzenie wstawienia węzła DOM. Użyłem stosunkowo łagodnej i mało używanej właściwości css, klipu. Użyłem koloru konturu, aby uniknąć bałaganu z zamierzonymi stylami strony - kod był kiedyś ukierunkowany na właściwość clip, ale od wersji 11. nie można go już animować w IE. powiedział, że każda właściwość, którą można animować, będzie działać, wybierz tę, którą lubisz.

Następnie dodałem detektor startu animacji obejmujący cały dokument, którego używam jako delegat do przetwarzania wstawień węzłów. Zdarzenie animacji ma właściwość o nazwie animationName, która informuje, która sekwencja klatek kluczowych rozpoczęła animację. Po prostu upewnij się, że właściwość animationName jest taka sama, jak nazwa sekwencji klatek kluczowych dodana do wstawiania węzłów i gotowe.

jokeyrhyme
źródło
Jest to rozwiązanie o najwyższej wydajności, a odpowiedź w zduplikowanym pytaniu dotyczy biblioteki.
Dan Dascalescu
1
Niedoceniona odpowiedź.
Gökhan Kurt
Ostrożny. Jeśli ważna jest precyzja w milisekundach, to podejście jest dalekie od dokładności. Ten jsbin pokazuje, że istnieje więcej niż 30ms różnica między zwrotnego inline i za pomocą animationstart, jsbin.com/netuquralu/1/edit .
Gajus
Zgadzam się z Kurtem, to niedoceniane.
Zabbu,
13

Możesz użyć livequerywtyczki do jQuery. Możesz podać wyrażenie selektora, takie jak:

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

Za każdym razem, gdy removeItemButtondodawany jest przycisk klasy, na pasku stanu pojawia się komunikat.

Jeśli chodzi o wydajność, możesz chcieć tego uniknąć, ale w każdym przypadku możesz wykorzystać wtyczkę zamiast tworzyć własne programy obsługi zdarzeń.

Ponownie odwiedzona odpowiedź

Powyższa odpowiedź miała na celu jedynie wykrycie, że element został dodany do DOM poprzez wtyczkę.

Jednak najprawdopodobniej jQuery.on()podejście byłoby bardziej odpowiednie, na przykład:

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

Jeśli masz dynamiczną zawartość, która powinna odpowiadać na przykład na kliknięcia, najlepiej powiązać zdarzenia z kontenerem nadrzędnym za pomocą jQuery.on.

Zero rozpraszania
źródło
11

ETA 24 kwietnia 17 Chciałem to trochę uprościć za pomocą niektórych async/ awaitmagii, ponieważ czyni to o wiele bardziej zwięzłym:

Używając tego samego obiecanego-obserwowalnego:

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

Twoja funkcja dzwonienia może być tak prosta, jak:

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

Jeśli chcesz dodać limit czasu, możesz użyć prostego Promise.racewzorca, jak pokazano tutaj :

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

Oryginalny

Możesz to zrobić bez bibliotek, ale musiałbyś użyć niektórych rzeczy z ES6, więc pamiętaj o problemach ze zgodnością (tj. Jeśli twoją publicznością są głównie Amisze, luddite lub, co gorsza, użytkownicy IE8)

Najpierw użyjemy interfejsu API MutationObserver do skonstruowania obiektu obserwatora. Owiniemy ten obiekt obietnicą, a resolve()kiedy wywołanie zwrotne zostanie uruchomione (h / t davidwalshblog) artykuł na blogu Davida Walsha o mutacjach :

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

Następnie utworzymy plik generator function. Jeśli jeszcze ich nie użyłeś, to przegapiłeś - ale krótkie podsumowanie jest takie: działa jak funkcja synchronizacji, a kiedy znajdzie yield <Promise>wyrażenie, czeka w sposób nieblokujący, aż obietnica się pojawi spełnione ( Generatory robią więcej niż to, ale to nas tutaj interesuje ).

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

Trudną częścią generatorów jest to, że nie „zwracają” jak normalna funkcja. Więc użyjemy funkcji pomocniczej, aby móc używać generatora jak zwykłej funkcji. (ponownie, h / t do 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 );
            }
        }
    })();
}

Następnie, w dowolnym momencie, zanim może nastąpić oczekiwana mutacja DOM, po prostu uruchom runGenerator(getMutation).

Teraz możesz zintegrować mutacje DOM w synchronicznym przepływie sterowania. Co powiesz na to.

Brandon
źródło
8

Istnieje obiecująca biblioteka javascript o nazwie Arrive, która wygląda na świetny sposób na rozpoczęcie korzystania z obserwatorów mutacji, gdy obsługa przeglądarki stanie się powszechna.

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

Dave Kiss
źródło
3

Sprawdź tę wtyczkę, która robi dokładnie to - jquery.initialize

Działa dokładnie jak każda funkcja. Różnica polega na tym, że bierze selektor, który wprowadziłeś i szuka nowych pozycji dodanych w przyszłości pasujących do tego selektora i inicjalizuje je

Inicjalizacja wygląda następująco

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

Ale teraz, jeśli nowy .some-elementselektor dopasowania elementu pojawi się na stronie, zostanie on natychmiast zainicjowany.

Sposób dodawania nowej pozycji nie jest ważny, nie musisz przejmować się żadnymi callbackami itp.

Więc jeśli dodasz nowy element, taki jak:

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

zostanie natychmiast zainicjowany.

Plugin jest oparty na MutationObserver

pie6k
źródło
0

Czyste rozwiązanie javascript (bez 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'));
vedant
źródło