Função fazer esperar até que o elemento exista


159

Estou tentando adicionar uma tela sobre outra tela - como posso fazer com que essa função aguarde até que a primeira tela seja criada?

function PaintObject(brush) {

    this.started = false;

    // get handle of the main canvas, as a DOM object, not as a jQuery Object. Context is unfortunately not yet
    // available in jquery canvas wrapper object.
    var mainCanvas = $("#" + brush).get(0);

    // Check if everything is ok
    if (!mainCanvas) {alert("canvas undefined, does not seem to be supported by your browser");}
    if (!mainCanvas.getContext) {alert('Error: canvas.getContext() undefined !');}

    // Get the context for drawing in the canvas
    var mainContext = mainCanvas.getContext('2d');
    if (!mainContext) {alert("could not get the context for the main canvas");}

    this.getMainCanvas = function () {
        return mainCanvas;
    }
    this.getMainContext = function () {
        return mainContext;
    }

    // Prepare a second canvas on top of the previous one, kind of second "layer" that we will use
    // in order to draw elastic objects like a line, a rectangle or an ellipse we adjust using the mouse
    // and that follows mouse movements
    var frontCanvas = document.createElement('canvas');
    frontCanvas.id = 'canvasFront';
    // Add the temporary canvas as a second child of the mainCanvas parent.
    mainCanvas.parentNode.appendChild(frontCanvas);

    if (!frontCanvas) {
        alert("frontCanvas null");
    }
    if (!frontCanvas.getContext) {
        alert('Error: no frontCanvas.getContext!');
    }
    var frontContext = frontCanvas.getContext('2d');
    if (!frontContext) {
        alert("no TempContext null");
    }

    this.getFrontCanvas = function () {
        return frontCanvas;
    }
    this.getFrontContext = function () {
        return frontContext;
    }

4
Quando você cria a tela ao clicar, execute a função ou dispara um evento que executa um manipulador que executa a função. não há evento interno do navegador que ocorra quando um elemento estiver disponível.
Kevin B

Respostas:


304

Se você tiver acesso ao código que cria a tela - basta chamar a função logo após a criação da tela.

Se você não tem acesso a esse código (por exemplo, se é um código de terceiros, como o google maps), o que você pode fazer é testar a existência em um intervalo:

var checkExist = setInterval(function() {
   if ($('#the-canvas').length) {
      console.log("Exists!");
      clearInterval(checkExist);
   }
}, 100); // check every 100ms

Mas observe: muitas vezes o código de terceiros tem uma opção para ativar seu código (por retorno de chamada ou acionamento de evento) quando ele terminar de carregar. Pode ser aí que você pode colocar sua função. A solução de intervalo é realmente uma solução ruim e deve ser usada apenas se nada mais funcionar.


solução perfeita para uso no typularhead angularjs. Obrigado por me guiar na direção certa!
JuanTrev 27/10/2014

1
Excelente solução para esperar que algo seja criado pelo Ajax antes de colocar outra coisa lá. Muito obrigado.
Countzero

@iftah Como eu faria isso funcionar se o seletor fosse uma variável? Além disso, se for um seletor de ID ou classe, também será alterado. Às vezes, existem vários elementos retornados quando eu seleciono com uma classe, e eu precisaria encontrar uma maneira de passar um índice para o seletor para descobrir qual deles. Como eu faria isso? Obrigado
Kragalon

@ Kraglon, essa é uma pergunta completamente diferente e não é adequada para comentários desta resposta. Eu sugiro que você faça uma nova pergunta, explique o que você tentou, qual é o problema, etc. ...
Iftah 17/03

8
Mais uma coisa é importante mencionar ao usar a solução fornecida, você deve ter esse pedaço de código dentro de um loop for e definir um contador máximo de novas tentativas, se algo der errado, você não terá um loop infinito :)
BJ

48

Dependendo do navegador que você precisa oferecer suporte, existe a opção de MutationObserver .

EDIT: Todos os principais navegadores suportam o MutationObserver agora .

Algo nesse sentido deve fazer o truque:

// callback executed when canvas was found
function handleCanvas(canvas) { ... }

// set up the mutation observer
var observer = new MutationObserver(function (mutations, me) {
  // `mutations` is an array of mutations that occurred
  // `me` is the MutationObserver instance
  var canvas = document.getElementById('my-canvas');
  if (canvas) {
    handleCanvas(canvas);
    me.disconnect(); // stop observing
    return;
  }
});

// start observing
observer.observe(document, {
  childList: true,
  subtree: true
});

NB: Eu não testei esse código, mas essa é a ideia geral.

Você pode estender isso facilmente para pesquisar apenas a parte do DOM que foi alterada. Para isso, use o mutationsargumento, é uma matriz de MutationRecordobjetos.


2
Adorei esse. Obrigado.
insira 26/03/19

1
Esse padrão é realmente útil em muitas instâncias, especialmente se você estiver inserindo JS em uma página e não souber se outros itens estão carregados.
Para o nome

1
A melhor resposta! Obrigado!
Antony Hatchkins

1
Estou preso com um navegador antigo (ff38) e isso me salvou.
Jung rhew # 17/10/19

1
Isso é incrível! Eu gostaria de saber que isso existia antes.
Rob

39

Isso funcionará apenas com navegadores modernos, mas acho mais fácil usar um then, por isso teste primeiro, mas:

Código

function rafAsync() {
    return new Promise(resolve => {
        requestAnimationFrame(resolve); //faster than set time out
    });
}

function checkElement(selector) {
    if (document.querySelector(selector) === null) {
        return rafAsync().then(() => checkElement(selector));
    } else {
        return Promise.resolve(true);
    }
}

Ou usando funções de gerador

async function checkElement(selector) {
    const querySelector = document.querySelector(selector);
    while (querySelector === null) {
        await rafAsync()
    }
    return querySelector;
}  

Uso

checkElement('body') //use whichever selector you want
.then((element) => {
     console.info(element);
     //Do whatever you want now the element is there
});

Há um erro. Ao usar funções de gerador, o querySelector deve ser atualizado a cada loop:while (document.querySelector(selector) === null) {await rafAsync()}
haofly

31

Uma abordagem mais moderna para aguardar elementos:

while(!document.querySelector(".my-selector")) {
  await new Promise(r => setTimeout(r, 500));
}
// now the element is loaded

Observe que esse código precisaria ser agrupado em uma função assíncrona .


4
isso é muito legal!
Dexter Bengil

O que tem rai?
Daniel Möller

Bem, ok, mas de onde vem? O que isso faz? Para o que você está enviando setTimeout?
Daniel Möller

@ DanielMöller, talvez seja necessário dar uma olhada no Promises para entender melhor esse código. Basicamente, o que o código faz aqui é configurar um tempo limite de 500ms e aguardar a conclusão antes de iniciar uma nova iteração do loop while. Solução inteligente!
ClementParis016 28/04

8

Aqui está uma pequena melhoria em relação à resposta de Jamie Hutber

const checkElement = async selector => {

while ( document.querySelector(selector) === null) {
    await new Promise( resolve =>  requestAnimationFrame(resolve) )
}

return document.querySelector(selector); };

8

É melhor retransmitir do requestAnimationFrameque em um setTimeout. esta é a minha solução nos módulos es6 e usando Promises.

es6, módulos e promessas:

// onElementReady.js
const onElementReady = $element => (
  new Promise((resolve) => {
    const waitForElement = () => {
      if ($element) {
        resolve($element);
      } else {
        window.requestAnimationFrame(waitForElement);
      }
    };
    waitForElement();
  })
);

export default onElementReady;

// in your app
import onElementReady from './onElementReady';

const $someElement = document.querySelector('.some-className');
onElementReady($someElement)
  .then(() => {
    // your element is ready
  }

plain js and promises:

var onElementReady = function($element) {
  return new Promise((resolve) => {
    var waitForElement = function() {
      if ($element) {
        resolve($element);
      } else {
        window.requestAnimationFrame(waitForElement);
      }
    };
    waitForElement();
  })
};

var $someElement = document.querySelector('.some-className');
onElementReady($someElement)
  .then(() => {
    // your element is ready
  });

Uncaught TypeError: Cannot read property 'then' of undefined
Jeff Puckett

Acho que perdi um retorno ... antes da nova promessa.
Ncubica 23/10

1
Esta é a solução adequada, muito melhor do que todas as verificações periódicas baseadas em temporizador.
András Szepesházi

4
Na verdade, isso não funciona em sua forma atual. Se $ someElement for inicialmente nulo (ou seja, ainda não estiver presente no DOM), você passará esse valor nulo (em vez do seletor CSS) para sua função onElementReady e o elemento nunca será resolvido. Em vez disso, passe o seletor CSS como texto e tente obter referência ao elemento via .querySelector em cada passagem.
András Szepesházi 10/02

1
Isso funcionou muito bem para o meu caso de uso e parece uma solução adequada para mim. Graças
rdhaese

5

Aqui está uma solução usando observáveis.

waitForElementToAppear(elementId) {                                          

    return Observable.create(function(observer) {                            
            var el_ref;                                                      
            var f = () => {                                                  
                el_ref = document.getElementById(elementId);                 
                if (el_ref) {                                                
                    observer.next(el_ref);                                   
                    observer.complete();                                     
                    return;                                                  
                }                                                            
                window.requestAnimationFrame(f);                             
            };                                                               
            f();                                                             
        });                                                                  
}                                                                            

Agora você pode escrever

waitForElementToAppear(elementId).subscribe(el_ref => doSomethingWith(el_ref);

4

Você pode verificar se o dom já existe definindo um tempo limite até que ele já seja renderizado no dom.

var panelMainWrapper = document.getElementById('panelMainWrapper');
setTimeout(function waitPanelMainWrapper() {
    if (document.body.contains(panelMainWrapper)) {
        $("#panelMainWrapper").html(data).fadeIn("fast");
    } else {
        setTimeout(waitPanelMainWrapper, 10);
    }
}, 10);

3

Se você deseja uma solução genérica usando MutationObserver, pode usar esta função

// MIT Licensed
// Author: jwilson8767

/**
 * Waits for an element satisfying selector to exist, then resolves promise with the element.
 * Useful for resolving race conditions.
 *
 * @param selector
 * @returns {Promise}
 */
export function elementReady(selector) {
  return new Promise((resolve, reject) => {
    const el = document.querySelector(selector);
    if (el) {resolve(el);}
    new MutationObserver((mutationRecords, observer) => {
      // Query for elements matching the specified selector
      Array.from(document.querySelectorAll(selector)).forEach((element) => {
        resolve(element);
        //Once we have resolved we don't need the observer anymore.
        observer.disconnect();
      });
    })
      .observe(document.documentElement, {
        childList: true,
        subtree: true
      });
  });
}

Fonte: https://gist.github.com/jwilson8767/db379026efcbd932f64382db4b02853e
Exemplo de como usá-lo

elementReady('#someWidget').then((someWidget)=>{someWidget.remove();});

Nota: MutationObserver tem um ótimo suporte ao navegador; https://caniuse.com/#feat=mutationobserver

Et voilà! :)


2

Outra variação de Iftah

var counter = 10;
var checkExist = setInterval(function() {
  console.log(counter);
  counter--
  if ($('#the-canvas').length || counter === 0) {
    console.log("by bye!");
    clearInterval(checkExist);
  }
}, 200);

Caso o elemento nunca seja mostrado, não verificamos infinitamente.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.